2010-09-10 16 views
13

Ich versuche, Daten für ein Diagramm mit LINQ vorzubereiten.Berechnen Sie den Unterschied zum vorherigen Element mit LINQ

Das Problem, das ich lösen kann nicht ist, wie die „Differenz zum vorherigen

das Ergebnis I ist

ID erwarten berechnen = 1, Date = Nun DiffToPrev = 0;.

ID = 1, Date = Now + 1, DiffToPrev = 3;

ID = 1, Date = Now + 2, DiffToPrev = 7;

ID = 1, Date = Now + 3, DiffToPrev = -6;

etc ...

Können Sie mir helfen, eine solche Abfrage zu erstellen?

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

Antwort

49

Eine Option (für LINQ to Objects) wäre eine eigene LINQ Operator zu erstellen:

// 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; 
     } 
    } 
} 

Dies Sie Ihre Projektion ausführen können nur einen einzigen Durchlauf der Quellensequenz mit , das ist immer ein Bonus (stellen Sie sich vor, es über eine große Protokolldatei laufen zu lassen).

Beachten Sie, dass es eine Sequenz der Länge n in eine Sequenz der Länge n-1 projiziert - Sie können z. B. ein "dummy" erstes Element voranstellen. (Oder die Methode ändert ein. Miteinschließt)

Hier ist ein Beispiel dafür, wie Sie es verwenden würden:

var query = list.SelectWithPrevious((prev, cur) => 
    new { ID = cur.ID, Date = cur.Date, DateDiff = (cur.Date - prev.Date).Days) }); 

Beachten Sie, dass dies das Endergebnis einer ID mit dem ersten Ergebnis der nächsten schließen ID ... möchten Sie vielleicht zuerst Ihre Sequenz nach ID gruppieren.

+0

Dies scheint eine richtige Antwort, aber ich kann Figur, wie man benutze es. – Marty

+0

Ich denke, dieser wäre effizienter als Branimirs Antwort, oder? – Marty

+0

@Martynas: Es ist allgemeiner als Branimirs Antwort und effizienter als Felix. –

5

In C# 4 können Sie die Zip-Methode verwenden, um zwei Elemente gleichzeitig zu verarbeiten. Wie folgt aus:

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

Verwenden Index vorherigen Objekt zu erhalten:

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

Danke :) Das einfach :) cool :) – Marty

+0

@Martynas: Beachten Sie, dass dies nicht sehr allgemein ist - es funktioniert nur in Szenarien, in denen Sie in die Sammlung indizieren können. –

+0

@Martynas Dank @Jon Skeet Sie haben Recht, aber es ist einfach – Branimir

0

Weitere Felix Ungman der Beitrag oben, unten ist ein Beispiel dafür, wie Sie die Daten erreichen können Sie die Verwendung von Zip machen müssen():

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

Grundsätzlich zippen Sie zwei Versionen der gleichen Liste, aber die erste Version (die aktuelle Liste) beginnt mit dem zweiten Element in der Sammlung, ansonsten würde ein Unterschied immer das gleiche Element unterscheiden, was eine Differenz von Null ergibt.

Ich hoffe, das macht Sinn,

Dave

3

Modifikation von Jon Skeet Antwort auf nicht das erste Element überspringen:

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

Einige wichtige Unterschiede ...Übergibt einen dritten Bool-Parameter, um anzugeben, ob es das erste Element des Aufzählungselements ist. Ich habe auch die Reihenfolge der aktuellen/vorherigen Parameter geändert.

Hier ist das passende Beispiel:

var query = list.SelectWithPrevious((cur, prev, isfirst) => 
    new { 
     ID = cur.ID, 
     Date = cur.Date, 
     DateDiff = (isfirst ? cur.Date : cur.Date - prev.Date).Days); 
    }); 
2

Noch eine andere Mod auf Jon Skeet Version (Dank für Ihre Lösung 1). Außer dies gibt eine Aufzählung von Paaren zurück.

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

Dies ist NICHT die erste Rückkehr, weil es über die Zwischen zwischen den einzelnen Posten zurück.

Gebrauch mag es:

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; 
    // .. 
} 

ODER

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

OR (wie jon zeigt)

var newList = myObjectList.Intermediate().Select(item => new 
{ 
    ID = item.Second.ID, 
    Date = item.Second.Date, 
    DateDiff = (item.Second.Date - item.First.Date).Days 
}); 
Verwandte Themen