2010-09-10 7 views
13

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

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

В результате я ожидаю

ID = 1, Date = Теперь 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(); 
     } 
    } 
} 

ответ

49

Одним из вариантов (для LINQ к объектам) было бы создать свой собственный оператор 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 ... вы можете сначала сгруппировать свою последовательность по идентификатору.

+0

Это похоже на правильный ответ, но я не могу понять, как используй это. – Marty

+0

Я предполагаю, что этот был бы более эффективен, чем ответ Бранимира, не так ли? – Marty

+0

@ Мартинас: Это более общий, чем ответ Бранимира, и более эффективный, чем Феликс. –

5

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

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

Используйте индекс, чтобы получить предыдущий объект:

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) 
      } 
    ); 
+1

Thanx :) Это просто :) cool :) – Marty

+0

@Martynas: Обратите внимание, что это не очень общая цель, но это работает только в сценариях, где вы можете индексировать коллекцию. –

+0

@Martynas Спасибо @Jon Skeet Вы правы, но это просто – Branimir

0

В дополнение к посту Феликса 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

3

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

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

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

Это НЕ возвращая первую, потому что речь идет о возвращении промежуточного между пунктами.

использовать его как:

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

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

var newList = myObjectList.Intermediate().Select(item => new 
{ 
    ID = item.Second.ID, 
    Date = item.Second.Date, 
    DateDiff = (item.Second.Date - item.First.Date).Days 
}); 
Смежные вопросы