40

I have an application that allows users to enter time they spend working, and I'm trying to get some good reporting built for this which leverages LINQ to Entities. Because each TrackedTime has a TargetDate which is just the "Date" portion of a DateTime, it is relatively simple to group the times by user and date (I'm leaving out the "where" clauses for simplicity):

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

Thanks to the DateTime.Month property, grouping by user and Month is only slightly more complicated:

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

Now comes the tricky part. Is there a reliable way to group by Week? I tried the following based on this response to a similar LINQ to SQL question:

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

But LINQ to Entities doesn't appear to support arithmetic operations on DateTime objects, so it doesn't know how to do (t.TargetDate - firstDay).Days / 7.

I've considered creating a View in the database that simply maps days to weeks, and then adding that View to my Entity Framework context and joining to it in my LINQ query, but that seems like a lot of work for something like this. Is there a good work-around to the arithmetic approach? Something that works with Linq to Entities, that I can simply incorporate into the LINQ statement without having to touch the database? Some way to tell Linq to Entities how to subtract one date from another?

Summary

I'd like to thank everyone for their thoughtful and creative responses. After all this back-and-forth, it's looking like the real answer to this question is "wait until .NET 4.0." I'm going to give the bounty to Noldorin for giving the most practical answer that still leverages LINQ, with special mention to Jacob Proffitt for coming up with a response that uses the Entity Framework without the need for modifications on the database side. There were other great answers, too, and if you're looking at the question for the first time, I'd strongly recommend reading through all of the ones that have been up-voted, as well as their comments. This has really been a superb example of the power of StackOverflow. Thank you all!

Community
  • 1
  • 1
StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • Can you please clarify the requirements regarding cross-year queries? From seeing `GetFirstDayOfFirstWeekOfYear` in your example, I assumed that you're basically querying only records for which year can be assumed to be the same. Are you actually looking at querying arbitrary dates, counting weeks from the arbitrary specified point (which may or may not be the first day of the year)? Or is it still single-year only, and merely adjusting to account for "short week" at the beginning of the year will be enough? – Pavel Minaev Jul 09 '09 at 21:01
  • Yes, sorry. With `GetFirstDayOfFirstWeekOfYear` I was trying to indicate that there was some date from which I could subtract all the other dates to figure out which week they are in. Ultimately, I'd like to represent the weeks as date ranges (7/12/09 - 7/18/09), but I figured that was outside the scope of this question. – StriplingWarrior Jul 13 '09 at 16:20

13 Answers13

40

You should be able to force the query to use LINQ to Objects rather than LINQ to Entities for the grouping, using a call to the AsEnumerable extension method.

Try the following:

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

This would at least mean that the where clause gets executed by LINQ to Entities, but the group clause, which is too complex for Entities to handle, gets done by LINQ to Objects.

Let me know if you have any luck with that.

Update

Here's another suggestion, which might allow you to use LINQ to Entities for the whole thing.

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

This simply expands the operation so that only integer subtraction is performed rather than DateTime subtraction.

It is currently untested, so it may or may not work...

Community
  • 1
  • 1
Noldorin
  • 144,213
  • 56
  • 264
  • 302
  • 1
    As I commented to dmo, this approach would probably be functional, but it would eliminate a lot of the advantage of using Linq to Entities. It would cause a lot more data to be transferred between the database and application than should be necessary. As far as I'm concerned, a correct solution is one that enables Linq to Entities to do the grouping on the database side. – StriplingWarrior Jul 06 '09 at 20:32
  • @StriplingWarrior: dmo did not suggest the same thing (at least, his solution would not seem to work), but yeah, I see your point... It does indeed become a data transfer problem. – Noldorin Jul 06 '09 at 20:45
  • 1
    DateTime doesn't have a .Days property. The .Days comes from the TimeSpan object that gets created when you subtract two DateTimes, which brings us back to our original problem because we can't subtract two DateTimes. I would try using .Ticks and multiplying by the number of ticks in a day, but .Ticks doesn't work in the Entity Framework either. Based on some other forums, it looks like as they were nearing the deadline for .NET 3.5, they decided to withdraw their limited DateDiff support in order to provide more full support in .NET 4. Maybe I'll just have to wait. – StriplingWarrior Jul 07 '09 at 16:56
  • @StriplingWarrior: How about DayOfYear? Are there *any* properties supported by LINQ to Entities? – Noldorin Jul 07 '09 at 19:30
  • Pavel's response provides a list of supported LINQ to Entities methods. – StriplingWarrior Jul 09 '09 at 17:48
  • 1
    +1, though, for not only coming up with a practical (if not perfect) solution, but also including clear sample code, which dmo failed to do. – StriplingWarrior Jul 09 '09 at 18:50
  • Thanks... and yeah, you've probably got several imperfect solutions here that could just about do the job. The "perfect" solution will probably only come with the EF release of .NET 4.0. – Noldorin Jul 09 '09 at 21:11
  • That's true. Congratulations on earning the bounty. – StriplingWarrior Jul 13 '09 at 16:31
  • Can you please help me with this linq query:http://stackoverflow.com/questions/38120664/how-to-group-by-on-2-child-entities-and-get-total-of-both-this-child-entities – I Love Stackoverflow Jun 30 '16 at 13:14
