代码之家  ›  专栏  ›  技术社区  ›  Codesleuth

使用LINQ按日期对序列分组,不留间隔

  •  5
  • Codesleuth  · 技术社区  · 14 年前

    我正在尝试选择一个列表的子组,其中项目有连续的日期,例如

    ID  StaffID  Title              ActivityDate
    --  -------  -----------------  ------------
     1       41  Meeting with John    03/06/2010
     2       41  Meeting with John    08/06/2010
     3       41  Meeting Continues    09/06/2010
     4       41  Meeting Continues    10/06/2010
     5       41  Meeting with Kay     14/06/2010
     6       41  Meeting Continues    15/06/2010
    

    我每次都使用一个透视点,所以以Pivot项示例为3,我希望得到围绕透视点的以下连续事件:

    ID  StaffID  Title              ActivityDate
    --  -------  -----------------  ------------
     2       41  Meeting with John    08/06/2010
     3       41  Meeting Continues    09/06/2010
     4       41  Meeting Continues    10/06/2010
    

    我目前的实施是一个艰难的“走”到过去,然后到未来,建立清单:

    var activity = // item number 3: Meeting Continues (09/06/2010)
    
    var orderedEvents = activities.OrderBy(a => a.ActivityDate).ToArray();
    
    // Walk into the past until a gap is found
    var preceedingEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID);
    DateTime dayBefore;
    var previousEvent = activity;
    while (previousEvent != null)
    {
        dayBefore = previousEvent.ActivityDate.AddDays(-1).Date;
        previousEvent = preceedingEvents.TakeWhile(a => a.ID != previousEvent.ID).LastOrDefault();
        if (previousEvent != null)
        {
            if (previousEvent.ActivityDate.Date == dayBefore)
                relatedActivities.Insert(0, previousEvent);
            else
                previousEvent = null;
        }
    }
    
    
    // Walk into the future until a gap is found
    var followingEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
    DateTime dayAfter;
    var nextEvent = activity;
    while (nextEvent != null)
    {
        dayAfter = nextEvent.ActivityDate.AddDays(1).Date;
        nextEvent = followingEvents.SkipWhile(a => a.ID != nextEvent.ID).Skip(1).FirstOrDefault();
        if (nextEvent != null)
        {
            if (nextEvent.ActivityDate.Date == dayAfter)
                relatedActivities.Add(nextEvent);
            else
                nextEvent = null;
        }
    }
    

    名单 relatedActivities 然后应按顺序包含连续事件。

    有没有更好的方法(可能使用LINQ)来解决这个问题?

    我想用 .Aggregate() 但是当它在序列中发现一个缺口时,却无法思考如何让聚合体破裂。

    3 回复  |  直到 14 年前
        1
  •  2
  •   LukeH    14 年前

    在这种情况下,我认为 foreach 循环可能比LINQ查询更可读:

    var relatedActivities = new List<TActivity>();
    bool found = false;
    
    foreach (var item in activities.OrderBy(a => a.ActivityDate))
    {
        int count = relatedActivities.Count;
        if ((count > 0) && (relatedActivities[count - 1].ActivityDate.Date.AddDays(1) != item.ActivityDate.Date))
        {
            if (found)
                break;
    
            relatedActivities.Clear();
        }
    
        relatedActivities.Add(item);
        if (item.ID == activity.ID)
            found = true;
    }
    
    if (!found)
        relatedActivities.Clear();
    

    对于它的价值,这里有一个大致相同的——而且可读性差得多的——LINQ查询:

    var relatedActivities = activities
        .OrderBy(x => x.ActivityDate)
        .Aggregate
        (
            new { List = new List<TActivity>(), Found = false, ShortCircuit = false },
            (a, x) =>
            {
                if (a.ShortCircuit)
                    return a;
    
                int count = a.List.Count;
                if ((count > 0) && (a.List[count - 1].ActivityDate.Date.AddDays(1) != x.ActivityDate.Date))
                {
                    if (a.Found)
                        return new { a.List, a.Found, ShortCircuit = true };
    
                    a.List.Clear();
                }
    
                a.List.Add(x);
                return new { a.List, Found = a.Found || (x.ID == activity.ID), a.ShortCircuit };
            },
            a => a.Found ? a.List : new List<TActivity>()
        );
    
        2
  •  5
  •   Amy B    14 年前

    下面是一个实现:

    public static IEnumerable<IGrouping<int, T>> GroupByContiguous(
      this IEnumerable<T> source,
      Func<T, int> keySelector
    )
    {
       int keyGroup = Int32.MinValue;
       int currentGroupValue = Int32.MinValue;
       return source
         .Select(t => new {obj = t, key = keySelector(t))
         .OrderBy(x => x.key)
         .GroupBy(x => {
           if (currentGroupValue + 1 < x.key)
           {
             keyGroup = x.key;
           }
           currentGroupValue = x.key;
           return keyGroup;
         }, x => x.obj);
    }
    

    您可以通过减法将日期转换为整数,或者设想一个日期时间版本(很容易)。

        3
  •  2
  •   Grace Note    14 年前

    不知怎么的,我不认为LINQ真的是用于双向一维深度优先搜索,但我使用聚合构造了一个有效的LINQ。对于这个例子,我将使用列表而不是数组。另外,我要用 Activity 指代存储数据的任何类。用适合您的代码的内容替换它。

    在我们开始之前,我们需要一个小函数来处理一些事情。 List.Add(T) 返回空值,但我们希望能够在列表中累积并返回此聚合函数的新列表。所以你所需要的只是一个简单的函数,如下所示。

    private List<T> ListWithAdd<T>(List<T> src, T obj)
    {
        src.Add(obj);
        return src;
    }
    

    首先,我们得到所有活动的排序列表,然后初始化相关活动的列表。此初始列表将仅包含要开始的目标活动。

    List<Activity> orderedEvents = activities.OrderBy(a => a.ActivityDate).ToList();
    List<Activity> relatedActivities = new List<Activity>();
    relatedActivities.Add(activity);
    

    我们必须把它分成两个列表,过去和未来,就像你现在做的那样。

    我们从过去开始,建筑应该看起来很熟悉。然后,我们将把所有这些集合到相关的活动中。这使用了 ListWithAdd 函数。您可以将其压缩为一行,并跳过将previousevents声明为其自己的变量,但在本例中,我将其单独保存。

    var previousEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID).Reverse();
    relatedActivities = previousEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, prevItem) => items.OrderBy(a => a.ActivityDate).First().ActivityDate.Subtract(prevItem.ActivityDate).Days.Equals(1) ? ListWithAdd(items, prevItem) : items).ToList();
    

    接下来,我们将以类似的方式构建以下事件,并对其进行聚合。

    var nextEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
    relatedActivities = nextEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, nextItem) => nextItem.ActivityDate.Subtract(items.OrderBy(a => a.ActivityDate).Last().ActivityDate).Days.Equals(1) ? ListWithAdd(items, nextItem) : items).ToList();
    

    之后您可以对结果进行适当的排序,因为现在相关的活动应该包含所有没有间隙的活动。当它到达第一个间隙时,它不会立即破裂,不,但我认为你不能真正地从一个直线加速器中脱离出来。所以它只是忽略了任何它发现的,超过了一个缺口的东西。

    请注意,此示例代码只对实际时间差进行操作。您的示例输出似乎暗示您需要一些其他比较因素,但这应该足以让您开始。只需在两个条目的日期相减比较中添加必要的逻辑即可。