2010-09-10 12 views
13

Estoy tratando de preparar los datos para un gráfico con LINQ.Calcular la diferencia con el artículo anterior con LINQ

El problema que no puedo resolver es cómo calcular la "diferencia de anteriores

el resultado que espero es

ID = 1, Fecha = Ahora, DiffToPrev = 0;.

ID = 1, Fecha = ahora + 1, DiffToPrev = 3;

ID = 1, Fecha = ahora + 2, DiffToPrev = 7;

ID = 1, Fecha = ahora + 3, DiffToPrev = -6;

etc ...

¿Me puede ayudar a crear una consulta de este tipo?

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

Respuesta

49

Una de las opciones (para LINQ a Objetos) sería la creación de su propio operador de 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; 
     } 
    } 
} 

Esto le permite realizar su proyección usando solamente una sola pasada de la secuencia de origen , que siempre es una ventaja (imagínese ejecutarlo sobre un archivo de registro grande).

Tenga en cuenta que proyectará una secuencia de longitud n en una secuencia de longitud n-1 - es posible que desee anteponer un primer elemento "ficticio", por ejemplo. (O cambiar el método para incluir una.)

Aquí hay un ejemplo de cómo debe usarlo:

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

Tenga en cuenta que esto incluirá el resultado final de una identificación con el primer resultado de la siguiente ID ... es posible que desee agrupar su secuencia por ID primero.

+0

Esto parece una respuesta correcta, pero no puedo averiguar cómo usarlo – Marty

+0

Supongo que este sería más eficiente que la respuesta de Branimir, ¿verdad? – Marty

+0

@Martynas: Es más general que la respuesta de Branimir, y más eficiente que la de Félix. –

5

En C# 4 puede utilizar el método Zip para procesar dos elementos a la vez. De esta manera:

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

Uso índice para obtener el objeto anterior:

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

Gracias :) Así de simple :) genial :) – Marty

+0

@Martynas: Sin embargo, tenga en cuenta que esto no es muy general: solo funciona en escenarios donde puede indexar en la colección. –

+0

@Martynas Gracias @Jon Skeet Tienes razón, pero es simple – Branimir

0

En relación con el puesto de Félix Ungman arriba, abajo es un ejemplo de cómo se puede lograr los datos que necesita hacer uso de 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))); 

Básicamente, está comprimiendo dos versiones de la misma lista pero la primera versión (la lista actual) comienza en el segundo elemento de la colección, de lo contrario la diferencia siempre diferiría del mismo elemento, dando una diferencia de cero.

Espero que esto tenga sentido,

de Dave

3

La modificación de la respuesta de Jon Skeet a no saltar al primer elemento:

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

algunas diferencias clave ...pasa un tercer parámetro bool para indicar si es el primer elemento del enumerable. También cambié el orden de los parámetros actuales/previos.

Aquí está el ejemplo a juego:

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

embargo otro mod en la versión de Jon Skeet (gracias por su solución 1). Excepto que esto devuelve un enumerable de pares.

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

Ésta es NO devolver la primera porque se trata de devolver el intermedio entre los elementos.

usarlo como:

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

O

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

O (como Jon muestra)

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