在LINQ to Entities中按周分组

时间:2009-06-29 18:28:32

标签: c# linq entity-framework linq-to-entities

我有一个应用程序,允许用户输入他们工作的时间,我正在尝试为此构建一个利用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)
                };

现在是棘手的部分。是否有可靠的方式按周分组?我基于this response尝试了以下类似的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

我考虑在数据库中创建一个只需几天到几周的视图,然后将该视图添加到我的实体框架上下文并在我的LINQ查询中加入它,但这对于类似的东西似乎很多工作这个。算术方法有一个很好的解决方法吗?与Linq to Entities一起使用的东西,我可以简单地将其合并到LINQ语句中而无需触及数据库?某种方式告诉Linq实体如何从另一个中减去一个日期?

摘要

我要感谢大家的深思熟虑和创造性的回应。在所有这些来回之后,看起来这个问题的真正答案是“等到.NET 4.0。”我将向Noldorin提供奖励,以提供仍然利用LINQ的最实用的答案,特别提到Jacob Proffitt提出的使用实体框架的响应,而无需在数据库端进行修改。还有其他很好的答案,如果你是第一次看到这个问题,我强烈建议你阅读所有已被投票的人,以及他们的评论。这确实是StackOverflow功能的极好例子。谢谢大家!

13 个答案:

答案 0 :(得分:40)

您应该能够使用对AsEnumerable扩展方法的调用来强制查询使用LINQ to Objects而不是LINQ to Entities进行分组。

尝试以下方法:

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

这至少意味着LINQ to Entities会执行where子句,但是group子句对于实体来说太复杂了,它由LINQ to Objects完成。

如果你对此有任何好运,请告诉我。

更新

这是另一个建议,它可能允许你使用LINQ to Entities来完成整个事情。

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

这只是扩展了操作,因此只执行整数减法而不是DateTime减法。

它目前尚未经过测试,因此可能会也可能不会......

答案 1 :(得分:8)

只要您使用SQLServer:

,您也可以在LINQ中使用SQL函数
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: a MSDN

答案 2 :(得分:6)

我的时间跟踪应用程序中确实存在这个问题,需要按周报告任务。我尝试了算术方法但没有得到任何合理的工作。我最终只是在数据库中添加一个新列,其中包含周数,并为每个新行计算。突然间,所有的痛苦都消失了。我讨厌这样做,因为我是DRY的忠实信徒,但我需要取得进步。不是你想要的答案,而是另一个用户的观点。我会看到这个答案。

答案 3 :(得分:2)

Here是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 ... 

答案 4 :(得分:2)

好的,这是可能的,但它是一个巨大的PITA,它绕过了实体框架的LINQ部分。它需要一些研究,但您可以使用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; }
}

答案 5 :(得分:2)

我不使用Linq to Entities,但如果它支持.Year,.Day .Week以及整数除法和模数,则应该可以使用Zellers算法/同余来计算周数。

有关详细信息,请参阅http://en.wikipedia.org/wiki/Zeller's_congruence

是否值得付出努力,我将留给你/其他人。

编辑:

维基百科都错了,或者我所拥有的论坛不是Zeller的,我不知道它是否有名字,但这里是

 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 

所以应该可以在一个组中使用它 (这可能需要根据您的一周开始的日期进行调整。)

限制性条款。这是一个我从未用过'真实'的公式。我手工输入(不记得了,但我可能已经从书中复制了),这意味着,虽然我检查了三倍,但我的括号是正确的,但可能是源错了。 因此,您需要在使用之前验证它是否有效。

就个人而言,除非数据量很大,否则我宁愿返回数据的七倍并在本地过滤,而不是使用类似的东西(这也许可以解释为什么我从未使用过论坛)

答案 6 :(得分:2)

很奇怪没有人发布.NET Framework的GetWeekOfYear方法。

只做第一次选择,添加一个虚拟的weeknumber属性,然后循环遍历所有这些属性并使用此方法在您的类型上设置正确的周数,它应该足够快,以便在两个选择之间执行jhust一个循环不应该它:

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

答案 7 :(得分:1)

我认为你遇到的唯一问题是对DateTime类型的算术运算,请尝试以下示例。它将DB中的操作保持为标量函数,仅返回所需的分组结果。由于TimeSpan实现有限且(大部分)仅在SQL 2008和.Net 3.5 SP1 additional notes here on that中可用./ / p>

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

供参考: Here are the supported DateTime methods that do translate properly in a LINQ to SQL scenario

答案 8 :(得分:1)

另一种解决方案是;每日组记录,然后迭代它们。当你看到周开始时,然后进行总和或计数操作。

以下是按周计算收入统计数据的示例代码。

  

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

答案 9 :(得分:-1)

lambda算吗?我从linq语法开始,但在声明性编程时,我似乎在C#3.0 lambda表达式中思考。

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

答案 10 :(得分:-1)

您可以尝试按年份和周数分组,从日期属性

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

答案 11 :(得分:-2)

您可以尝试使用扩展方法进行转换:

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

答案 12 :(得分:-2)

嗯,根据数据库的索引方式,可能会有更多的工作,但你可以按天排序,然后按天排序 - >客户端的周映射。