web-dev-qa-db-fra.com

Regrouper par semaines dans LINQ to Entities

J'ai une application qui permet aux utilisateurs d'entrer le temps qu'ils passent à travailler, et j'essaie de créer de bons rapports pour cela qui exploitent LINQ to Entities. Parce que chaque TrackedTime a un TargetDate qui est juste la partie "Date" d'un DateTime, il est relativement simple de regrouper les heures par utilisateur et par date (je pars les clauses "où" pour simplifier):

var userTimes = from t in context.TrackedTimes
                group t by new {t.User.UserName, t.TargetDate} into ut
                select new
                {
                    UserName = ut.Key.UserName,
                    TargetDate = ut.Key.TargetDate,
                    Minutes = ut.Sum(t => t.Minutes)
                };

Grace à DateTime.Month, le regroupement par utilisateur et par mois n'est que légèrement plus compliqué:

var userTimes = from t in context.TrackedTimes
                group t by new {t.User.UserName, t.TargetDate.Month} into ut
                select new
                {
                    UserName = ut.Key.UserName,
                    MonthNumber = ut.Key.Month,
                    Minutes = ut.Sum(t => t.Minutes)
                };

Maintenant vient la partie délicate. Existe-t-il un moyen fiable de regrouper par semaine? J'ai essayé ce qui suit en me basant sur cette réponse à une question LINQ to SQL similaire:

DateTime firstDay = GetFirstDayOfFirstWeekOfYear();
var userTimes = from t in context.TrackedTimes
                group t by new {t.User.UserName, WeekNumber = (t.TargetDate - firstDay).Days / 7} into ut
                select new
                {
                    UserName = ut.Key.UserName,
                    WeekNumber = ut.Key.WeekNumber,
                    Minutes = ut.Sum(t => t.Minutes)
                };

Mais LINQ to Entities ne semble pas prendre en charge les opérations arithmétiques sur les objets DateTime, il ne sait donc pas comment faire (t.TargetDate - firstDay).Days / 7.

J'ai envisagé de créer une vue dans la base de données qui mappe simplement des jours sur des semaines, puis d'ajouter cette vue à mon contexte Entity Framework et de la joindre dans ma requête LINQ, mais cela semble être beaucoup de travail pour quelque chose comme ça. Y a-t-il une bonne solution de contournement à l'approche arithmétique? Quelque chose qui fonctionne avec Linq to Entities, que je peux simplement incorporer dans l'instruction LINQ sans avoir à toucher à la base de données? Une façon de dire à Linq aux entités comment soustraire une date d'une autre?

Résumé

Je tiens à remercier tout le monde pour leurs réponses réfléchies et créatives. Après tous ces va-et-vient, il semble que la vraie réponse à cette question soit "attendez jusqu'à .NET 4.0". Je vais donner la prime à Noldorin pour avoir donné la réponse la plus pratique qui exploite encore LINQ, avec une mention spéciale à Jacob Proffitt pour avoir proposé une réponse qui utilise Entity Framework sans avoir besoin de modifications du côté de la base de données. Il y avait également d'autres réponses intéressantes, et si vous examinez la question pour la première fois, je vous recommande fortement de lire toutes celles qui ont été votées à la hausse, ainsi que leurs commentaires. Cela a vraiment été un superbe exemple de la puissance de StackOverflow. Merci à tous!

39
StriplingWarrior

Vous devriez être en mesure de forcer la requête à utiliser LINQ to Objects plutôt que LINQ to Entities pour le regroupement, en utilisant un appel à la méthode d'extension AsEnumerable.

Essayez ce qui suit:

DateTime firstDay = GetFirstDayOfFirstWeekOfYear();
var userTimes = 
    from t in context.TrackedTimes.Where(myPredicateHere).AsEnumerable()
    group t by new {t.User.UserName, WeekNumber = (t.TargetDate - firstDay).Days / 7} into ut
    select new
    {
        UserName = ut.Key.UserName,
        WeekNumber = ut.Key.WeekNumber,
        Minutes = ut.Sum(t => t.Minutes)
    };

Cela signifierait au moins que la clause where est exécutée par LINQ to Entities, mais la clause group, qui est trop complexe à gérer pour les Entities, est exécutée par LINQ to Objects.

Faites-moi savoir si vous avez de la chance avec ça.

Mettre à jour

Voici une autre suggestion, qui pourrait vous permettre d'utiliser LINQ to Entities pour l'ensemble.

(t.TargetDate.Days - firstDay.Days) / 7

Cela étend simplement l'opération de sorte que seule la soustraction d'entiers est effectuée plutôt que la soustraction DateTime.

