使用LINQ在列表上返回基于连续日期的子集列表

7

我有一个场景,其中我有一个包含对象的列表,对象中有一个datetime字段。我正在尝试找出是否有一种方法可以使用LINQ按连续日期对列表进行分组,并返回具有连续日期范围的子列表。

public virtual IList<LineItem> LineItems { get; set; }
...
public class LineItem
{
    public virtual string Status { get; set; }
    public virtual DateTime TransDate { get; set; }
    ...
}

所以,如果我有6个LineItem,所有的Status = P,那么
TransDate = { 8/1/2011 , 8/2/2011 , 8/3/2011 , 8/5/2011 , 8/6/2011 , 8/9/2011 }

分别返回如下列表:
{ (P, 8/1/2011-8/3/2011) , (P,8/5/2011-8/6/2011) , (P,8/9/2011) }

有什么想法吗?我可以通过手动迭代列表并检查TransDate是否连续来完成此操作,但我正在寻找更优雅(最好是使用LINQ)的方法。谢谢!


日期会是不同的吗?另外,它们是否保证按时间顺序排序? - Ani
3个回答

6
我会使用这样的帮助方法:

private static IEnumerable<ICollection<T>> PartitionByPredicate<T>(
    this IEnumerable<T> seq, Func<T, T, bool> split)
{
    var buffer = new List<T>();

    foreach (var x in seq)
    {
        if (buffer.Any() && split(buffer.Last(), x))
        {
            yield return buffer;
            buffer = new List<T>();
        }

        buffer.Add(x);
    }

    if (buffer.Any())
        yield return buffer;
}

然后:

var sorted = LineItems.OrderBy(i => i.TransDate);
var split = sorted.PartitionByPredicate(
    (x, y) => (y.TransDate.Date - x.TransDate.Date).TotalDays > 1)

(edit:稍微修改了一下,我的第一个版本有点傻。)

我明白了,所以你将有序序列输入PartitionByPredicate方法,当谓词为真时(在这种情况下是日期之间存在间隔),该方法开始下一个分区。+1 非常聪明;很好的、通用的 LINQ 解决方案。 - George Duckett
有点偏题,但是我认为我在Linq方面还算不错,我看了你的回答,只理解了大约20%,这表明我还有很多东西要学习。您有什么关于yield和helper predicates等高级内容的书籍或有用文章的指导吗? - Chris
我没有读过任何C#的书,但我可以推荐Eric Lippert和Bart de Smet的博客,它们经常写关于C#语言和LINQ的长篇文章,你可能会觉得有些困难,但它们值得你花时间去学习。我个人发现我的Scheme经验使我很容易掌握函数式C#构造,但这可能是一条非常迂回的通向专业知识的道路。为了练习,我还建议你在空闲时间尝试解决Stack Overflow上的问题 :) - mqp
谢谢 - 这让我朝着正确的方向前进并且非常有效。我也同意Chris的评论。这确实是一次令人印象深刻的经历,让我受益匪浅! - sgeddes

1
我建议您采用迭代器块实现,正如@mquander所建议的那样。
但是,这里有一个有趣的、纯LINQ的解决方案,假设日期是不同的并且按时间顺序排列,它将起作用(尽管效率低下)。
var groups = from item in LineItems
             let startDate = item.TransDate
             group item by LineItems.Select(lineItem => lineItem.TransDate)
                                    .SkipWhile(endDate => endDate < startDate)                                        
                                    .TakeWhile((endDate, index) => 
                                                startDate.AddDays(index) == endDate)
                                    .Last();

//If required:
 var groupsAsLists = groups.Select(g => g.ToList()).ToList();

这个程序是通过选择任何日期序列中的最后一个顺序日期作为该序列的关键来实现的。


嗨Ani - 感谢您的帖子,非常有趣。最终我使用了之前的解决方案(正如您所建议的)。我不得不向匿名函数添加额外的逻辑(不一定是唯一的日期),但仍然非常感谢您的建议。 - sgeddes

0

我不认为这很优雅,但它是LINQ并且它能工作 :)

var list = new[] { 
    new DateTime(2011, 1, 1), 
    new DateTime(2011, 1, 2), 
    new DateTime(2011, 1, 3), 
    new DateTime(2011, 1, 5), 
    new DateTime(2011, 1, 6), 
    new DateTime(2011, 1, 8), 
    new DateTime(2011, 1, 10), 
};
var ordered = list.OrderBy(d => d);
var accum = ordered.Aggregate(new Dictionary<DateTime, List<DateTime>>(), (dic, val) => {
    if (!dic.Any())
    {
        dic.Add(val, new List<DateTime> { val });
    }
    else
    {
        if ((val - dic[dic.Keys.Last()].Last()).Days <= 1)
            dic[dic.Keys.Last()].Add(val);
        else
            dic.Add(val, new List<DateTime> { val });
    }
    return dic;
});

结果中accum将有4个组:1-3,5-6,8和10。


这通常不起作用,因为 dic.Values.Last() 可能并不是您放入字典中的最后一件事情。 - mqp
1
我非常确定 dic.Keys.Last() 不会有任何改善。字典是无序的,无论是按插入顺序还是其他外部明显属性。它们是哈希表,如果要同时跟踪插入顺序,就没有办法不损害其性能或使用大量额外的内存。 - mqp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接