views:

3261

answers:

11

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!

A: 

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
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
My mistake - I've only used linq-to-objects.
dmo
A: 

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
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
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
sorry. I just read the reminder of your question that actually suggests the same technique.
Robert Koritnik
+8  A: 

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

Noldorin
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
@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
See my updated answer.
Noldorin
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
@StriplingWarrior: How about DayOfYear? Are there *any* properties supported by LINQ to Entities?
Noldorin
Pavel's response provides a list of supported LINQ to Entities methods.
StriplingWarrior
+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
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
That's true. Congratulations on earning the bounty.
StriplingWarrior
+2  A: 

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
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
+2  A: 

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
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
+3  A: 

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
Probably not the result I would go with, for obvious reasons, but +1 for serious effort and thinking out of the box!
StriplingWarrior
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
+1  A: 

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
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
+1  A: 

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
It's good to know that method exists, but since it isn't supported by the Entity Framework, it doesn't answer the question.
StriplingWarrior
A: 

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);
}
x13
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
+1  A: 

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
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
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
A: 

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
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
Yep, sorry, I'm using llblgen and it is working well in simple queries.
Danil