Il n'est actuellement pas testé, il peut donc fonctionner ou non ...

40
Noldorin

Vous pouvez également utiliser les fonctions SQL dans LINQ tant que vous utilisez SQLServer:

using System.Data.Objects.SqlClient;

var codes = (from c in _twsEntities.PromoHistory
                         where (c.UserId == userId)
                         group c by SqlFunctions.DatePart("wk", c.DateAdded) into g
                         let MaxDate = g.Max(c => SqlFunctions.DatePart("wk",c.DateAdded))
                         let Count = g.Count()
                         orderby MaxDate
                         select new { g.Key, MaxDate, Count }).ToList();

Cet exemple a été adapté du MVP Zeeshan Hirani dans ce fil: n MSDN

8
HBlackorby

J'ai eu exactement ce problème dans mon application de suivi du temps qui doit rapporter les tâches par semaine. J'ai essayé l'approche arithmétique mais je n'ai rien réussi à faire fonctionner. J'ai fini par ajouter une nouvelle colonne à la base de données avec le numéro de la semaine et le calculer pour chaque nouvelle ligne. Soudain, toute la douleur est partie. Je détestais faire ça car je suis un grand partisan de DRY mais j'avais besoin de progresser. Pas la réponse que vous voulez mais un autre point de vue des utilisateurs. Je vais regarder cela pour une réponse décente.

6
Sam Mackrill

Bizarre personne n'a encore publié la méthode GetWeekOfYear du .NET Framework.

faites simplement votre première sélection, ajoutez une propriété factice de numéro de semaine, puis bouclez-les toutes et utilisez cette méthode pour définir le numéro de semaine correct sur votre type, cela devrait être assez rapide pour faire une boucle entre 2 sélections, n'est-ce pas?:

public static int GetISOWeek(DateTime day)
{
  return System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(day, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
2
Colin

D'accord, c'est possible, mais c'est un énorme PITA et il contourne la partie LINQ de l'Entity Framework. Il a fallu quelques recherches, mais vous pouvez accomplir ce que vous voulez avec la fonction spécifique au fournisseur SqlServer SqlServer.DateDiff () et "Entity SQL". Cela signifie que vous êtes de retour aux requêtes de chaîne et au remplissage manuel des objets personnalisés, mais cela fait ce que vous voulez. Vous pourrez peut-être affiner un peu cela en utilisant un Select au lieu d'une boucle foreach et autres, mais j'ai en fait fait fonctionner cela sur un ensemble d'entités généré par le concepteur de YourEntities.

void getTimes()
{
    YourEntities context = new YourEntities();
    DateTime firstDay = GetFirstDayOfFirstWeekOfYear();
    var weeks = context.CreateQuery<System.Data.Common.DbDataRecord>(
        "SELECT t.User.UserName, SqlServer.DateDiff('DAY', @beginDate, t.TargetDate)/7 AS WeekNumber " +
        "FROM YourEntities.TrackedTimes AS t " +
        "GROUP BY SqlServer.DateDiff('DAY', @beginDate, t.TargetDate)/7, t.User.UserName", new System.Data.Objects.ObjectParameter("beginDate", firstDay));
    List<customTimes> times = new List<customTimes>();
    foreach (System.Data.Common.DbDataRecord rec in weeks)
    {
        customTimes time = new customTimes()
        {
            UserName = rec.GetString(0),
            WeekNumber = rec.GetInt32(1)
        };
        times.Add(time);
    }
}

class customTimes
{
    public string UserName{ get; set; }
    public int WeekNumber { get; set; }
}
2
Jacob Proffitt

Je n'utilise pas Linq to Entities, mais s'il prend en charge .Year, .Day .Week ainsi que la division entière et modulo, il devrait être possible de calculer le numéro de semaine en utilisant l'algorithme/congruence de Zellers.

Pour plus de détails, voir http://en.wikipedia.org/wiki/Zeller 's_congruence

Si cela en vaut la peine, je laisserai le choix à vous/aux autres.

Éditer:

Soit wikipedia est faux, soit le forumla que j'ai n'est pas celui de Zeller, je ne sais pas s'il a un nom, mais le voici

 weekno = (( 1461 * ( t.TransactionDate.Year + 4800 
           + ( t.TransactionDate.Month - 14 ) / 12 ) ) / 4
           +  ( 367 * ( t.TransactionDate.Month - 2 - 12 *
                    ( ( t.TransactionDate.Month - 14 ) / 12 ) ) ) / 12
            -   ( 3 * ( ( t.TransactionDate.Year + 4900 
            + ( t.TransactionDate.Month - 14 ) / 12 ) / 100 ) ) / 4 
            +   t.TransactionDate.Day - 32075) / 7 

Il devrait donc être possible de l'utiliser en groupe par (cela peut devoir être modifié en fonction du jour de début de votre semaine).

Condition. C'est une formule que je n'ai jamais utilisée "pour de vrai". J'ai tapé à la main (je ne m'en souviens pas, mais je l'ai peut-être copié à partir d'un livre), ce qui signifie que, même si j'ai triplé, vérifié que mes parenthèses étaient correctes, il est possible que la source soit erronée. Vous devez donc vérifier que cela fonctionne avant de l'utiliser.

Personnellement, à moins que la quantité de données ne soit massive, je préférerais renvoyer sept fois les données et filtrer localement plutôt que d'utiliser quelque chose comme ça, (ce qui explique peut-être pourquoi je n'ai jamais utilisé le forumla)

