使用 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<T>
确实 实现了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 struct
和 ValueTuple
(也称为 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 计算与上一项的差异的主要内容,如果未能解决你的问题,请参考以下文章