C# 使用LINQ按日期对序列进行分组,无间隔

C# 使用LINQ按日期对序列进行分组,无间隔,c#,linq,sequence,C#,Linq,Sequence,我正在尝试选择列表中项目具有连续日期的子组,例如 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

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

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 然后,列表
relatedActivities
应按顺序包含连续事件

是否有更好的方法(可能使用LINQ)实现此目的?


我有一个使用的想法,但不知道如何让聚合在序列中发现一个间隙时爆发。

不知何故,我不认为LINQ真正用于双向一维深度优先搜索,但我使用聚合构建了一个工作LINQ。对于这个例子,我将使用列表而不是数组。另外,我将使用
Activity
来引用存储数据的任何类。将其替换为适合您的代码的任何内容

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

private List<T> ListWithAdd<T>(List<T> src, T obj)
{
    src.Add(obj);
    return src;
}
接下来,我们将以类似的方式构建以下事件,并对其进行聚合

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();
var nextEvents=orderedEvents.SkipWhile(a=>a.ID!=activity.ID);
relatedActivities=nextEvents.Aggregate(relatedActivities,(items,nextItem)=>nextItem.ActivityDate.Subtract(items.OrderBy(a=>a.ActivityDate).Last().ActivityDate.Days.Equals(1)?ListWithAdd(items,nextItem):items.ToList();
您可以在之后对结果进行适当排序,因为现在relatedActivities应该包含所有没有间隙的活动。当它到达第一个间隙时不会立即断裂,不,但我不认为你真的可以突破LINQ。因此,它只是忽略了它发现的任何超过间隙的东西


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

在这种情况下,我认为标准的
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();
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>()
    );
var relatedActivities=new List();
bool-found=false;
foreach(activities.OrderBy中的var项(a=>a.ActivityDate))
{
int count=relatedActivities.count;
如果((计数>0)和(&(relatedActivities[count-1].ActivityDate.Date.AddDays(1)!=item.ActivityDate.Date))
{
如果(找到)
打破
相关活动。清除();
}
相关活动。添加(项目);
if(item.ID==activity.ID)
发现=真;
}
如果(!找到)
相关活动。清除();
值得一提的是,这里有一个大致等效的、可读性差得多的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();
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>()
    );
var relatedActivities=活动
.OrderBy(x=>x.ActivityDate)
总数的
(
新建{List=new List(),Found=false,ShortCircuit=false},
(a,x)=>
{
如果(a.短路)
返回a;
int count=a.List.count;
如果((计数>0)&(a.List[count-1].ActivityDate.Date.AddDays(1)!=x.ActivityDate.Date))
{
如果(a.Found)
返回新的{a.List,a.Found,ShortCircuit=true};
a、 List.Clear();
}
a、 增加(x);
返回新的{a.List,Found=a.Found | |(x.ID==activity.ID),a.ShortCircuit};
},
a=>a.Found?a.List:newlist()
);

以下是一个实现:

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);
}
公共静态IEnumerable GroupByContinental(
这是一个数不清的来源,
Func键选择器
)
{
int keyGroup=Int32.MinValue;
int currentGroupValue=Int32.MinValue;
返回源
.Select(t=>new{obj=t,key=keySelector(t))
.OrderBy(x=>x.key)
.GroupBy(x=>{
如果(currentGroupValue+1x.obj);
}

您可以通过减法将日期转换为整数,也可以(轻松地)想象一个日期时间版本。

precedIngeEvents.TakeWhile(a=>a.ID!=previousEvent.ID)的用途是什么?在您的示例中,ID似乎总是唯一的。该行仅从
先前事件
项之前的可枚举项中获取项。将其视为
.Previous()
method。对示例进行了一些清理,使其仅包括会议。经过再三考虑,显示“年假”可能更好,但您得到了我希望的要点:sAs ccomet还提到,您问题中显示的示例结果与您的规范或代码不匹配。我的代码应生成与您相同的结果(即,它们应与您的规范匹配)。如果希望结果与示例结果相匹配,您可能需要一些附加逻辑。这比我的要干净得多。可能要快得多,而且看起来肯定更花哨。“您问题中显示的示例结果与您的规范或代码不匹配”嗯,我一直在使用它,它工作得很好。我不确定你的意思。@codesluth:你的示例源数据显示了从2010年6月7日到2010年6月11日的连续运行日期。如果你只在
ActivityDate
上分组,那么你的结果将包含记录1到5(如果透视ID为3)。您的示例结果仅包含记录2、3和4-要实现这一点,您还需要对其他内容进行分组。(
键入
可能?)啊哈,我现在明白了。对不起,我不应该把
类型
留在问题中-到目前为止,我已经把它们过滤掉了。我将重新构造这个问题,使其更有意义。“您的示例输出似乎暗示您需要一些其他比较因素”正如我在问题中指出的,有一个起点:ID 3。结果应该只有与第3项相邻的事件。LukeH刚刚为我澄清了这一点,对不起。我已经更改了q
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>()
    );
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);
}