2009-05-20 5 views
9

Ich implementiere ein Fuzzy-Datum-Steuerelement in C# für eine winforms-Anwendung. Das Fuzzy-Datum soll vor nehmen kann Fuzzy-Werte wieFuzzy-Datum-Zeit-Picker-Steuerelement in C# .NET?

  • Im Juni letztes Jahr
  • 2 Stunden
  • 2 Monate
  • Letzte Woche
  • Gestern
  • Im vergangenen Jahr

und dergleichen

Gibt es Beispiel-Implementierungen von "Fuzzy" -Datenzeit-Pickern? Ich bin mir bewusst, der Fuzzy-Datum Algorithmus gesprochen über here und here, die ich für die Entwicklung eines solchen für alle Ideen und Inspirationen suchen bin wirklich:

Irgendwelche Ideen, eine solche Kontrolle zu implementieren würde

PS geschätzt werden Kontrolle

+1

Als Nebenfrage, angenommen, Sie hatten Code, der alle Ihre Fälle abgedeckt, wie würde der Benutzer wissen, was sie eingeben könnten? In Bezug auf die Zeit der Aufgabenbeendigung, wie würde das Tippen gestern schneller sein als die Verwendung einer Datumsauswahl? Ich wäre sehr interessiert zu wissen, warum Sie denken, Sie brauchen eine solche Kontrolle? – RichardOD

+0

Benötigen? - Nun würde das Steuerelement in Szenarien verwendet werden, in denen die Eingabe/der Wert von zeitbasierten Entitäten abgerufen würde. Trivial Beispiel: "Wann hast du den Kuchen in der Mikrowelle aufbewahrt?" Ich denke, es ist viel einfacher, "vor 25 Minuten" einzugeben [25 ist der Wert auf dem Mikrowellentimer] als Manuell die aktuelle Zeit minus 25 Minuten zu berechnen. Die Fuzzy Date Time-Auswahl würde in solchen Szenarien verwendet werden, in denen die Eingabe von Fuzzy-Werten einfacher wäre als die manuelle Berechnung von Date Time-Werten. Puh .. Das war lang .. – abhilash

Antwort

21

Das Parsen ist ziemlich einfach. Es kann als eine Reihe von Regexps und einige Datumsberechnungen implementiert werden.

Das folgende Beispiel kann leicht erweitert werden, um Ihren Anforderungen zu entsprechen. Ich habe getestet es grob und es funktioniert zumindest für die folgenden Strings:

  • nächsten Monat, nächstes Jahr
  • nächste 4 Monate, 3 Tage
  • vor 3 Tagen, 5 Stunden vor
  • morgen, gestern
  • im letzten Jahr, im letzten Monat,
  • letzter Di, nächster fri
  • im Juni letztes Jahr, im Mai nächster Jahr,
  • Januar 2008, 1. Januar 2009,
  • Juni 2019, 2009.01.01

Die Hilfsklasse:

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; 
    } 
} 

Anwendungsbeispiel:

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

Requisiten für Sie, guter Herr! –

+0

Es scheint einen Fehler für '@" tomorrow "' zu geben, der mit '@" today "' identisch ist, wobei es 'return DateTime.Now.AddDays (1)' sein sollte. –

+0

@FreshCode Vielen Dank für diesen Fehler. (Ich habe den Eintrag korrigiert) –

2

Wir haben eine ähnliche Kontrolle. Wir fügen nur eine Liste von Kombinationsfeldern hinzu - Steuerelemente, um Ihre Auswahl zu treffen.

PeriodSelector:

  • Von [picker] Bis [picker]
  • [NumericUpDown] Monate vor
  • [NumericUpDown] Stunden
  • Letzte Woche
  • Gestern
  • Woche [ datepicker]
  • Tag [Datumpicker]
  • ...

Und nehmen Sie einfach die Auswahl, die für Ihren Zweck sinnvoll sind.

Es ist viel einfacher, dies zu implementieren, dann den Text zu analysieren. Die Berechnungen sind ziemlich einfach.

Es ist wichtig zu sehen, dass Sie eine Periode auswählen. Letztes Jahr bedeutet von Januar 2008> Dezember 2008. Vor zwei Stunden von jetzt bis jetzt - 2 Stunden. Usw.

3

Einer der Systeme, die unsere Benutzer benutzen, erlauben ihnen, Daten wie folgt einzugeben:

  • T // Heute
  • T + 1 // Heute plus/minus eine Anzahl von Tagen
  • T + 1W // Heute plus/minus eine Anzahl von Wochen
  • T + 1m // Heute plus/minus eine Anzahl von Monaten
  • T + 1J // Heute plus/minus einige Jahre

sie scheinen es zu mögen, und forderten sie in unserer App, so kam ich mit dem folgenden Code auf. ParseDateToString wird eine Zeichenfolge aus einem der obigen Formulare plus einigen anderen übernehmen, das Datum berechnen und es im "MM/TT/YYYY" -Format zurückgeben. Es ist einfach genug, es zu ändern, um das tatsächliche DateTime-Objekt zurückzugeben, sowie um Unterstützung für Stunden, Minuten, Sekunden oder was immer Sie möchten hinzuzufügen.

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); 
     } 
    } 
} 

Das einzige Beispiel in Ihrer Liste, die „June Last“ wäre schwierig sein könnte, aber man konnte nur die Zeichenfolge berechnen, indem herauszufinden, passieren, in wie viele Monate es seit Juni letzten Jahr gewesen ist.

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

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

Natürlich, das würde auf der Genauigkeit der AddMonths Funktion Datetime ab, und ich habe nicht wirklich Grenzfälle für das getestet. Es sollte Ihnen eine DateTime letzten Juni geben, und Sie könnten einfach diese verwenden, um den ersten und letzten des Monats zu finden.

Alles andere sollte mit regulären Ausdrücken relativ einfach zu parsen oder zu parsen sein.Zum Beispiel:

  • Letzte Woche => "t - 1W"
  • Gestern => "t - 1d"
  • Letztes Jahr => "t - 1J"
  • Nächste Woche => „t + 1W“
  • Tomorrow => "t + 1d"
  • Nächstes Jahr => "t + 1J"
0

Es gibt einen Bug der in Piotr Czapla Antwort lautet:

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 wird anstelle von AddHours() verwendet.

PS: Ich kann seine Antwort wegen niedriger Forum Punkte nicht kommentieren. Ich habe schon Zeit damit verbracht, es zu debuggen, warum es 5 Tage entfernt, wenn ich es mit "5 Stunden" versuche.