2009-09-23 5 views
6

J'ai une requête Linq qui compte essentiellement combien d'entrées ont été créées un jour particulier, ce qui est fait en groupant par année, mois, jour. Le problème est que parce que certains jours n'auront pas d'entrées, je dois remplir ces "jours civils" manquants avec une entrée de 0 compte. Ma conjecture est que cela peut probablement être fait avec un syndicat ou quelque chose, ou peut-être même une simple boucle pour traiter les enregistrements après la requête.Remplissage des dates manquantes à l'aide d'une requête de groupe linq par date

Voici la question:

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

Si j'ai données pour 12/1 - 12/4/2009 comme (simplifié):

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

Je veux une entrée avec 12/3/2009 0 ajouté par code. Je sais qu'en général cela devrait être fait dans la base de données en utilisant une table dénormalisée que vous remplissez avec des données ou que vous vous joignez à une table de calendrier, mais ma question est comment j'accomplirais ceci dans le code?
Cela peut-il être fait dans Linq? Devrait-il être fait à Linq?

+0

double possible de [groupe LINQ par date - inclure les jours vides SANS utiliser la jointure] (http://stackoverflow.com/questions/17086120/linq-group-by-date-include-empty-days-without-using-join) – ensisNoctis

Répondre

0

Je viens de le faire aujourd'hui. J'ai rassemblé les données complètes de la base de données, puis généré un tableau "échantillon vide". Enfin, j'ai fait une jointure externe de la table vide avec les données réelles et j'ai utilisé la construction DefaultIfEmpty() pour savoir quand une ligne manquait dans la base de données pour la remplir avec les valeurs par défaut.

Voici mon code:

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

Ceci est très similaire à ce que j'ai fini par faire mais fait une Union sur les résultats au lieu d'effectuer une jointure. –

0

Vous pouvez générer la liste des dates à partir de « début » et se terminant à « finir », une étape puis par étape vérifier le nombre de comptage pour chaque jour séparément

+0

Ceci est ok, mais je voulais voir comment cela peut être fait en utilisant des constructions linq comme opérateur de l'Union. –

1

Essentiellement ce que je fini par faire ici est créer une liste du même type avec toutes les dates de la plage et 0 valeur pour le comptage. Puis fusionnez les résultats de ma requête d'origine avec cette liste. L'obstacle majeur consistait simplement à créer un IEqualityComparer personnalisé. Pour plus de détails ici: click here

0

J'ai fait une fonction d'aide qui est conçu pour être utilisé avec des types anonymes et réutilisés de façon aussi générique que possible.

Disons que c'est votre requête pour obtenir une liste de commandes pour chaque date.

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

Pour que ma fonction fonctionne, veuillez noter que cette liste doit être triée par date. Si nous avions une journée sans ventes, il y aurait un trou dans la liste.

Maintenant, pour la fonction qui remplira les blancs avec une valeur par défaut (instance de type anonyme).

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'utilisation est la suivante:

// 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 
           }); 
  • doit assurer qu'il n'y a pas de doublons dans la liste des dates souhaitée
  • Les deux desiredDates et sourceData doit être pour
  • Parce que la méthode est générique si vous utilisez un type anonyme alors le compilateur vous dira automatiquement si votre élément 'par défaut' n'est pas la même 'forme' comme un article régulier.
  • En ce moment j'inclure un chèque pour les doublons dans sourceData mais il n'y a pas de contrôle dans desiredDates
  • Si vous voulez vous assurer que les listes sont classés par date, vous devrez ajouter du code supplémentaire
+0

Je pense qu'il s'agit d'un scénario «business» si spécifique que je pense que le fait de le transformer en un linq «élégant» est contre-productif - mais c'était la deuxième chose la plus élégante que je pouvais trouver. –