Подтвердить что ты не робот

Группировать по неделям в LINQ для организаций

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

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. Спасибо всем!

4b9b3361

Ответ 1

Вы можете заставить запрос использовать LINQ для объектов, а не 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, которое слишком сложно обрабатывать Entities, выполняется LINQ to Objects.

Сообщите мне, если вам повезло с этим.

Update

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

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

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

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

Ответ 2

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

Ответ 3

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

Ответ 4

Здесь - список методов, поддерживаемых 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 ... 

Ответ 5

Хорошо, это возможно, но это огромный PITA, и он обходит часть LINQ Entity Framework. Это потребовало нескольких исследований, но вы можете выполнить то, что хотите, с помощью функции SqlServer.DetDiff() и SqlServer-провайдера SqlServer. Это означает, что вы вернулись к строковым запросам и вручную заполняете пользовательские объекты, но это делает то, что вы хотите. Возможно, вы сможете немного уточнить это, используя 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; }
}

Ответ 6

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

Подробнее см. 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. Это формула, которую я никогда не использовал "по-настоящему". Я набрал вручную (не могу вспомнить, но я, возможно, скопировал его из книги), а это значит, что, хотя я проверял три раза, мои скобки правильные, возможно, источник был неправ. Таким образом, вы должны убедиться, что он работает перед использованием.

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

Ответ 7

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

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

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

Ответ 8

Я думаю, что единственная проблема, с которой вы сталкиваетесь, - это арифметические операции с типом 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

Ответ 9

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

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

Метод 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;
    }

Ответ 10

делает лямбда счет? Я начал с синтаксиса linq, но я, кажется, думаю, что в выражениях лямбда С# 3.0, когда дело доходит до декларативного программирования.

public static void TEST()
{
    // creating list of some random dates
    Random rnd = new Random();
    List<DateTime> lstDte = new List<DateTime>();
    while (lstDte.Count < 100)
    {
        lstDte.Add(DateTime.Now.AddDays((int)(Math.Round((rnd.NextDouble() - 0.5) * 100))));
    }
    // grouping 
    var weeksgrouped = lstDte.GroupBy(x => (DateTime.MaxValue - x).Days / 7);
}

Ответ 11

Вы можете попытаться сгруппировать по годам и неделям номер, получив его из свойства 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)
                };

Ответ 12

Вы можете попробовать использовать метод расширения для выполнения преобразования:

static int WeekNumber( this DateTime dateTime, DateTime firstDay) {
    return (dateTime - firstDay).Days / 7;
}

Тогда у вас будет:

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

Ответ 13

Ну, это может быть больше работы в зависимости от того, как индексируется БД, но вы можете просто сортировать по дням, а затем сделать отображение дня → недели на стороне клиента.