2009-05-20 68 views
9

我正在C#中爲WinForms應用程序實現模糊日期控件。模糊日期應該能夠採取模糊值就像C#.NET中的模糊日期時間選擇器控件?

  • 去年六月
  • 2小時前
  • 2個月前
  • 上週
  • 昨天
  • 去年

是否有任何「模糊」日期時間選取器的示例實現?

任何實現這樣的控制思想,將不勝感激

PS: 我知道模糊日期算法談到herehere,我期待爲開發任何的想法和靈感這樣的控制

+1

作爲一個側面問題,假設您的代碼覆蓋了您的所有案例,用戶將如何知道他們可以鍵入的內容?在任務完成時間方面,如何打字昨天比使用日期選擇器更快? 我會很感興趣知道爲什麼你認爲你需要這樣的控制? – RichardOD 2009-05-20 10:35:58

+0

需要? - 那麼控制將用於輸入/值將從基於定時器的實體獲取的場景。微不足道的例子:「你什麼時候把蛋糕放在微波爐裏?」我覺得進入「25分鐘前」[25是微波計時器的讀數]要比手動計算CurrentTime減去25分鐘容易得多。 如果輸入模糊值比手動計算日期時間值更容易,則使用模糊日期時間選擇器。 唷..那很長.. – abhilash 2009-05-20 11:14:21

回答

21

解析是很容易的。它可以作爲一堆正則表達式和一些日期計算來實現。

下面的示例可以很容易地擴展以滿足您的需求。 我大概測試了它和它的作品至少在以下字符串:

  • 下個月,明年,
  • 接下來的4個月,未來3天
  • 3天前,4小時前
  • 明天,昨天
  • ,去年最後一個月,
  • 最後週二,週五旁邊
  • 去年六月,明年5月,
  • 2008年1月,2009年1月1日,
  • 2019年6月,2009/01/01

的輔助類:

class FuzzyDateTime 
{ 

    static List<string> dayList = new List<string>() { "sun", "mon", "tue", "wed", "thu", "fri", "sat" }; 
    static List<IDateTimePattern> parsers = new List<IDateTimePattern>() 
    { 
     new RegexDateTimePattern (
      @"next +([2-9]\d*) +months", 
      delegate (Match m) { 
       var val = int.Parse(m.Groups[1].Value); 
       return DateTime.Now.AddMonths(val); 
      } 
     ), 
     new RegexDateTimePattern (
      @"next +month", 
      delegate (Match m) { 
       return DateTime.Now.AddMonths(1); 
      } 
     ),   
     new RegexDateTimePattern (
      @"next +([2-9]\d*) +days", 
      delegate (Match m) { 
       var val = int.Parse(m.Groups[1].Value); 
       return DateTime.Now.AddDays(val); 
      } 
     ), 

     new RegexDateTimePattern (
      @"([2-9]\d*) +months +ago", 
      delegate (Match m) { 
       var val = int.Parse(m.Groups[1].Value); 
       return DateTime.Now.AddMonths(-val); 
      } 
     ), 
     new RegexDateTimePattern (
      @"([2-9]\d*) days +ago", 
      delegate (Match m) { 
       var val = int.Parse(m.Groups[1].Value); 
       return DateTime.Now.AddDays(-val); 
      } 
     ), 
     new RegexDateTimePattern (
      @"([2-9]\d*) *h(ours)? +ago", 
      delegate (Match m) { 
       var val = int.Parse(m.Groups[1].Value); 
       return DateTime.Now.AddMonths(-val); 
      } 
     ), 
     new RegexDateTimePattern (
      @"tomorrow", 
      delegate (Match m) { 
       return DateTime.Now.AddDays(1); 
      } 
     ), 
     new RegexDateTimePattern (
      @"today", 
      delegate (Match m) { 
       return DateTime.Now; 
      } 
     ), 
     new RegexDateTimePattern (
      @"yesterday", 
      delegate (Match m) { 
       return DateTime.Now.AddDays(-1); 
      } 
     ), 
     new RegexDateTimePattern (
      @"(last|next) *(year|month)", 
      delegate (Match m) { 
       int direction = (m.Groups[1].Value == "last")? -1 :1; 
       switch(m.Groups[2].Value) 
       { 
        case "year": 
         return new DateTime(DateTime.Now.Year+direction, 1,1); 
        case "month": 
         return new DateTime(DateTime.Now.Year, DateTime.Now.Month+direction, 1); 
       } 
       return DateTime.MinValue; 
      } 
     ), 
     new RegexDateTimePattern (
      String.Format(@"(last|next) *({0}).*", String.Join("|", dayList.ToArray())), //handle weekdays 
      delegate (Match m) { 
       var val = m.Groups[2].Value; 
       var direction = (m.Groups[1].Value == "last")? -1 :1; 
       var dayOfWeek = dayList.IndexOf(val.Substring(0,3)); 
       if (dayOfWeek >= 0) { 
        var diff = direction*(dayOfWeek - (int)DateTime.Today.DayOfWeek); 
        if (diff <= 0) { 
         diff = 7 + diff; 
        } 
        return DateTime.Today.AddDays(direction * diff); 
       } 
       return DateTime.MinValue; 
      } 
     ), 

     new RegexDateTimePattern (
      @"(last|next) *(.+)", // to parse months using DateTime.TryParse 
      delegate (Match m) { 
       DateTime dt; 
       int direction = (m.Groups[1].Value == "last")? -1 :1; 
       var s = String.Format("{0} {1}",m.Groups[2].Value, DateTime.Now.Year + direction); 
       if (DateTime.TryParse(s, out dt)) { 
        return dt; 
       } else { 
        return DateTime.MinValue; 
       } 
      } 
     ), 
     new RegexDateTimePattern (
      @".*", //as final resort parse using DateTime.TryParse 
      delegate (Match m) { 
       DateTime dt; 
       var s = m.Groups[0].Value; 
       if (DateTime.TryParse(s, out dt)) { 
        return dt; 
       } else { 
        return DateTime.MinValue; 
       } 
      } 
     ), 
    }; 

