【问题标题】:Fuzzy Date Time Picker Control in C# .NET?C# .NET 中的模糊日期时间选择器控件?
【发布时间】:2009-05-20 10:27:23
【问题描述】:

我正在 C# 中为 winforms 应用程序实现模糊日期控件。模糊日期应该能够采用模糊值,例如

  • 去年六月
  • 2 小时前
  • 2 个月前
  • 上周
  • 昨天
  • 去年

之类的

是否有“模糊”日期时间选择器的示例实现?

任何实现这种控制的想法将不胜感激

附言: 我知道关于 herehere 的模糊日期算法,我真的在寻找开发这种控件的任何想法和灵感

【问题讨论】:

  • 作为一个附带问题,假设您的代码涵盖了所有案例,用户如何知道他们可以输入什么?就任务完成时间而言,昨天打字会比使用日期选择器更快吗?我很想知道你为什么认为你需要这样的控制?
  • 需要吗? -- 那么控件将用于从基于计时器的实体中获取输入/值的场景。小例子:“你什么时候把蛋糕放在微波炉里的?”我觉得输入“25 分钟前”[25 是微波计时器上的读数] 比手动计算 CurrentTime 减去 25 分钟要容易得多。当输入模糊值比手动计算日期时间值更容易时,将使用模糊日期时间选择器。唷..那很长..

标签: c# winforms datetime user-controls fuzzy


【解决方案1】:

解析非常简单。它可以实现为一堆正则表达式和一些日期计算。

可以轻松扩展以下示例以满足您的需求。 我已经对其进行了粗略的测试,它至少适用于以下字符串:

  • 下个月,明年,
  • 接下来的 4 个月,接下来的 3 天
  • 3 天前,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";

【讨论】:

  • @"tomorrow" 似乎有一个错误,与@"today" 相同,应该是return DateTime.Now.AddDays(1)
  • @FreshCode 感谢您发现该错误。 (我已经更正了列表)
  • 大家好,我冒昧地将这段代码放入我的github repo 之一。自述文件尚未准备好,但本文将作为起点提及。目前我围绕它进行单元测试并使其可测试。稍后,我想为其添加更多扩展。
  • 我已更新您的代码以添加“下一个/上周”的功能,我在下一个/最后一个块中发现了一个错误,将整数添加到一天/周/月/ year 可能导致 OutOfRangeException(将 1 添加到第 31 天超出范围),因此我将其更新为使用 .AddDays()、.AddWeeks() 和 .AddYears() 以及正向或负向。你可能想把我的叉子和你的合并,这里是github.com/omar-ebrahim/fuzzy-data-for-specflow。我也添加了相应的测试。
【解决方案2】:

我们的用户使用的其中一个系统允许他们像这样输入日期:

  • T // 今天
  • T + 1 // 今天加/减天数
  • T + 1w // 今天加上/减去几个星期
  • T + 1m // 今天加上/减去几个月
  • 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);
        }
    }
}

您的列表中唯一可能比较困难的示例是“Last June”,但您可以通过计算自去年 6 月以来的几个月来计算要传入的字符串。

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

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

当然,这将取决于 DateTime 的 AddMonths 函数的准确性,我还没有真正测试过边缘情况。它应该会在去年 6 月为您提供 DateTime,您可以使用它来查找该月的第一天和最后一个月。

其他一切都应该很容易用正则表达式映射或解析。例如:

  • 上周 => “t - 1w”
  • 昨天 => “t - 1d”
  • 去年 => “t - 1y”
  • 下周 => "t + 1w"
  • 明天 => "t + 1d"
  • 明年 => "t + 1y"

【讨论】:

    【解决方案3】:

    我们有类似的控制。我们只是添加一个组合框列表 - 控件供您选择。

    期间选择器:

    • 从 [datepicker] 到 [datepicker]
    • [numericupdown] 个月前
    • [numericupdown] 小时前
    • 上周
    • 昨天
    • 周 [日期选择器]
    • 日期 [日期选择器]
    • ...

    并且只选择对你的目的有意义的选择。

    实现这一点比解析文本要容易得多。计算相当简单。

    重要的是要看到您正在选择一个句号。去年意味着从 2008 年 1 月 > 2008 年 12 月。两个小时前从现在到现在 - 2 小时。等等。

    【讨论】:

      【解决方案4】:

      Piotr 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 天。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-11-16
        • 1970-01-01
        • 2011-09-26
        • 1970-01-01
        • 2016-04-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多