Подтвердить что ты не робот

Вычислить отличие от предыдущего элемента с 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();
        }
    }
}
4b9b3361

Ответ 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