Вычислить отличие от предыдущего элемента с LINQ
Я пытаюсь подготовить данные для графика, используя LINQ.
Проблема, которую я не могу решить, - это вычислить "разницу с предыдущей".
ожидаемый результат
ID = 1, Date = Now, DiffToPrev = 0;
ID = 1, Date = Now + 1, DiffToPrev = 3;
ID = 1, Date = 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
- вы можете, например, добавить первый элемент "dummy". (Или изменить метод, чтобы включить его.)
Вот пример того, как вы его используете:
var query = list.SelectWithPrevious((prev, cur) =>
new { ID = cur.ID, Date = cur.Date, DateDiff = (cur.Date - prev.Date).Days) });
Обратите внимание, что это будет включать окончательный результат одного ID с первым результатом следующего идентификатора... вы можете сначала сгруппировать свою последовательность по идентификатору.
Ответ 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)
}
);
Ответ 3
В С# 4 вы можете использовать метод Zip для обработки двух элементов одновременно. Вот так:
var list1 = list.Take(list.Count() - 1);
var list2 = list.Skip(1);
var diff = list1.Zip(list2, (item1, item2) => ...);
Ответ 4
Модификация ответа Джона Скита, чтобы не пропустить первый элемент:
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);
});
Ответ 5
Еще один мод в версии Jon Skeet (спасибо за ваше решение +1). Кроме того, это возвращает перечислимые пары.
public static IEnumerable<Pair<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 Pair<T, T>(previous, iterator.Current);
previous = iterator.Current;
}
}
}
Это NOT, возвращая первое, потому что оно возвращает возвращаемое промежуточное звено между элементами.
используйте его как:
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
});
Ответ 6
В дополнение к сообщению 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