    public static DateTime Parse(string text) 
    { 
     text = text.Trim().ToLower(); 
     var dt = DateTime.Now; 
     foreach (var parser in parsers) 
     { 
      dt = parser.Parse(text); 
      if (dt != DateTime.MinValue) 
       break; 
     } 
     return dt; 
    } 
} 
interface IDateTimePattern 
{ 
    DateTime Parse(string text); 
} 

class RegexDateTimePattern : IDateTimePattern 
{ 
    public delegate DateTime Interpreter(Match m); 
    protected Regex regEx; 
    protected Interpreter inter; 
    public RegexDateTimePattern(string re, Interpreter inter) 
    { 
     this.regEx = new Regex(re); 
     this.inter = inter; 
    } 
    public DateTime Parse(string text) 
    { 
     var m = regEx.Match(text); 

     if (m.Success) 
     { 
      return inter(m); 
     } 
     return DateTime.MinValue; 
    } 
} 

用例:

var val = FuzzyDateTime.Parse(textBox1.Text); 
if (val != DateTime.MinValue) 
    label1.Text = val.ToString(); 
else 
    label1.Text = "unknown value"; 
2

我們有一個類似的控件。我們只是添加一個組合框列表 - 控件來選擇你的選擇。

PeriodSelector:

  • 從[日期選擇器]到[日期選擇器]
  • [的NumericUpDown]個月前
  • [的NumericUpDown]小時前
  • 上週
  • 昨天
  • 周[日期選擇器]
  • 日期[日期選擇器]
  • ...

而只是採取適合您的目的的選擇。

實現這個然後解析文本要容易得多。計算相當簡單。

重要的是看到您正在選擇期間。去年意味着從2008年1月> 2008年12月。兩個小時前從現在開始直到現在 - 兩個小時。等

3

之一我們的用戶使用的系統允許他們輸入像這樣的日期:

  • Ť//今天
  • T + 1 //今天加/減的天數
  • T +1瓦特//今天加/減數週
  • T +1米//今天加/減數月
  • T + 1Y //今天加/減若干年

他們似乎喜歡它,並要求在我們的應用程序,所以我想出了下面的代碼。 ParseDateToString將採用上述形式之一的字符串以及其他一些形式,計算日期並以「MM/DD/YYYY」格式返回。很容易將其更改爲返回實際的DateTime對象,以及添加對小時,分鐘,秒或任何您想要的支持。

