使用LINQ计算与前一项的差异

26

我正在尝试使用LINQ准备图形数据。

我无法解决的问题是如何计算“与前一项的差异”。

我期望的结果是

ID= 1, Date= 现在, DiffToPrev= 0;

ID= 1, Date= 现在+1, DiffToPrev= 3;

ID= 1, Date= 现在+2, DiffToPrev= 7;

ID= 1, Date= 现在+3, DiffToPrev= -6;

等等...

您能帮我创建这样的查询吗?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    public class MyObject
    {
        public int ID { get; set; }
        public DateTime Date { get; set; }
        public int Value { get; set; }
    }

    class Program
    {
        static void Main()
        {
               var list = new List<MyObject>
          {
            new MyObject {ID= 1,Date = DateTime.Now,Value = 5},
            new MyObject {ID= 1,Date = DateTime.Now.AddDays(1),Value = 8},
            new MyObject {ID= 1,Date = DateTime.Now.AddDays(2),Value = 15},
            new MyObject {ID= 1,Date = DateTime.Now.AddDays(3),Value = 9},
            new MyObject {ID= 1,Date = DateTime.Now.AddDays(4),Value = 12},
            new MyObject {ID= 1,Date = DateTime.Now.AddDays(5),Value = 25},
            new MyObject {ID= 2,Date = DateTime.Now,Value = 10},
            new MyObject {ID= 2,Date = DateTime.Now.AddDays(1),Value = 7},
            new MyObject {ID= 2,Date = DateTime.Now.AddDays(2),Value = 19},
            new MyObject {ID= 2,Date = DateTime.Now.AddDays(3),Value = 12},
            new MyObject {ID= 2,Date = DateTime.Now.AddDays(4),Value = 15},
            new MyObject {ID= 2,Date = DateTime.Now.AddDays(5),Value = 18}

        };

            Console.WriteLine(list);   

            Console.ReadLine();
        }
    }
}
7个回答

73

对于LINQ to Objects,其中一个选项是创建自己的LINQ操作符:

// I don't like this name :(
public static IEnumerable<TResult> SelectWithPrevious<TSource, TResult>
    (this IEnumerable<TSource> source,
     Func<TSource, TSource, TResult> projection)
{
    using (var iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
             yield break;
        }
        TSource previous = iterator.Current;
        while (iterator.MoveNext())
        {
            yield return projection(previous, iterator.Current);
            previous = iterator.Current;
        }
    }
}

这使您能够仅使用源序列的一次传递执行投影,这总是一个优点(想象在大型日志文件上运行它)。

请注意,它将长度为n的序列投影到长度为n-1的序列中 - 您可能希望添加一个“虚拟”第一个元素。例如。(或更改方法以包括一个。)

以下是如何使用它的示例:

var query = list.SelectWithPrevious((prev, cur) =>
     new { ID = cur.ID, Date = cur.Date, DateDiff = (cur.Date - prev.Date).Days) });

请注意,这将包括一个ID的最终结果和下一个ID的第一个结果...您可能希望首先按ID分组您的序列。

嗨。我实际上有像“ID”,“颜色”,“日期”,“值”这样的数据,其中(id,color和date)是关键,根据这些关键字应该对数据进行分组。但我无法弄清楚如何分组数据并使用您的函数。您能帮忙在此查询中添加一个group by子句吗? - Marty
2
那真是个不错的小函数,Jon。简洁而甜美。 - Doctor Jones
已添加修改版本以不跳过第一个项目...请查看我的答案获取代码 - stackoverflow.com/q/3683105/26307521#26307521 - Edyn
ByFormerByAdjacent :) - M.kazem Akhgary
1
@NetMage: IEnumerator<T> 确实 实现了 IDisposable,你应该总是使用它 - 就像 foreach 隐式地使用它一样。非泛型版本则不会。 - Jon Skeet
显示剩余14条评论

21

使用索引获取前一个对象:

   var LinqList = list.Select( 
       (myObject, index) => 
          new { 
            ID = myObject.ID, 
            Date = myObject.Date, 
            Value = myObject.Value, 
            DiffToPrev = (index > 0 ? myObject.Value - list[index - 1].Value : 0)
          }
   );

3
楼上的JonSkeet先生,楼主有一个列表,并且并没有要求通用性,因此这个回答更具优越性。 - Jim Balter
2
@JimBalter:Stack Overflow 的目的不仅仅是为了回答提问者的问题。有时候,严格遵循所需范围是有意义的(尽管我至少会格式化此代码以避免滚动),但其他时候,我认为提供更普遍有用的方法是有帮助的。 - Jon Skeet
@MichaelG:请注意,这仅适用于您可以随机访问列表的情况。它无法与任意的 IEnumerable<T> 一起使用。(我个人认为,理解这是在尝试处理相邻元素而不是一个名为 SelectWithPrevious 的方法需要更长时间。但可读性至少在某种程度上是主观的。) - Jon Skeet
嗨@JonSkeet, 我使用了_BenchmarkDotNet_和_.NET 4.8_来比较两种算法:_SelectWithPrevious_和_SelectWithIndex_。有趣的是,当列表中只有5个元素时,_SelectWithPrevious_确实有一个有利的性能比率为1 vs 1.43。 然而,从10个项目的列表开始,一直到高达1000000个项目的列表,比率开始变得相同,甚至对于_SelectWithIndex_解决方案来说更好:1 vs 0.9。 我对此非常好奇! - Michael G
1
@MichaelG:我不会特别期望有显著的性能差异 - 但是SelectWithIndex需要通过索引访问源,而SelectWithPrevious则不需要。 - Jon Skeet
显示剩余2条评论

8
在C#4中,您可以使用Zip方法来一次处理两个项目。像这样:
        var list1 = list.Take(list.Count() - 1);
        var list2 = list.Skip(1);
        var diff = list1.Zip(list2, (item1, item2) => ...);

