2009-09-23 14 views
6

Ho una query Linq che conta essenzialmente quante voci sono state create in un particolare giorno, che viene eseguito raggruppando per anno, mese, giorno. Il problema è che, poiché alcuni giorni non avranno alcuna voce, devo tornare a riempire quei "giorni di calendario" mancanti con una voce di 0 conteggio. La mia ipotesi è che questo può probabilmente essere fatto con una Union o qualcosa del genere, o forse anche qualche semplice ciclo per elaborare i record dopo la query.Compilare le date mancanti utilizzando un gruppo linq per query di data

Ecco la domanda:

from l in context.LoginToken 
where l.CreatedOn >= start && l.CreatedOn <= finish 
group l by 
new{l.CreatedOn.Year, l.CreatedOn.Month, l.CreatedOn.Day} into groups 
orderby groups.Key.Year , groups.Key.Month , groups.Key.Day 
    select new StatsDateWithCount { 
            Count = groups.Count(), 
            Year = groups.Key.Year, 
            Month = groups.Key.Month, 
             Day = groups.Key.Day 
                    })); 

Se ho i dati per 12/1 - 12/4/2009 simile (semplificato):

12/1/2009 20 
12/2/2009 15 
12/4/2009 16 

voglio una voce con 12/3/2009 0 aggiunto dal codice.

So che in generale questo dovrebbe essere fatto nel DB usando una tabella denormalizzata che si popola con i dati o si uniscono a una tabella di calendario, ma la mia domanda è come dovrei farlo nel codice?
Si può fare a Linq? Dovrebbe essere fatto a Linq?

+0