using System; 
using System.Text.RegularExpressions; 

namespace Utils 
{ 
    class DateParser 
    { 
     private static readonly DateTime sqlMinDate = DateTime.Parse("01/01/1753"); 
     private static readonly DateTime sqlMaxDate = DateTime.Parse("12/31/9999"); 
     private static readonly Regex todayPlusOrMinus = new Regex(@"^\s*t(\s*[\-\+]\s*\d{1,4}([dwmy])?)?\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); // T +/- number of days 
     private static readonly Regex dateWithoutSlashies = new Regex(@"^\s*(\d{6}|\d{8})\s*$", RegexOptions.Compiled); // Date in MMDDYY or MMDDYYYY format 

     private const string DATE_FORMAT = "MM/dd/yyyy"; 

     private const string ERROR_INVALID_SQL_DATE_FORMAT = "Date must be between {0} and {1}!"; 
     private const string ERROR_DATE_ABOVE_MAX_FORMAT = "Date must be on or before {0}!"; 
     private const string ERROR_USAGE = @"Unable to determine date! Please enter a valid date as either: 
    MMDDYY 
    MMDDYYYY 
    MM/DD/YY 
    MM/DD/YYYY 

You may also use the following: 
    T (Today's date) 
    T + 1 (Today plus/minus a number of days) 
    T + 1w (Today plus/minus a number of weeks) 
    T + 1m (Today plus/minus a number of months) 
    T + 1y (Today plus/minus a number of years)"; 

     public static DateTime SqlMinDate 
     { 
      get { return sqlMinDate; } 
     } 

     public static DateTime SqlMaxDate 
     { 
      get { return sqlMaxDate; } 
     } 

     /// <summary> 
     /// Determine if user input string can become a valid date, and if so, returns it as a short date (MM/dd/yyyy) string. 
     /// </summary> 
     /// <param name="dateString"></param> 
     /// <returns></returns> 
     public static string ParseDateToString(string dateString) 
     { 
      return ParseDateToString(dateString, sqlMaxDate); 
     } 

     /// <summary> 
     /// Determine if user input string can become a valid date, and if so, returns it as a short date (MM/dd/yyyy) string. Date must be on or before maxDate. 
     /// </summary> 
     /// <param name="dateString"></param> 
     /// <param name="maxDate"></param> 
     /// <returns></returns> 
     public static string ParseDateToString(string dateString, DateTime maxDate) 
     { 
      if (null == dateString || 0 == dateString.Trim().Length) 
      { 
       return null; 
      } 

      dateString = dateString.ToLower(); 

      DateTime dateToReturn; 

      if (todayPlusOrMinus.IsMatch(dateString)) 
      { 
       dateToReturn = DateTime.Today; 

       int amountToAdd; 
       string unitsToAdd; 

       GetAmountAndUnitsToModifyDate(dateString, out amountToAdd, out unitsToAdd); 

       switch (unitsToAdd) 
       { 
        case "y": 
         { 
          dateToReturn = dateToReturn.AddYears(amountToAdd); 
          break; 
         } 
        case "m": 
         { 
          dateToReturn = dateToReturn.AddMonths(amountToAdd); 
          break; 
         } 
        case "w": 
         { 
          dateToReturn = dateToReturn.AddDays(7 * amountToAdd); 
          break; 
         } 
        default: 
         { 
          dateToReturn = dateToReturn.AddDays(amountToAdd); 
          break; 
         } 
       } 
      } 
      else 
      { 
       if (dateWithoutSlashies.IsMatch(dateString)) 
       { 
        /* 
        * It was too hard to deal with 3, 4, 5, and 7 digit date strings without slashes, 
        * so I limited it to 6 (MMDDYY) or 8 (MMDDYYYY) to avoid ambiguity. 
        * For example, 12101 could be: 
        *  1/21/01 => Jan 21, 2001 
        *  12/1/01 => Dec 01, 2001 
        *  12/10/1 => Dec 10, 2001 
        * 
        * Limiting it to 6 or 8 digits is much easier to deal with. Boo hoo if they have to 
        * enter leading zeroes. 
        */ 

        // All should parse without problems, since we ensured it was a string of digits 
        dateString = dateString.Insert(4, "/").Insert(2, "/"); 
       } 

       try 
       { 
        dateToReturn = DateTime.Parse(dateString); 
       } 
       catch 
       { 
        throw new FormatException(ERROR_USAGE); 
       } 
      } 

      if (IsDateSQLValid(dateToReturn)) 
      { 
       if (dateToReturn <= maxDate) 
       { 
        return dateToReturn.ToString(DATE_FORMAT); 
       } 

       throw new ApplicationException(string.Format(ERROR_DATE_ABOVE_MAX_FORMAT, maxDate.ToString(DATE_FORMAT))); 
      } 

      throw new ApplicationException(String.Format(ERROR_INVALID_SQL_DATE_FORMAT, SqlMinDate.ToString(DATE_FORMAT), SqlMaxDate.ToString(DATE_FORMAT))); 
     } 

     /// <summary> 
     /// Converts a string of the form: 
     /// 
     /// "T [+-] \d{1,4}[dwmy]" (spaces optional, case insensitive) 
     /// 
     /// to a number of days/weeks/months/years to add/subtract from the current date. 
     /// </summary> 
     /// <param name="dateString"></param> 
     /// <param name="amountToAdd"></param> 
     /// <param name="unitsToAdd"></param> 
     private static void GetAmountAndUnitsToModifyDate(string dateString, out int amountToAdd, out string unitsToAdd) 
     { 
      GroupCollection groups = todayPlusOrMinus.Match(dateString).Groups; 

      amountToAdd = 0; 
      unitsToAdd = "d"; 

      string amountWithPossibleUnits = groups[1].Value; 
      string possibleUnits = groups[2].Value; 

      if (null == amountWithPossibleUnits || 
       0 == amountWithPossibleUnits.Trim().Length) 
      { 
       return; 
      } 

      // Strip out the whitespace 
      string stripped = Regex.Replace(amountWithPossibleUnits, @"\s", ""); 

      if (null == possibleUnits || 
       0 == possibleUnits.Trim().Length) 
      { 
       amountToAdd = Int32.Parse(stripped); 
       return; 
      } 

      // Should have a parseable integer followed by a units indicator (d/w/m/y) 
      // Remove the units indicator from the end, so we have a parseable integer. 
      stripped = stripped.Remove(stripped.LastIndexOf(possibleUnits)); 

      amountToAdd = Int32.Parse(stripped); 
      unitsToAdd = possibleUnits; 
     } 

     public static bool IsDateSQLValid(string dt) { return IsDateSQLValid(DateTime.Parse(dt)); } 

     /// <summary> 
     /// Make sure the range of dates is valid for SQL Server 
     /// </summary> 
     /// <param name="dt"></param> 
     /// <returns></returns> 
     public static bool IsDateSQLValid(DateTime dt) 
     { 
      return (dt >= SqlMinDate && dt <= SqlMaxDate); 
     } 
    } 
} 

在列表中唯一的例子,可能是困難的將是「去年六月」,但你可以只計算字符串搞清楚它的多少個月以來去年六月通過英寸

int monthDiff = (DateTime.Now.Month + 6) % 12; 

if(monthDiff == 0) monthDiff = 12; 
string lastJuneCode = string.Format("T - {0}m", monthDiff); 

當然,這將依賴於日期時間的AddMonths功能的準確性,我還沒有真正測試的邊緣情況了點。它應該在去年六月給你一個DateTime,你可以用它來查找本月的第一個和最後一個。

其他所有東西都應該很容易用正則表達式映射或分析。例如:

  • 上週=> 「T - 1瓦特」
  • 昨日=> 「T - 1D」
  • 去年=> 「T - 1Y」
  • 下週=>「T +1瓦特」
  • 明天=> 「T + 1D」
  • 明年=> 「T + 1Y」
0

有在彼得·Czapla的回答了一個錯誤:

new RegexDateTimePattern (
      @"([2-9]\d*) *h(ours)? +ago", 
      delegate (Match m) { 
       var val = int.Parse(m.Groups[1].Value); 
       return DateTime.Now.AddMonths(-val); 
      } 
     ), 

AddMonths用來代替AddHours()。

PS:我不能評論他的答案,因爲論壇的觀點很低。我已經浪費時間去調試它,爲什麼當我嘗試「5小時前」時,它會移除5天。