7
修改Jon Skeet的答案,以不跳过第一个项目:
public static IEnumerable<TResult> SelectWithPrev<TSource, TResult>
    (this IEnumerable<TSource> source, 
    Func<TSource, TSource, bool, TResult> projection)
{
    using (var iterator = source.GetEnumerator())
    {
        var isfirst = true;
        var previous = default(TSource);
        while (iterator.MoveNext())
        {
            yield return projection(iterator.Current, previous, isfirst);
            isfirst = false;
            previous = iterator.Current;
        }
    }
}

这里有一些关键的不同之处……传递了一个额外的 bool 参数来表示是否为可枚举对象的第一个元素。我还调整了当前/前一个参数的顺序。

以下是对应的示例:

var query = list.SelectWithPrevious((cur, prev, isfirst) =>
    new { 
        ID = cur.ID, 
        Date = cur.Date, 
        DateDiff = (isfirst ? cur.Date : cur.Date - prev.Date).Days);
    });

3

继Felix Ungman上面的帖子之后,以下是一个示例,说明如何利用Zip()实现您需要的数据:

        var diffs = list.Skip(1).Zip(list,
            (curr, prev) => new { CurrentID = curr.ID, PreviousID = prev.ID, CurrDate = curr.Date, PrevDate = prev.Date, DiffToPrev = curr.Date.Day - prev.Date.Day })
            .ToList();

        diffs.ForEach(fe => Console.WriteLine(string.Format("Current ID: {0}, Previous ID: {1} Current Date: {2}, Previous Date: {3} Diff: {4}",
            fe.CurrentID, fe.PreviousID, fe.CurrDate, fe.PrevDate, fe.DiffToPrev)));

基本上,您正在压缩同一列表的两个版本,但第一个版本(当前列表)始于集合中的第二个元素,否则差异将始终不同,从而给出零差异。

我希望这有意义,

Dave


2

这是对 Jon Skeet 版本的另一种修改(感谢您的解决方案 +1)。不同之处在于它返回一个元组可枚举对象。

public static IEnumerable<Tuple<T, T>> Intermediate<T>(this IEnumerable<T> source)
{
    using (var iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
            yield break;
        }
        T previous = iterator.Current;
        while (iterator.MoveNext())
        {
            yield return new Tuple<T, T>(previous, iterator.Current);
            previous = iterator.Current;
        }
    }
}

这不是返回第一个元素,而是返回两个元素之间的中间元素。

使用方法如下:

public class MyObject
{
    public int ID { get; set; }
    public DateTime Date { get; set; }
    public int Value { get; set; }
}

var myObjectList = new List<MyObject>();

// don't forget to order on `Date`

foreach(var deltaItem in myObjectList.Intermediate())
{
    var delta = deltaItem.Second.Offset - deltaItem.First.Offset;
    // ..
}

或者

var newList = myObjectList.Intermediate().Select(item => item.Second.Date - item.First.Date);

或者(像Jon所展示的那样)

var newList = myObjectList.Intermediate().Select(item => new 
{ 
    ID = item.Second.ID, 
    Date = item.Second.Date, 
    DateDiff = (item.Second.Date - item.First.Date).Days
});

你在使用哪个 Pair 类?我在 .Net 中没有看到公共的类? - NetMage
@NetMage 不好意思,你可以用 Tuple 替换它。我已经修改了。谢谢。 - Jeroen van Langen

2
这是使用C# 7.2重构的代码,使用了readonly structValueTuple(也是struct)。
我使用Zip()创建了一个由5个元素组成的元组(CurrentID, PreviousID, CurrDate, PrevDate, DiffToPrev)。可以很容易地用foreach迭代它:
foreach(var (CurrentID, PreviousID, CurrDate, PrevDate, DiffToPrev) in diffs)

完整的代码:
public readonly struct S
{
    public int ID { get; }
    public DateTime Date { get; }
    public int Value { get; }

    public S(S other) => this = other;

    public S(int id, DateTime date, int value)
    {
        ID = id;
        Date = date;
        Value = value;
    }

    public static void DumpDiffs(IEnumerable<S> list)
    {
        // Zip (or compare) list with offset 1 - Skip(1) - vs the original list
        // this way the items compared are i[j+1] vs i[j]
        // Note: the resulting enumeration will include list.Count-1 items
        var diffs = list.Skip(1)
                        .Zip(list, (curr, prev) => 
                                    (CurrentID: curr.ID, PreviousID: prev.ID, 
                                    CurrDate: curr.Date, PrevDate: prev.Date, 
                                    DiffToPrev: curr.Date.Day - prev.Date.Day));

        foreach(var (CurrentID, PreviousID, CurrDate, PrevDate, DiffToPrev) in diffs)
            Console.WriteLine($"Current ID: {CurrentID}, Previous ID: {PreviousID} " +
                              $"Current Date: {CurrDate}, Previous Date: {PrevDate} " +
                              $"Diff: {DiffToPrev}");
    }
}

单元测试输出:

// the list:

// ID   Date
// ---------------
// 233  17-Feb-19
// 122  31-Mar-19
// 412  03-Mar-19
// 340  05-May-19
// 920  15-May-19

// CurrentID PreviousID CurrentDate PreviousDate Diff (days)
// ---------------------------------------------------------
//    122       233     31-Mar-19   17-Feb-19      14
//    412       122     03-Mar-19   31-Mar-19      -28
//    340       412     05-May-19   03-Mar-19      2
//    920       340     15-May-19   05-May-19      10

注意:结构体(尤其是只读结构体)的性能比类好得多。
感谢 @FelixUngman 和 @DavidHuxtable 提供的 Zip() 想法!

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