Рассчитать разницу от предыдущего элемента с помощью LINQ - PullRequest
18 голосов
/ 10 сентября 2010

Я пытаюсь подготовить данные для графика с помощью LINQ.

Проблема, которую я не могу решить, состоит в том, как рассчитать разницу с предыдущим.

ожидаемый результат

ID = 1, дата = сейчас, DiffToPrev = 0;

ID = 1, дата = сейчас + 1, DiffToPrev = 3;

ID = 1, дата = сейчас + 2, DiffToPrev = 7;

ID = 1, дата = сейчас + 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();
        }
    }
}

Ответы [ 6 ]

57 голосов
/ 10 сентября 2010

Одним из вариантов (для 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) });

Обратите внимание, что это будет включать в себя конечный результат одного идентификатора с первымрезультат следующего идентификатора ... вы можете сначала сгруппировать последовательность по идентификатору.

15 голосов
/ 10 сентября 2010

Использовать индекс для получения предыдущего объекта:

   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)
          }
   );
5 голосов
/ 11 октября 2014

Модификация ответа Джона Скита, чтобы не пропустить первый пункт:

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 голосов
/ 10 сентября 2010

В C # 4 вы можете использовать метод Zip для обработки двух элементов одновременно.Как это:

        var list1 = list.Take(list.Count() - 1);
        var list2 = list.Skip(1);
        var diff = list1.Zip(list2, (item1, item2) => ...);
2 голосов
/ 14 сентября 2015

Еще один мод на версии Джона Скита (спасибо за ваше решение +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;
    // ..
}

OR

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

ИЛИ (как показывает Джон)

var newList = myObjectList.Intermediate().Select(item => new 
{ 
    ID = item.Second.ID, 
    Date = item.Second.Date, 
    DateDiff = (item.Second.Date - item.First.Date).Days
});
1 голос
/ 09 июля 2013

В дополнение к посту Феликса Унгмана, приведенному выше, ниже приведен пример того, как вы можете получить данные, необходимые для использования 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)));

По сути, вы архивируете две версии одного и того же спискано первая версия (текущий список) начинается со 2-го элемента в коллекции, иначе разница всегда будет отличаться от одного и того же элемента, что дает разницу в ноль.

Надеюсь, это имеет смысл,

Dave

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...