8

You can use SQL functions also in the LINQ as long as you are using 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();

This sample was adapted from MVP Zeeshan Hirani in this thread: a MSDN

HBlackorby
  • 1,308
  • 1
  • 11
  • 18
  • Can you expand this example to show how this would allow someone to group items into weeks? – StriplingWarrior Nov 22 '11 at 21:10
  • I meant to say you can use ALL the date functions in this manner if you provide related EDM functions and slightly re-write your queries to be less object-orientated. I am using this on a database query myself. – HBlackorby Nov 22 '11 at 21:27
  • It'll involve using AddDays mostly; I'll work through a full sample and post it up. – HBlackorby Nov 22 '11 at 21:30
  • I apologize for the confusion on this post; I edited my answer to use SqlFunctions instead after discovering that I cannot use two nested EDM functions in Linq; The Linq processor blows up with invalid table aliases in the SQL statement. – HBlackorby Nov 22 '11 at 22:02
7

I had exactly this problem in my Time tracking application which needs to report tasks by week. I tried the arithmetic approach but failed to get anything reasonable working. I ended-up just adding a new column to the database with the week number in it and calculating this for each new row. Suddenly all the pain went away. I hated doing this as I am a great believer in DRY but I needed to make progress. Not the answer you want but another users point of view. I will be watching this for a decent answer.

Sam Mackrill
  • 4,004
  • 8
  • 35
  • 55
  • It sounds like you went with a pattern somewhat similar to the View I was thinking of creating. Thanks for taking the time to let me know that it worked for you. – StriplingWarrior Jul 09 '09 at 18:31
2

Here is the list of methods supported by LINQ to Entities.

The only option that I see is to retrieve the week number from DateTime.Month and DateTime.Day. Something like this:

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 ... 
Pavel Minaev
  • 99,783
  • 25
  • 219
  • 289
  • 1
    If I understand your example correctly, you are simply finding the day of the year and dividing it by 7. This would be correct if I were counting weeks beginning at the first day of the year. Instead I am looking to make weeks always start on a given day (e.g. Sunday). Also, it would be good to have the query work across years (e.g. between December '09 and January '10). +1 for the list of supported methods, though! – StriplingWarrior Jul 09 '09 at 18:47
2

Okay, this is possible, but it's a huge PITA and it bypasses the LINQ part of the Entity Framework. It took some research, but you can accomplish what you want with the SqlServer provider-specific function SqlServer.DateDiff() and "Entity SQL". This means you're back to string queries, and manually populating custom objects, but it does what you want. You might be able to refine this a bit by using a Select instead of a foreach loop and such, but I actually got this to work on a designer-generated entity set of 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; }
}
Jacob Proffitt
  • 12,664
  • 3
  • 41
  • 47
  • Probably not the result I would go with, for obvious reasons, but +1 for serious effort and thinking out of the box! – StriplingWarrior Jul 09 '09 at 18:27
  • Bear in mind that the underlying object "weeks" is still an IQueryable and you can thus build it up iteratively. So, for example, after the above, you could do something like weeks = weeks.Where("t.User = 'Smith'"); – Jacob Proffitt Jul 09 '09 at 21:17
2

I don't use Linq to Entities, but if it supports .Year, .Day .Week as well as integer division and modulo then it should be possible to work out the week number using Zellers algorithm/congruence.

For more details see http://en.wikipedia.org/wiki/Zeller's_congruence

Whether it is worth the effort, I shall leave up to you/others.

Edit:

Either wikipedia is wrong, or the forumla I have is not Zeller's , I don't know if it has a name, but here it is

 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 

So it should be possible to use this in a group by (This may need to be tweaked depending on what day your week starts on).