2
sgmoore

Ici est la liste des méthodes prises en charge par LINQ to Entities.

La seule option que je vois est de récupérer le numéro de semaine à partir de DateTime.Month et DateTime.Day. Quelque chose comme ça:

int yearToQueryIn = ...;
int firstWeekAdjustment = (int)GetDayOfWeekOfFirstDayOfFirstWeekOfYear(yearToQueryIn);

from t in ...
where t.TargetDate.Year == yearToQueryIn
let month = t.TargetDate.Month
let precedingMonthsLength =
    month == 1 ? 0 :
    month == 2 ? 31 :
    month == 3 ? 31+28 :
    month == 4 ? 31+28+31 : 
    ...
let dayOfYear = precedingMonthsLength + t.TargetDate.Day
let week = (dayOfYear + firstWeekAdjustment) / 7
group by ... 
2
Pavel Minaev

Je pense que le seul problème que vous rencontrez est les opérations arithmétiques sur le type DateTime, essayez l'exemple ci-dessous. Il conserve l'opération dans la base de données en tant que fonction scalaire, ne renvoie que les résultats groupés souhaités. La fonction .Days ne fonctionnera pas de votre côté car l'implémentation de TimeSpan est limitée et (principalement) disponible uniquement dans SQL 2008 et .Net 3.5 SP1, notes supplémentaires ici à ce sujet .

var userTimes = from t in context.TrackedTimes
    group t by new 
    {
        t.User.UserName, 
        WeekNumber = context.WeekOfYear(t.TargetDate)
    } into ut
    select new 
    {
        UserName = ut.Key.UserName,
        WeekNumber = ut.Key.WeekNumber,
        Minutes = ut.Sum(t => t.Minutes)
    };

Fonction dans la base de données ajoutée à votre contexte (comme context.WeekOfYear):

CREATE FUNCTION WeekOfYear(@Date DateTime) RETURNS Int AS 
BEGIN
RETURN (CAST(DATEPART(wk, @Date) AS INT))
END

Pour plus d'informations: Voici les méthodes DateTime prises en charge qui se traduisent correctement dans un scénario LINQ to SQL

1
Nick Craver

Une autre solution peut être; les enregistrements de groupe quotidiens, puis les parcourir. Lorsque vous voyez le début de la semaine, effectuez une opération de somme ou de comptage.

Voici un exemple de code qui calcule les statistiques de revenu par semaine.

La méthode GetDailyIncomeStatisticsData récupère les données de la base de données quotidiennement.

   private async Task<List<IncomeStastistic>> GetWeeklyIncomeStatisticsData(DateTime startDate, DateTime endDate)
    {
        var dailyRecords = await GetDailyIncomeStatisticsData(startDate, endDate);
        var firstDayOfWeek = DateTimeFormatInfo.CurrentInfo == null
            ? DayOfWeek.Sunday
            : DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek;

        var incomeStastistics = new List<IncomeStastistic>();
        decimal weeklyAmount = 0;
        var weekStart = dailyRecords.First().Date;
        var isFirstWeek = weekStart.DayOfWeek == firstDayOfWeek;

        dailyRecords.ForEach(dailyRecord =>
        {
            if (dailyRecord.Date.DayOfWeek == firstDayOfWeek)
            {
                if (!isFirstWeek)
                {
                    incomeStastistics.Add(new IncomeStastistic(weekStart, weeklyAmount));
                }

                isFirstWeek = false;
                weekStart = dailyRecord.Date;
                weeklyAmount = 0;
            }

            weeklyAmount += dailyRecord.Amount;
        });

        incomeStastistics.Add(new IncomeStastistic(weekStart, weeklyAmount));
        return incomeStastistics;
    }
1
Alper Ebicoglu