Группировка по неделям в LINQ to Entities - PullRequest
39 голосов
/ 29 июня 2009

У меня есть приложение, которое позволяет пользователям вводить время, которое они проводят за работой, и я пытаюсь создать для этого хорошую отчетность, которая использует LINQ to Entities. Поскольку каждый TrackedTime имеет TargetDate, который является просто частью "Дата" в DateTime, относительно просто сгруппировать время по пользователю и дате (я опускаю предложения "где" для простоты) :

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

Благодаря свойству DateTime.Month группировка по пользователю и месяцу лишь немного сложнее:

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

Теперь самое сложное. Есть ли надежный способ группировки по неделям? Я попробовал следующее на основе этого ответа на похожий вопрос LINQ to SQL:

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

Но LINQ to Entities не поддерживает арифметические операции над объектами DateTime, поэтому не знает, как это сделать (t.TargetDate - firstDay).Days / 7.

Я подумал о создании представления в базе данных, которое просто отображает дни в недели, а затем добавляет это представление в мой контекст Entity Framework и присоединяется к нему в моем запросе LINQ, но это похоже на большую работу для чего-то вроде этот. Есть ли хороший обходной путь для арифметического подхода? Что-то, что работает с Linq to Entities, что я могу просто включить в оператор LINQ, не касаясь базы данных? Какой-нибудь способ рассказать Linq Entities, как вычесть одну дату из другой?

Краткое описание

Я хотел бы поблагодарить всех за их вдумчивые и творческие ответы. После всего этого, похоже, реальный ответ на этот вопрос - «подождите до .NET 4.0». Я собираюсь отдать награду Нолдорину за самый практичный ответ, который до сих пор использует LINQ, и особенно упомянуть Джейкоба Проффитта за то, что он придумал ответ, который использует Entity Framework без необходимости изменений на стороне базы данных. Были и другие отличные ответы, и если вы смотрите на вопрос впервые, я настоятельно рекомендую прочитать все те, за которые проголосовали, а также их комментарии. Это действительно был превосходный пример силы StackOverflow. Спасибо всем!

Ответы [ 13 ]

40 голосов
/ 07 июля 2009

Вы должны быть в состоянии заставить запрос использовать LINQ to Objects вместо LINQ to Entities для группировки, используя вызов метода расширения AsEnumerable.

Попробуйте следующее:

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

Это, по крайней мере, означает, что предложение where выполняется LINQ to Entities, но предложение group, которое слишком сложно для обработки сущностями, выполняется LINQ to Objects.

Дайте мне знать, если вам повезет с этим.

Обновление

Вот еще одно предложение, которое может позволить вам использовать LINQ to Entities для всего этого.

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

Это просто расширяет операцию, так что выполняется только целочисленное вычитание, а не DateTime вычитание.

В настоящее время не проверено, поэтому может работать или не работать ...

8 голосов
/ 23 ноября 2011

Вы можете использовать функции SQL также в LINQ, если вы используете 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();

Этот образец был адаптирован из MVP Zeeshan Hirani в этой теме: MSDN

6 голосов
/ 08 июля 2009

У меня была именно эта проблема в моем приложении отслеживания времени, которое должно сообщать о задачах по неделям. Я попробовал арифметический подход, но не получил ничего разумного, работающего. В итоге я просто добавил новый столбец в базу данных с номером недели и вычислил его для каждой новой строки. Внезапно вся боль ушла. Я ненавидел это, потому что я большой сторонник DRY , но мне нужно было добиться прогресса. Не тот ответ, который вам нужен, а точка зрения другого пользователя. Я буду смотреть это для достойного ответа.

2 голосов
/ 10 июля 2009

Странно, что никто еще не опубликовал метод GetWeekOfYear .NET Framework.

просто сделайте свой первый выбор, добавьте фиктивное свойство weeknumber, затем зациклите все из них и используйте этот метод, чтобы установить правильный номер недели для вашего типа, он должен быть достаточно быстрым, чтобы сделать только один цикл между 2 выборами. это:

public static int GetISOWeek(DateTime day)
{
  return System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(day, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
2 голосов
/ 08 июля 2009

Я не использую Linq to Entities, но если он поддерживает .Year, .Day .Week, а также целочисленное деление и модуль, тогда должна быть возможность определить номер недели, используя алгоритм / конгруэнтность Целлера.

Подробнее см. http://en.wikipedia.org/wiki/Zeller's_congruence

Стоит ли усилий, я оставлю до вас / других.

Edit:

Либо Википедия не так, либо форум, который у меня есть, не принадлежит Зеллеру, я не знаю, есть ли у него имя, но вот оно

 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 

Так что должно быть возможно использовать это в группе (Это может потребоваться настроить в зависимости от того, в какой день начинается ваша неделя).

Proviso. Это формула, которую я никогда не использовал «по-настоящему». Я набрал вручную (не помню, но, возможно, я скопировал его из книги), что означает, что, хотя я и проверил трижды, у меня были правильные скобки, возможно, источник был неверным. Итак, перед использованием вы должны убедиться, что он работает.

Лично, если объем данных не огромен, я бы предпочел возвращать данные в семь раз и фильтровать их локально, а не использовать что-то подобное (что, возможно, объясняет, почему я никогда не использовал форум)

2 голосов
/ 08 июля 2009

Хорошо, это возможно, но это огромный PITA, и он обходит часть LINQ Entity Framework. Потребовалось некоторое исследование, но вы можете выполнить то, что вы хотите, с помощью специфических для поставщика SqlServer функций SqlServer.DateDiff () и «Entity SQL». Это означает, что вы вернулись к строковым запросам и вручную заполнили пользовательские объекты, но это делает то, что вы хотите. Возможно, вам удастся немного уточнить это, используя Select вместо цикла foreach и т. Д., Но я действительно заставил его работать с созданным дизайнером набором сущностей 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 голосов
/ 08 июля 2009

Здесь - список методов, поддерживаемых LINQ to Entities.

Единственный вариант, который я вижу, - это получить номер недели из DateTime.Month и DateTime.Day. Как то так:

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 ... 
1 голос
/ 17 мая 2017

Другое решение может быть; ежедневные групповые записи, а затем перебирать их. Когда вы видите начало недели, выполните операцию суммирования или подсчета.

Вот пример кода, который рассчитывает статистику доходов по неделям.

Метод GetDailyIncomeStatisticsData извлекает данные из базы данных на ежедневной основе.

   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 голос
/ 12 июля 2009

Я думаю, что единственная проблема, с которой вы столкнулись - это арифметические операции с типом DateTime, попробуйте пример ниже. Он сохраняет операцию в БД как скалярную функцию, возвращает только сгруппированные результаты, которые вы хотите. Функция .Days не будет работать на вашем конце, потому что реализация TimeSpan ограничена и (в основном) доступна только в SQL 2008 и .Net 3.5 SP1, дополнительные примечания здесь по этому .

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

Функция в базе данных добавлена ​​в ваш контекст (как context.WeekOfYear):

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

Для дополнительной справки: Вот поддерживаемые методы DateTime, которые правильно переводятся в сценарии LINQ to SQL

0 голосов
/ 04 февраля 2010

Вы можете попытаться сгруппировать по году и номеру недели, получив его из свойства DayOfYear

var userTimes = from t in context.TrackedTimes
                group t by new {t.User.UserName, Year =  t.TargetDate.Year, WeekNumber = t.TargetDate.DayOfYear/7} into ut
                select new
                {
                    UserName = ut.Key.UserName,
                    WeekNumber = ut.Key.WeekNumber,
                    Minutes = ut.Sum(t => t.Minutes)
                };
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...