Proviso. This is a formula that I have never used 'for real'. I typed in by hand (can't remember, but I may have copied it from a book), which means that, although I tripled-checked I had my brackets correct, it is possible the source was wrong. So, you need to verify that it works before using.

Personally, unless the amount of data is massive, I would prefer to return seven times the data and filter locally rather than use something like this, (which maybe explains why I have never used the forumla)

sgmoore
  • 15,694
  • 5
  • 43
  • 67
  • I agree with your assessment: better to lose performance and gain readability, etc. I realize that I've been demanding more than is really "practical" as a response to this question, but I think it was worth it to encourage creative answers. +1 for a crazy response the might actually work. – StriplingWarrior Jul 09 '09 at 18:30
2

Weird no one's posted the GetWeekOfYear method of the .NET Framework yet.

just do your first select, add a dummy weeknumber property, then loop over all of them and use this method to set the correct weeknumber on your type, it should be fast enough to do jhust one loop between 2 selects shouldn't it?:

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

Another solution can be; daily group records and then iterate through them. When you see week start then make sum or count operation.

Here is a sample code that calculates income statistics by week.

GetDailyIncomeStatisticsData method retrieves data from database on daily bases.

   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;
    }
Alper Ebicoglu
  • 8,884
  • 1
  • 49
  • 55
1

I think the only problem you're having is arithmetic operations on the DateTime Type, try the example below. It keeps the operation in the DB as a scalar function, only returns the grouped results you want. The .Days function won't work on your end because TimeSpan implementation is limited and (mostly) only available in SQL 2008 and .Net 3.5 SP1, additional notes here on that.

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

Function in the database added to your context (as context.WeekOfYear):

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

For additional reference: Here are the supported DateTime methods that do translate properly in a LINQ to SQL scenario

Nick Craver
  • 623,446
  • 136
  • 1,297
  • 1,155
  • This looks promising. Similar to the View idea I had, but a lot more reusable. I'll see if it works tomorrow morning. If so, you'll probably get the bounty. – StriplingWarrior Jul 13 '09 at 01:07
  • You were so very close, but we are once again thwarted by the inadequacies of the current version of the Entity Framework, which doesn't allow us to call User-Defined Functions using Linq to Entities. +1 for giving such a complete response, though. – StriplingWarrior Jul 13 '09 at 15:58
-1

does lambda count? I started with linq syntax but I seem to think in C# 3.0 lambda expressions when it comes to declarative programming.

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);
}
Your Friend Ken
  • 8,644
  • 3
  • 32
  • 41
  • The difficulty I'm having is not in creating syntax that puts the days into weeks. The problem is that the Entity Framework cannot evaluate the "DateTime.MaxValue - x" so this won't work once you try to actually evaluate the query. – StriplingWarrior Jul 13 '09 at 01:05
-1

You can try to group by year and weekNumber by getting it from DayOfYear property

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)
                };
Danil
  • 1,883
  • 1
  • 21
  • 22
  • 1
    If you look at the comments in the accepted answer, you will see that Noldorin asked about using DayOfYear. It turns out DayOfYear is not supported by Linq to Entities. Pavel's response includes a link to the list of supported methods. Besides, this would make each "week" begin with the day on which the year began, which doesn't translate very well to most people's business needs. – StriplingWarrior Feb 04 '10 at 16:39
  • Yep, sorry, I'm using llblgen and it is working well in simple queries. – Danil Feb 05 '10 at 10:46
-2

You could try an extension method for doing the conversion:

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

Then you'd have:

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)
                };
dmo
  • 3,993
  • 6
  • 35
  • 39
  • 1
    This won't work because LINQ to Entities can't convert the invocation of the WeekNumber extension method into something it can apply to the data layer--in short, for the same reason I can't use the arithmetic directly. – StriplingWarrior Jun 30 '09 at 23:25
  • My mistake - I've only used linq-to-objects. – dmo Jul 01 '09 at 00:11
-2

Well, it might be more work depending on how the DB is indexed, but you could just sort by day, then do the day -> week mapping on the client side.

dmo
  • 3,993
  • 6
  • 35
  • 39
  • The database-application connection is one of the biggest bottlenecks in an application like this. What you're suggesting would be easy enough to code, but it would cause 7x the data to be transferred from the db, plus the added time for the application to sum up all the data that gets returned--an operation that databases are optimized for. It could work, but it's not quite what I'm looking for. – StriplingWarrior Jul 01 '09 at 15:38
  • if your db-app connection is the problem, then I suggest you create a view with an additional computed column that calculates week of the year combined with year number like 012000..542000. And return grouped by this calculated column. – Robert Koritnik Jul 08 '09 at 21:09
  • sorry. I just read the reminder of your question that actually suggests the same technique. – Robert Koritnik Jul 08 '09 at 21:12