【问题标题】:Determine Event Recurrence Pattern for a set of dates确定一组日期的事件重复模式
【发布时间】:2011-05-07 10:44:01
【问题描述】:

我正在寻找一种模式、算法或库,它们将采用一组日期并在一个退出时返回重复的描述,即集合 [11-01-2010, 11-08-2010, 11- 15-2010, 11-22-2010, 11-29-2010] 会产生类似“11 月的每个星期一”。

以前有没有人见过这样的事情,或者对实现它的最佳方式有任何建议?

【问题讨论】:

  • 您的复发有多复杂?
  • 对于某些编程课程来说听起来像是一个非常不寻常且非常有趣的任务。 (我将很快教 java,我正在考虑将你的“问题”包括在作业集中)。 +1。
  • 看看我收到的一个(有点)类似问题的答案;也适用于您的情况:stackoverflow.com/questions/3165867/…
  • 序列中是否有多次重复[例如每周一和第二个周二]?
  • 这是一个很好的问题;真的让你想知道为什么这样的东西还不存在

标签: algorithm design-patterns data-structures data-modeling


【解决方案1】:

我认为你必须建造它,而且我认为这将是一个细节问题的项目。从获得更全面的要求开始。您想识别哪些日期模式?列出您希望算法成功识别的示例列表。编写您的算法以满足您的示例。将您的示例放入测试套件中,这样当您以后遇到不同的要求时,您可以确保没有破坏旧的要求。

我预计你会写 200 条 if-then-else 语句。

好的,我确实有一个想法。熟悉setsunionscoverageintersection等概念。列出您搜索的简短模式,例如“十月的每一天”、“十一月的每一天”和“十二月的每一天”。如果这些短模式包含在日期集中,则定义一个union 函数,该函数可以智能方式组合短模式。例如,假设您匹配了我上面提到的三种模式。如果你Union他们在一起,你会得到“从 10 月到 12 月的每一天”。您的目标可以是返回unions 中最简洁的setcover 您的一组日期或类似的东西。

【讨论】:

  • 然后,把它放在 git-hub 或其他东西上,这样下一个人就不必构建它了。
【解决方案2】:

看看您最喜欢的日历程序。查看它可以生成哪些事件重复模式。对它们进行逆向工程。

【讨论】:

  • -1:您的建议是“对模式进行逆向工程”。这个问题是关于如何真正做到这一点。因此,您的答案会直接回到问题。
  • 您能否提供一个提供此类功能的“日历程序”示例?我不能说我见过这里描述的工作实现......
【解决方案3】:

如果您的目的是生成模式的可读描述,例如“11 月的每个星期一”,那么您可能希望从列举可能的描述开始。描述可以分解为频率和范围,例如,

频率:

  • 每天...
  • 每隔/第三/第四天...
  • 工作日/周末 ...
  • 每周一...
  • 周一交替 ...
  • 第一个/第二个/最后一个星期一...
  • ...

界限:

  • ...一月
  • ...3 月 25 日至 10 月 25 日之间
  • ...

每个都不会有那么多,你可以一个一个检查。

