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

Posted

技术标签:

【中文标题】使用 LINQ 计算与上一项的差异【英文标题】:Calculate difference from previous item with LINQ 【发布时间】:2011-04-10 15:10:43 【问题描述】:

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

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

我期望的结果是

ID= 1,日期= 现在,DiffToPrev= 0;

ID= 1,日期= Now+1,DiffToPrev= 3;

ID= 1,日期= Now+2,DiffToPrev= 7;

ID= 1, Date= Now+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();
        
    

【问题讨论】:

【参考方案1】:

一种选择(对于 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 对序列进行分组。

【讨论】:

这似乎是一个正确的答案,但我不知道如何使用它。 我猜这个会比 Branimir 的回答更有效率,对吧? @Martynas:比 Branimir 的回答更笼统,比 Felix 的回答更有效。 这是一个不错的小函数 Jon;甜美而简单。 @NetMage: IEnumerator&lt;T&gt; 确实 实现了IDisposable,并且您应该始终使用它——就像foreach 隐式执行的那样。非通用版本没有。【参考方案2】:

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

   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)
          
   );

【讨论】:

@Martynas:请注意,这不是很通用的用途 - 它只适用于您可以索引到集合的场景。 @JonSkeet OP 有一个列表,并没有要求通用,所以这是一个更好的答案。 @JimBalter:Stack Overflow 的目的不仅仅是解决 OP 的问题。有时严格遵守要求的界限是有意义的(尽管我至少已经格式化了这段代码以避免滚动),但有时我认为提供更普遍有用的方法是有帮助的。 我喜欢它:漂亮而简单,就像 LINQ 应该的那样! @JonSkeet,您的自定义操作符丰富了我的技能,也提供了操作迭代器的好例子。但我和我的团队成员希望代码尽可能简单易读。 @MichaelG:我不会特别期望有显着的性能差异 - 但 SelectWithIndex 要求源可按索引访问,而 SelectWithPrevious 则不需要。【参考方案3】:

在 C#4 中,您可以使用 Zip 方法一次处理两个项目。像这样:

        var list1 = list.Take(list.Count() - 1);
        var list2 = list.Skip(1);
        var diff = list1.Zip(list2, (item1, item2) => ...);

【讨论】:

【参考方案4】:

修改 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;
        
    

几个关键区别...传递第三个布尔参数来指示它是否是可枚举的第一个元素。我还切换了当前/之前参数的顺序。

这是匹配的示例:

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

【讨论】:

【参考方案5】:

除了上面的 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)));

基本上,您正在压缩同一个列表的两个版本,但第一个版本(当前列表)从集合中的第二个元素开始,否则差异总是相同的元素不同,差异为零。

我希望这是有道理的,

戴夫

【讨论】:

【参考方案6】:

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);

OR (如乔恩表演)

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 我的错,你可以换成Tuple。我已经改变了它。谢谢你。【参考方案7】:

这是使用 readonly structValueTuple(也称为 struct)的 C# 7.2 重构代码。

我使用Zip() 创建(CurrentID, PreviousID, CurrDate, PrevDate, DiffToPrev) 5 个成员的元组。使用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

注意:struct(尤其是readonly)的性能要比class好很多。

感谢@FelixUngman 和@DavidHuxtable 提出的Zip() 想法!

【讨论】:

以上是关于使用 LINQ 计算与上一项的差异的主要内容,如果未能解决你的问题,请参考以下文章

计算具有重复项的两个列表的差异

无法使用 group by 和使用 linq 的多个联接访问字段

齿轮公式中inv(a)怎么计算?

Linq 采取不同的方式

使用 .Net/C# 计算集合的频率分布

Windows科学计算器与Python,计算结果的巨大差异?