Eventuali duplicati di [LINQ gruppo per data - include giorni vuoti SENZA usare join] (http://stackoverflow.com/questions/17086120/linq-group-by-date-include-empty-days-without-using-join) – ensisNoctis

risposta

0

Ho appena fatto questo oggi. Ho raccolto i dati completi dal database e ho quindi generato una tabella "campione vuoto". Infine, ho eseguito un join esterno della tabella vuota con i dati reali e ho utilizzato il costrutto DefaultIfEmpty() per capire quando mancava una riga dal database per riempirla con i valori predefiniti.

Ecco il mio codice:

int days = 30; 

// Gather the data we have in the database, which will be incomplete for the graph (i.e. missing dates/subsystems). 
var dataQuery = 
    from tr in SourceDataTable 
    where (DateTime.UtcNow - tr.CreatedTime).Days < 30 
    group tr by new { tr.CreatedTime.Date, tr.Subsystem } into g 
    orderby g.Key.Date ascending, g.Key.SubSystem ascending 
    select new MyResults() 
    { 
     Date = g.Key.Date, 
     SubSystem = g.Key.SubSystem, 
     Count = g.Count() 
    }; 

// Generate the list of subsystems we want. 
var subsystems = new[] { SubSystem.Foo, SubSystem.Bar }.AsQueryable(); 

// Generate the list of Dates we want. 
var datetimes = new List<DateTime>(); 
for (int i = 0; i < days; i++) 
{ 
    datetimes.Add(DateTime.UtcNow.AddDays(-i).Date); 
} 

// Generate the empty table, which is the shape of the output we want but without counts. 
var emptyTableQuery = 
    from dt in datetimes 
    from subsys in subsystems 
    select new MyResults() 
    { 
     Date = dt.Date, 
     SubSystem = subsys, 
     Count = 0 
    }; 

// Perform an outer join of the empty table with the real data and use the magic DefaultIfEmpty 
// to handle the "there's no data from the database case". 
var finalQuery = 
    from e in emptyTableQuery 
    join realData in dataQuery on 
     new { e.Date, e.SubSystem } equals 
     new { realData.Date, realData.SubSystem } into g 
    from realDataJoin in g.DefaultIfEmpty() 
    select new MyResults() 
    { 
     Date = e.Date, 
     SubSystem = e.SubSystem, 
     Count = realDataJoin == null ? 0 : realDataJoin.Count 
    }; 

return finalQuery.OrderBy(x => x.Date).AsEnumerable(); 
+1

Questo è molto simile a quello che ho fatto, ma ho fatto un sindacato sui risultati invece di eseguire un join. –

0

È possibile generare l'elenco delle date a partire da "start" e termina a "finire", un poi passo dopo passo controllare il numero di conteggio per ogni data separatamente

+0

Questo è ok, ma volevo vedere come si può fare usando alcuni costrutti linq come l'operatore dell'Unione. –

1

Essenzialmente quello che ho finito per fare qui è creare un elenco dello stesso tipo con tutte le date nell'intervallo e il valore 0 per il conteggio. Quindi unione i risultati della mia query originale con questo elenco. L'ostacolo principale stava semplicemente creando un utente IEqualityComparer personalizzato. Per ulteriori dettagli: click here

0

Ho fatto una funzione di supporto che è stato progettato per essere utilizzato con i tipi anonimi, e riutilizzato in modo più generico possibile.

Diciamo che questa è la tua richiesta per ottenere un elenco di ordini per ogni data.

var orders = db.Orders 
      .GroupBy(o => o.OrderDate) 
      .Select(o => new 
      { 
       OrderDate = o.Key, 
       OrderCount = o.Count(), 
       Sales = o.Sum(i => i.SubTotal) 
      } 
      .OrderBy(o => o.OrderDate); 

Perché la mia funzione funzioni, tenere presente che questo elenco deve essere ordinato per data. Se avessimo un giorno senza vendite, ci sarebbe un buco nella lista.

Ora per la funzione che riempirà gli spazi vuoti con un valore predefinito (istanza di tipo anonimo).

private static IEnumerable<T> FillInEmptyDates<T>(IEnumerable<DateTime> allDates, IEnumerable<T> sourceData, Func<T, DateTime> dateSelector, Func<DateTime, T> defaultItemFactory) 
    { 
     // iterate through the source collection 
     var iterator = sourceData.GetEnumerator(); 
     iterator.MoveNext(); 

     // for each date in the desired list 
     foreach (var desiredDate in allDates) 
     { 
      // check if the current item exists and is the 'desired' date 
      if (iterator.Current != null && 
       dateSelector(iterator.Current) == desiredDate) 
      { 
       // if so then return it and move to the next item 
       yield return iterator.Current; 
       iterator.MoveNext(); 

       // if source data is now exhausted then continue 
       if (iterator.Current == null) 
       { 
        continue; 
       } 

       // ensure next item is not a duplicate 
       if (dateSelector(iterator.Current) == desiredDate) 
       { 
        throw new Exception("More than one item found in source collection with date " + desiredDate); 
       } 
      } 
      else 
      { 
       // if the current 'desired' item doesn't exist then 
       // create a dummy item using the provided factory 
       yield return defaultItemFactory(desiredDate); 
      } 
     } 
    } 

L'utilizzo è la seguente:

// first you must determine your desired list of dates which must be in order 
// determine this however you want  
var desiredDates = ....; 

// fill in any holes 
var ordersByDate = FillInEmptyDates(desiredDates, 

           // Source list (with holes) 
           orders, 

           // How do we get a date from an order 
           (order) => order.OrderDate, 

           // How do we create an 'empty' item 
           (date) => new 
           { 
            OrderDate = date, 
            OrderCount = 0, 
            Sales = 0 
           }); 
  • necessario assicurarsi che non vi siano duplicati nella lista date desiderato
  • Sia desiredDates e sourceData devono essere in ordine
  • Perché il il metodo è generico se si utilizza un tipo anonimo, quindi il compilatore ti dirà automaticamente se l'elemento 'predefinito' non è la stessa 'forma' come un oggetto normale.
  • In questo momento mi comprendono un assegno di elementi duplicati in sourceData ma non c'è come il check-in desiredDates
  • Se si vuole garantire le liste sono ordinate per data è necessario aggiungere codice aggiuntivo
+0

Penso che questo sia uno specifico scenario di 'business' che penso che provare a comprimerlo in un 'elegante' costruzione di linq sia controproducente - ma questa era la seconda cosa più elegante che avrei potuto inventare –

Problemi correlati