【讨论】:

    【解决方案4】:

    我会做什么:

    1. 创建数据样本
    2. 使用聚类算法
    3. 使用算法生成样本
    4. 创建一个适应度函数来衡量它与完整数据集的相关程度。聚类算法将提出 0 或 1 条建议,您可以衡量它与完整集的匹配程度。
    5. 元素化/合并事件与已找到的集合并重新运行此算法。

    您可能希望使用模拟退火或遗传算法。此外,如果您有描述,您可能需要比较描述以生成样本。

    【讨论】:

      【解决方案5】:

      Grammatical Evolution (GE) 适合此类问题,因为您正在寻找符合某种语言的答案。 Grammatical Evolution 还用于程序生成、作曲、设计等。

      我会这样处理任务:

      用语法构造问题空间

      构造一个Context-free Grammar,它可以代表所有需要的重复模式。考虑这样的生产规则:

      datepattern -> datepattern 'and' datepattern
      datepattern -> frequency bounds
      frequency -> 'every' ordinal weekday 'of the month'
      frequency -> 'every' weekday
      ordinal -> ordinal 'and' ordinal
      ordinal -> 'first' | 'second' | 'third'
      bounds -> 'in the year' year
      

      这些规则生成的模式示例如下:'2010 年每月的第二个和第三个星期三以及 2011 年的每个星期二'

      实现这种语法的一种方法是通过类层次结构,稍后您将通过反射对其进行操作,正如我在下面的示例中所做的那样。

      将此语言映射到一组日期

      您应该创建一个函数,该函数从您的语言中获取一个子句并递归地返回它所涵盖的所有日期的集合。这使您可以将您的答案与输入进行比较。

      以语法为导向,寻找可能的解决方案

      您可以使用遗传算法或模拟退火将日期与语法相匹配,使用动态编程试试运气,或者从所有可能的子句的暴力枚举开始。

      如果您使用遗传算法,您的变异概念应该包括根据您的生产规则之一的应用用一个表达式替换另一个表达式。

      查看以下与 GE 相关的网站以获取代码和信息: http://www.bangor.ac.uk/~eep201/jge/
      http://nohejl.name/age/
      http://www.geneticprogramming.us/Home_Page.html

      评估每个解决方案

      适应度函数可以考虑解决方案的文本长度、多次生成的日期数、错过的日期数以及生成的错误日期数。

      示例代码

      应要求,并且因为这是一个非常有趣的挑战,我已经编写了算法的基本实现来帮助您入门。虽然它可以工作,但它还没有完成,但设计确实应该得到更多的思考,一旦你从这个例子中收集了基本的收获,我建议你考虑使用我上面提到的一个库。

        /// <summary>
        ///  This is a very basic example implementation of a grammatical evolution algorithm for formulating a recurrence pattern in a set of dates.
        ///  It needs significant extensions and optimizations to be useful in a production setting.
        /// </summary>
        static class Program
        {
      
          #region "Class hierarchy that codifies the grammar"
      
          class DatePattern
          {
      
            public Frequency frequency;
            public Bounds bounds;
      
            public override string ToString() { return "" + frequency + " " + bounds; }
      
            public IEnumerable<DateTime> Dates()
            {
              return frequency == null ? new DateTime[] { } : frequency.FilterDates(bounds.GetDates());
            }
      
          }
      
          abstract class Bounds
          {
            public abstract IEnumerable<DateTime> GetDates();
          }
      
          class YearBounds : Bounds
          {
      
            /* in the year .. */
            public int year;
      
            public override string ToString() { return "in the year " + year; }
      
            public override IEnumerable<DateTime> GetDates()
            {
              var firstDayOfYear = new DateTime(year, 1, 1);
              return Enumerable.Range(0, new DateTime(year, 12, 31).DayOfYear)
                .Select(dayOfYear => firstDayOfYear.AddDays(dayOfYear));
            }
          }
      
          abstract class Frequency
          {
            public abstract IEnumerable<DateTime> FilterDates(IEnumerable<DateTime> Dates);
          }
      
          class WeeklyFrequency : Frequency
          {
      
            /* every .. */
            public DayOfWeek dayOfWeek;
      
            public override string ToString() { return "every " + dayOfWeek; }
      
            public override IEnumerable<DateTime> FilterDates(IEnumerable<DateTime> Dates)
            {
              return Dates.Where(date => (date.DayOfWeek == dayOfWeek));
            }
      
          }
      
          class MonthlyFrequency : Frequency
          {
      
            /* every .. */
            public Ordinal ordinal;
            public DayOfWeek dayOfWeek;
            /* .. of the month */
      
            public override string ToString() { return "every " + ordinal + " " + dayOfWeek + " of the month"; }
      
            public override IEnumerable<DateTime> FilterDates(IEnumerable<DateTime> Dates)
            {
              return Dates.Where(date => (date.DayOfWeek == dayOfWeek) && (int)ordinal == (date.Day - 1) / 7);
            }
      
          }
      
          enum Ordinal { First, Second, Third, Fourth, Fifth }
      
          #endregion
      
          static Random random = new Random();
          const double MUTATION_RATE = 0.3;
          static Dictionary<Type, Type[]> subtypes = new Dictionary<Type, Type[]>();
      
          static void Main()
          {
      
            // The input signifies the recurrence 'every first thursday of the month in 2010':
            var input = new DateTime[] {new DateTime(2010,12,2), new DateTime(2010,11,4),new DateTime(2010,10,7),new DateTime(2010,9,2),
                          new DateTime(2010,8,5),new DateTime(2010,7,1),new DateTime(2010,6,3),new DateTime(2010,5,6),
                          new DateTime(2010,4,1),new DateTime(2010,3,4),new DateTime(2010,2,4),new DateTime(2010,1,7) };
      
      
            for (int cTests = 0; cTests < 20; cTests++)
            {
              // Initialize with a random population
              int treesize = 0;
              var population = new DatePattern[] { (DatePattern)Generate(typeof(DatePattern), ref treesize), (DatePattern)Generate(typeof(DatePattern), ref treesize), (DatePattern)Generate(typeof(DatePattern), ref treesize) };
              Run(input, new List<DatePattern>(population));
            }
          }
      
          private static void Run(DateTime[] input, List<DatePattern> population)
          {
            var strongest = population[0];
            int strongestFitness = int.MinValue;
            int bestTry = int.MaxValue;
            for (int cGenerations = 0; cGenerations < 300 && strongestFitness < -100; cGenerations++)
            {
              // Select the best individuals to survive:
              var survivers = population
                  .Select(individual => new { Fitness = Fitness(input, individual), individual })
                  .OrderByDescending(pair => pair.Fitness)
                  .Take(5)
                  .Select(pair => pair.individual)
                  .ToArray();
              population.Clear();
      
              // The survivers are the foundation for the next generation:
              foreach (var parent in survivers)
              {
                for (int cChildren = 0; cChildren < 3; cChildren++)
                {
                  int treeSize = 1;
                  DatePattern child = (DatePattern)Mutate(parent, ref treeSize); // NB: procreation may also be done through crossover.
                  population.Add((DatePattern)child);
      
                  var childFitness = Fitness(input, child);
                  if (childFitness > strongestFitness)
                  {
                    bestTry = cGenerations;
                    strongestFitness = childFitness;
                    strongest = child;
                  }
      
                }
              }
            }
            Trace.WriteLine("Found best match with fitness " + Fitness(input, strongest) + " after " + bestTry + " generations: " + strongest);
      
          }
      
          private static object Mutate(object original, ref int treeSize)
          {
            treeSize = 0;
      
      
            object replacement = Construct(original.GetType());
            foreach (var field in original.GetType().GetFields())
            {
              object newFieldValue = field.GetValue(original);
              int subtreeSize;
              if (field.FieldType.IsEnum)
              {
                subtreeSize = 1;
                if (random.NextDouble() <= MUTATION_RATE)
                  newFieldValue = ConstructRandomEnumValue(field.FieldType);
              }
              else if (field.FieldType == typeof(int))
              {
                subtreeSize = 1;
                if (random.NextDouble() <= MUTATION_RATE)
                  newFieldValue = (random.Next(2) == 0
                  ? Math.Min(int.MaxValue - 1, (int)newFieldValue) + 1
                  : Math.Max(int.MinValue + 1, (int)newFieldValue) - 1);
              }
              else
              {
                subtreeSize = 0;
                newFieldValue = Mutate(field.GetValue(original), ref subtreeSize); // mutate pre-maturely to find out subtreeSize
      
                if (random.NextDouble() <= MUTATION_RATE / subtreeSize) // makes high-level nodes mutate less.
                {
                  subtreeSize = 0; // init so we can track the size of the subtree soon to be made.
                  newFieldValue = Generate(field.FieldType, ref subtreeSize);
                }
              }
              field.SetValue(replacement, newFieldValue);
              treeSize += subtreeSize;
            }
            return replacement;
      
          }
      
          private static object ConstructRandomEnumValue(Type type)
          {
            var vals = type.GetEnumValues();
            return vals.GetValue(random.Next(vals.Length));
          }
      
          private static object Construct(Type type)
          {
            return type.GetConstructor(new Type[] { }).Invoke(new object[] { });
          }
      
          private static object Generate(Type type, ref int treesize)
          {
            if (type.IsEnum)
            {
              return ConstructRandomEnumValue(type);
            }
            else if (typeof(int) == type)
            {
              return random.Next(10) + 2005;
            }
            else
            {
              if (type.IsAbstract)
              {
                // pick one of the concrete subtypes:
                var subtypes = GetConcreteSubtypes(type);
                type = subtypes[random.Next(subtypes.Length)];
              }
              object newobj = Construct(type);
      
              foreach (var field in type.GetFields())
              {
                treesize++;
                field.SetValue(newobj, Generate(field.FieldType, ref treesize));
              }
              return newobj;
            }
          }
      
      
          private static int Fitness(DateTime[] input, DatePattern individual)
          {
            var output = individual.Dates().ToArray();
            var avgDateDiff = Math.Abs((output.Average(d => d.Ticks / (24.0 * 60 * 60 * 10000000)) - input.Average(d => d.Ticks / (24.0 * 60 * 60 * 10000000))));
            return
              -individual.ToString().Length // succinct patterns are preferred.
              - input.Except(output).Count() * 300 // Forgetting some of the dates is bad.
              - output.Except(input).Count() * 3000 // Spurious dates cause even more confusion to the user.
            - (int)(avgDateDiff) * 30000; // The difference in average date is the most important guide.
          }
      
          private static Type[] GetConcreteSubtypes(Type supertype)
          {
            if (subtypes.ContainsKey(supertype))
            {
              return subtypes[supertype];
            }
            else
            {
      
              var types = AppDomain.CurrentDomain.GetAssemblies().ToList()
                  .SelectMany(s => s.GetTypes())
                  .Where(p => supertype.IsAssignableFrom(p) && !p.IsAbstract).ToArray();
              subtypes.Add(supertype, types);
              return types;
            }
          }
        }
      

      希望这能让你走上正轨。请务必在某处分享您的实际解决方案;我认为它在很多场景中都会非常有用。

      【讨论】:

      • 这是一个有趣的答案;你能提供任何具体的例子来解决类似的问题吗?
      • @DanP:作曲、设计庇护所、程序生成、股票交易。这种技术被称为语法进化。我还编辑了我的答案以详细说明。
      • @Arjen:对不起...我应该更清楚...使用您描述的技术的一些开源项目示例怎么样?参考实现在这里将非常有用。
      • @DanP 我明白你现在的意思了。我为您链接了两个参考实现和一个包含更多一般信息的站点。
      • @Arjen Kruithof:对于我自己(和其他开发人员)没有太多涉足通用算法等;你愿意提供一个小的概念证明吗?我意识到任何形式的全面实施都将是一项艰巨的任务 - 但是让球在这里滚动怎么样?
      【解决方案6】:

      您可以访问系统日期或系统日期和时间,并根据调用或函数结果返回的日期和星期几在内存中构造粗略的日历点。然后使用相关月份的天数对它们求和,并在输入中添加天变量的天数和/或访问从星期日或星期一开始的相关周的日历点,并计算或递增索引到正确的日。使用固定字符构造文本字符串,并根据需要插入相关变量,例如星期几的全名。可能需要多次遍历才能获得要显示或统计其发生的所有事件。

      【讨论】:

        【解决方案7】:

        首先,找到一个序列,如果存在的话:

        step = {day,month,year}
        period=0
        for d = 1 to dates.count-1
            interval(d,step)=datedifference(s,date(d),date(d+1))
        next
        ' Find frequency with largest interval
        for s = year downto day
            found=true
            for d = 1 to dates.count-2
                if interval(d,s)=interval(d+1,s) then
                    found=false
                    exit for
                end if
            next
            if found then
                period=s
                frequency=interval(1,s)
                exit for
            end if
        next
        
        if period>0
            Select case period
              case day
                if frequency mod 7 = 0 then
                  say "every" dayname(date(1))
                else
                  say "every" frequency "days"
                end if
              case month
                say "every" frequency "months on day" daynumber(date(1))
              case years
                say "every" frequency "years on" daynumber(date(1)) monthname(date(1))
            end select
        end if
        

        最后,处理“in November”、“from 2007 to 2010”等,应该很明显了。

        HTH

        【讨论】:

          【解决方案8】:

          我喜欢@arjen 的回答,但我认为不需要复杂的算法。这太简单了。如果有一个模式,就有一个模式......因此一个简单的算法就可以工作。首先,我们需要考虑我们正在寻找的模式类型:每日、每周、每月和每年。

          如何识别?

          每日:每天都有记录 每周:每周有一个记录 每月:每个月都有一个记录 每年:每年都有记录

          难吗?不。只需计算你有多少重复,然后分类。

          这是我的实现

          RecurrencePatternAnalyser.java

          public class RecurrencePatternAnalyser {
          
              // Local copy of calendars by add() method 
              private ArrayList<Calendar> mCalendars = new ArrayList<Calendar>();
          
              // Used to count the uniqueness of each year/month/day 
              private HashMap<Integer, Integer> year_count = new HashMap<Integer,Integer>();
              private HashMap<Integer, Integer> month_count = new HashMap<Integer,Integer>();
              private HashMap<Integer, Integer> day_count = new HashMap<Integer,Integer>();
              private HashMap<Integer, Integer> busday_count = new HashMap<Integer,Integer>();
          
              // Used for counting payments before due date on weekends
              private int day_goodpayer_ocurrences = 0;
              private int day_goodPayer = 0;
          
              // Add a new calendar to the analysis
              public void add(Calendar date)
              {
                  mCalendars.add(date);
                  addYear( date.get(Calendar.YEAR) );
                  addMonth( date.get(Calendar.MONTH) );
                  addDay( date.get(Calendar.DAY_OF_MONTH) );
                  addWeekendDays( date );
              }
          
              public void printCounts()
              {
                  System.out.println("Year: " +  getYearCount() + 
                          " month: " +  getMonthCount() + " day: " +  getDayCount());
              }
          
              public RecurrencePattern getPattern()
              {
                  int records = mCalendars.size();
                  if (records==1)
                      return null;
          
                  RecurrencePattern rp = null;
          
                  if (getYearCount()==records)
                  {
                      rp = new RecurrencePatternYearly();
                      if (records>=3)
                          rp.setConfidence(1);
                      else if (records==2)
                          rp.setConfidence(0.9f);
                  }
                  else if (getMonthCount()==records)
                  {
                      rp = new RecurrencePatternMonthly();
                      if (records>=12)
                          rp.setConfidence(1);
                      else
                          rp.setConfidence(1-(-0.0168f * records + 0.2f));
                  } 
                  else 
                  {
                      calcDaysRepetitionWithWeekends();
                      if (day_goodpayer_ocurrences==records)
                      {
                          rp = new RecurrencePatternMonthly();
                          rp.setPattern(RecurrencePattern.PatternType.MONTHLY_GOOD_PAYER);
                          if (records>=12)
                              rp.setConfidence(0.95f);
                          else
                              rp.setConfidence(1-(-0.0168f * records + 0.25f));
                      }
                  }
          
                  return rp;
              }
          
              // Increment one more year/month/day on each count variable
              private void addYear(int key_year)  { incrementHash(year_count, key_year); }
              private void addMonth(int key_month)    { incrementHash(month_count, key_month); }
              private void addDay(int key_day)    { incrementHash(day_count, key_day); }
          
              // Retrieve number of unique entries for the records
              private int getYearCount() { return year_count.size(); }
              private int getMonthCount() { return month_count.size(); }
              private int getDayCount() { return day_count.size(); }
          
              // Generic function to increment the hash by 1  
              private void incrementHash(HashMap<Integer, Integer> var, Integer key)
              {
                  Integer oldCount = var.get(key);
                  Integer newCount = 0;
                  if ( oldCount != null ) {
                      newCount = oldCount;
                  }
                  newCount++;
                  var.put(key, newCount);
              }
          
              // As Bank are closed during weekends, some dates might be anticipated
              // to Fridays. These will be false positives for the recurrence pattern.
              // This function adds Saturdays and Sundays to the count when a date is 
              // Friday.
              private void addWeekendDays(Calendar c)
              {
                  int key_day = c.get(Calendar.DAY_OF_MONTH);
                  incrementHash(busday_count, key_day);
                  if (c.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY)
                  {
                      // Adds Saturday
                      c.add(Calendar.DATE, 1);
                      key_day = c.get(Calendar.DAY_OF_MONTH);
                      incrementHash(busday_count, key_day);
                      // Adds Sunday
                      c.add(Calendar.DATE, 1);
                      key_day = c.get(Calendar.DAY_OF_MONTH);
                      incrementHash(busday_count, key_day);
                  }
              }
          
              private void calcDaysRepetitionWithWeekends()
              {               
                  Iterator<Entry<Integer, Integer>> it =
                          busday_count.entrySet().iterator();
                  while (it.hasNext()) {
                      @SuppressWarnings("rawtypes")
                      Map.Entry pair = (Map.Entry)it.next();
                      if ((int)pair.getValue() > day_goodpayer_ocurrences)
                      {
                          day_goodpayer_ocurrences = (int) pair.getValue();
                          day_goodPayer = (int) pair.getKey();
                      }
                      //it.remove(); // avoids a ConcurrentModificationException
                  }
              }
          }
          

          RecurrencePattern.java

          public abstract class RecurrencePattern {
          
              public enum PatternType {
                  YEARLY, MONTHLY, WEEKLY, DAILY, MONTHLY_GOOD_PAYER 
              }   
              public enum OrdinalType {
                  FIRST, SECOND, THIRD, FOURTH, FIFTH 
              }
          
              protected PatternType pattern;
              private float confidence;
              private int frequency;
          
              public PatternType getPattern() {
                  return pattern;
              }
          
              public void setPattern(PatternType pattern) {
                  this.pattern = pattern;
              }
          
              public float getConfidence() {
                  return confidence;
              }
              public void setConfidence(float confidence) {
                  this.confidence = confidence;
              }
              public int getFrequency() {
                  return frequency;
              }
              public void setFrequency(int frequency) {
                  this.frequency = frequency;
              }   
          }
          

          RecurrencePatternMonthly.java

          public class RecurrencePatternMonthly extends RecurrencePattern {
              private boolean isDayFixed;
              private boolean isDayOrdinal;
              private OrdinalType ordinaltype;
          
              public RecurrencePatternMonthly()
              {
                  this.pattern = PatternType.MONTHLY;
              }
          }
          

          RecurrencePatternYearly.java

          public class RecurrencePatternYearly extends RecurrencePattern {
              private boolean isDayFixed;
              private boolean isMonthFixed;
              private boolean isDayOrdinal;
              private OrdinalType ordinaltype;
          
              public RecurrencePatternYearly()
              {
                  this.pattern = PatternType.YEARLY;
              }
          }   
          

          Main.java

          public class Algofin {
          
              static Connection c = null;
          
              public static void main(String[] args) {
                  //openConnection();
                  //readSqlFile();
          
                  RecurrencePatternAnalyser r = new RecurrencePatternAnalyser();
          
                  //System.out.println(new GregorianCalendar(2015,1,30).get(Calendar.MONTH));
                  r.add(new GregorianCalendar(2015,0,1));
                  r.add(new GregorianCalendar(2015,0,30));
                  r.add(new GregorianCalendar(2015,1,27));
                  r.add(new GregorianCalendar(2015,3,1));
                  r.add(new GregorianCalendar(2015,4,1));
          
                  r.printCounts();
          
                  RecurrencePattern rp;
                  rp=r.getPattern();
                  System.out.println("Pattern: " + rp.getPattern() + " confidence: " + rp.getConfidence());
              }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-01-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-01-17
            • 2012-05-19
            • 2020-03-20
            相关资源
            最近更新 更多