I've created two entities (simplified) in C#:
class Log {
entries = new List<Entry>();
DateTime Date { get; set; }
IList<Entry> entries { get; set; }
}
class Entry {
DateTime ClockIn { get; set; }
DateTime ClockOut { get; set; }
}
I am using the following code to initialize the objects:
Log log1 = new Log() {
Date = new DateTime(2010, 1, 1),
};
log1.Entries.Add(new Entry() {
ClockIn = new DateTime(0001, 1, 1, 9, 0, 0),
ClockOut = new DateTime(0001, 1, 1, 12, 0, 0)
});
Log log2 = new Log()
{
Date = new DateTime(2010, 2, 1),
};
The method below is used to get the date logs:
var query =
from l in DB.GetLogs()
from e in l.Entries
orderby l.Date ascending
select new
{
Date = l.Date,
ClockIn = e.ClockIn,
ClockOut = e.ClockOut,
};
The result of the above LINQ query is:
/*
Date | Clock In | Clock Out
01/01/2010 | 09:00 | 12:00
*/
My question is, what is the best way to rewrite the LINQ query above to include the results from the second object I created (Log2), since it has an empty list. In the other words, I would like to display all dates even if they don't have time values.
The expected result would be:
/*
Date | Clock In | Clock Out
01/01/2010 | 09:00 | 12:00
02/01/2010 | |
*/
Try this:
var query =
from l in DB.GetLogs()
from e in l.Entries.DefaultIfEmpty()
orderby l.Date ascending
select new
{
Date = l.Date,
ClockIn = e == null ? null : e.ClockIn,
ClockOut = e == null ? null : e.ClockOut,
};
See the docs for DefaultIfEmpty for more information on it.
EDIT: You might want to just change it to perform the final part in memory:
var dbQuery =
from l in DB.GetLogs()
from e in l.Entries.DefaultIfEmpty()
orderby l.Date ascending
select new { Date = l.Date, Entry = e };
var query = dbQuery.AsEnumerable()
.Select(x => new {
Date = x.Date,
ClockIn = x.Entry == null ? null : x.Entry.CLockIn,
ClockOut = x.Entry == null ? null : x.Entry.CLockOut
});
This is building on top of Jon's solution. Using it I get the following error:
Cannot assign to anonymous type
property
I have updated Jon's example to the following and it appears to give the desired result:
var logs = new []{log1,log2};
var query =
from l in logs.DefaultIfEmpty()
from e in l.entries.DefaultIfEmpty()
orderby l.Date ascending
select new
{
Date = l.Date,
ClockIn = e == null ? (DateTime?)null : e.ClockIn,
ClockOut = e == null ? (DateTime?)null : e.ClockOut,
};
query.Dump();
Andrew
P.S. the .Dump() is due to me using LINQ Pad.
You should have a look at the LINQ Union Operator.
http://srtsolutions.com/public/blog/251070
Related
I have one table in database named Balance and a list of dates as follows:
List<string> allDates = { "2021-01-02", "2021-01-03", "2021-01-04" }
Balance table:
Id, Amount, BalanceDate
1, 233, "2021-01-02"
2, 442, "2021-01-03
I need to fetch the records in Balance table with amount 0 for the missing dates. For example:
233, "2021-01-02"
442, "2021-01-03"
0, "2021-01-04"
I have tried the following:
balnces.GroupJoin(allDates,
balance => balance.Date,
d => d,
(balance, d) => balance);
But the records are still the same (only the ones in the balance table)
Given a data structure from database:
private class balance
{
public int id { get; set; }
public double amount { get; set; }
public string date { get; set; }
}
You get your data as you want (this is only a mock-up)
List<string> allDates = new List<string> { "2021-01-02", "2021-01-03", "2021-01-04" };
List<balance> balances = new List<balance>();
balances.Add(new balance { id = 1, amount = 233 , date = "2021-01-02" });
balances.Add(new balance { id = 2, amount = 442, date = "2021-01-03" });
you can get your desired result this way:
List<balance> result = allDates.Select(d=>
new balance {
amount =
balances.Any(s=> s.date == d)?
balances.FirstOrDefault(s => s.date == d).amount:0,
date = d
}).ToList();
If your default contains a 0 in amount instead a null, you can skip the .Any check
Assumption
Balance query had been materialized and data are returned from the database.
Solution 1: With .DefaultIfEmpty()
using System.Linq;
var result = (from a in allDates
join b in balances on a equals b.Date.ToString("yyyy-MM-dd") into ab
from b in ab.DefaultIfEmpty()
select new { Date = a, Amount = b != null ? b.Amount : 0 }
).ToList();
Sample Program for Solution 1
Solution 2: With .ToLookup()
var lookup = balances.ToLookup(x => x.Date.ToString("yyyy-MM-dd"));
var result = (from a in allDates
select new
{
Date = a,
Amount = lookup[a] != null && lookup[a].Count() > 0 ? lookup[a].First().Amount : 0
}
).ToList();
Sample Program for Solution 2
I have a C# list items as follows-
List<MyClass> All_Items = GetListItems();
GetListItems() returns the result as follows-
Category StartDate EndDate
AA 2008-05-1
AA 2012-02-1
BB 2009-09-1
BB 2010-08-1
CC 2009-10-1
Using LINQ on All_Items, I want to update EndDate column in a way that if
If the current Category's StartDate is less than the Start Date of next bigger date item within same Category then use one less day than that of bigger date.
If there is no bigger date remaining then update to 2099-12-31
Final result is as follows-
Category StartDate EndDate
AA 2008-05-1 2012-01-31
AA 2012-02-1 2099-12-31
BB 2009-09-1 2010-07-31
BB 2010-08-1 2099-12-31
CC 2009-10-1 2099-12-31
I can only think of getting it done using too many loops. What is the better option?
Try this code. It Loops over all items and selects the next bigger item.StartDate for the same category.
If such an item is not available it sets you default date.
I couldn't Test the code as I'm writing on my mobile, so any correction is welcome.
foreach(var item in All_Items)
{
var nextItem = (from i in All_Items
where i != null &&
i.Category == item.Category &&
i.StartDate > item.StartDate
orderby i.StartDate
select i).FirstOrDefault();
item.EndDate = nextItem != null ? nextItem.StartDate.AddDays(-1) : new DateTime(2099,12,31);
}
LINQ is not good for processing dependencies between elements of a sequence, and for sure is not intended for updating.
Here is the simple and efficient way to achieve the goal:
var groups = All_Items.OrderBy(item => item.StartDate).GroupBy(item => item.Category);
foreach (var group in groups)
{
MyClass last = null;
foreach (var item in group)
{
if (last != null) last.EndDate = item.StartDate.AddDays(-1);
last = item;
}
last.EndDate = new DateTime(2099, 12, 31);
}
So we use LINQ just to order the elements by StartDate and group the result by Category (which preserves the ordering inside each group). Then simply iterate the LINQ query result and update the EndDate accordingly.
You can select dates for each category and put it into dictionary to save time later.
Then you just go through all your items and check if the start date less than next in category or not, according to you requirements.
Here it is:
var categoryDictionary = All_Items
.GroupBy(i => i.Category)
.ToDictionary(
g => g.Key,
g => g.Select(i => i.StartDate));
var defaultDate = DateTime.Parse("2099-12-31");
foreach (var item in All_Items)
{
var nextDateInCategory = categoryDictionary[item.Category]
.Where(i => i > item.StartDate)
.OrderBy(i => i)
.FirstOrDefault();
item.EndDate =
nextDateInCategory != default(DateTime)
? nextDateInCategory.AddDays(-1)
: defaultDate;
}
Let's assume your MyClass looks something like this:
public class MyClass
{
public string Category { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
Here is how you can do it, see the comments in the code for an explanation.
IEnumerable<MyClass> All_Items = new List<MyClass>
{
new MyClass { Category = "AA", StartDate = new DateTime(2008, 5, 1) },
new MyClass { Category = "AA", StartDate = new DateTime(2012, 2, 1) },
new MyClass { Category = "BB", StartDate = new DateTime(2009, 9, 1) },
new MyClass { Category = "BB", StartDate = new DateTime(2010, 8, 1) },
new MyClass { Category = "CC", StartDate = new DateTime(2009, 10, 1) }
}
// Group by category
.GroupBy(c => c.Category)
// Colapse the groups into a single IEnumerable
.SelectMany(g =>
{
// Store the already used dates
List<DateTime> usedDates = new List<DateTime>();
// Get a new MyClass that has the EndDate set, from each MyClass in the category
return g.Select(c =>
{
// Get all biggerDates that were not used already
var biggerDates = g.Where(gc => gc.StartDate > c.StartDate && !usedDates.Any(ud => ud == gc.StartDate));
// Set the endDate to the default one
DateTime date = new DateTime(2099, 12, 31);
// If a bigger date was found, mark it as used and set the EndDate to it
if (biggerDates.Any()) {
date = biggerDates.Min(gc => gc.StartDate).AddDays(-1);
usedDates.Add(date);
}
return new MyClass
{
Category = c.Category,
StartDate = c.StartDate,
EndDate = date
};
});
});
In a single LINQ statement (maxEndDate is 2099-12-31):
All_Items.GroupBy(category => category.Category).Select(key =>
{
var maxCategoryStartDate = key.Max(value => value.StartDate);
return key.Select(v => {
if (DateTime.Equals(v.StartDate, maxCategoryStartDate))
{
v.EndDate = maxEndDate;
}
else
{
v.EndDate = maxCategoryStartDate - TimeSpan.FromDays(1);
}
return v;
});
}
).SelectMany(x => x);
This is my table structure
ID A B C D
1 null 10 5 null
2 3 5 null D2
3 8 null 2 D2
4 null 4 3 D1
5 4 6 1 D2
This is c# class and its property to store query result.
public class GrillTotals
{
public int? SumOfA {get; set;}
public int? SumOfB{get; set;}
public int? SumOfC{get; set;}
public int? CountOfD1{get; set;}
public int? CountOfD2{get; set;}
}
What I expect is:
SumOfA = 15
SumOfB = 20
SumOfC = 11
CountOfD1 = 1
CountOfD2 = 3
What I am getting is :
SumOfA = null,
SumOfB = null,
SumOfC = null,
CountOfD1 = 0,
CountOfD2 = 0
Here is a code what I have tried.
var _FinalResult = from s in dbContext.tblSchedules
group s by new
{
s.A,
s.B,
s.C,
s.D,
} into gt
select new GrillTotals
{
SumOfA = gt.Sum(g => g.A),
SumOfB = gt.Sum(g => g.B),
SumOfC = gt.Sum(g => g.C),
CountOfD1 = gt.Count(g => g.D == "D1"),
CountOfD2 = gt.Count(g => g.D == "D2"),
};
Try to correct me if I am doing something wrong or incorrectly.Any help will be appreciated.
You should not be grouping by the fields you want to calculate aggregates. When you group by them, every aggregate (Sum, Min, Max etc) will return the value itself (and Count 1 or 0 depending of the condition).
From what I see you are trying to return several aggregates with single SQL query. If that's correct, it's possible by using group by constant technique.
Just replace
group s by new
{
s.A,
s.B,
s.C,
s.D,
} into gt
with
group s by 1 // any constant
into gt
Try this:
var _FinalResult = from s in dbContext.tblSchedules
group s by new
{
s.A,
s.B,
s.C,
s.D,
} into gt
select new GrillTotals
{
SumOfA = gt.Sum(g => g.A ?? 0),
SumOfB = gt.Sum(g => g.B ?? 0),
SumOfC = gt.Sum(g => g.C ?? 0),
CountOfD1 = gt.Count(g => g.D == "D1"),
CountOfD2 = gt.Count(g => g.D == "D2"),
};
I'm trying to convert the following SQL from Oracle into a Linq to Entity query.
ORDER by
case when(e.prev_co = 'ABC' and(nvl(co_seniority, '1-jan-2099') < to_date('10-apr-2001')))
then '2001-04-01'
else to_char(nvl(co_seniority, '1-jan-2099'), 'YYYY-MM-DD') end,
nvl(co_seniority, '1-jan-2099'),
birth_dt
I was hoping I could use a function to pass in some parameters and have it return the correct date. I tried creating an new property called SortDate and then created a function on my page that would take in the parameters and return the correct date but that didn't work. I get and exception that says "LINQ to Entities does not recognize the method GetSortDate".
Model
SortByDate = GetSortDate(e.PREV_CO, e.CO_SENIORITY),
Function
public static DateTime GetSortDate(string PreviousCo, DateTime? CoSeniorityDate)
{
//set variable to default date
DateTime sortDate = System.DateTime.Parse("2001-04-01");
//set default date if NULL
if (CoSeniorityDate == null)
{
CoSeniorityDate = System.DateTime.Parse("2099-01-01");
}
if (PreviousCo == "ABC" && (CoSeniorityDate < System.DateTime.Parse("2001-04-10")))
{
sortDate = System.DateTime.Parse("2001-04-01");
}
else
{
sortDate = System.DateTime.Parse(CoSeniorityDate.ToString());
}
return sortDate;
}
Here is my complete EF
using (DataContext db = new DataContext())
{
db.Configuration.AutoDetectChangesEnabled = false; //no changes needed so turn off for performance.
var workStatus = new string[] { "1", "3" };
var company = new string[] { "EX", "SM" };
var eventReason = new string[] { "21", "22", "23" };
data = (from e in db.EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& company.Contains(e.CO.Substring(0, 2))
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
select new Employee
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY == null ? DateTime.MaxValue : e.CO_SENIORITY,
BirthDate = e.BIRTH_DT,
SortByDate = GetSortDate(e.PREV_CO, e.CO_SENIORITY),
PreviousCo = e.PREV_CO
}).OrderBy(o => o.SortByDate).ThenBy(o => o.SeniorityDate).ThenBy(o => o.BirthDate).ToList();
}
Anyone have a suggestion on how I can convert this OrderBy?
UPDATED QUESTION
At the moment I have my query working correctly by using a secondary SELECT like #Markus showed. The first query just pulls the data and then all the formatting and calling of a method to get the correct SortByDate.
However, my manager would really prefer to do the sorting in the DB versus in memory. He let this one go because there are very few people calling this seniority list and only once a month.
For learning purposes I would like to see if I could get the DB to do all the sorting as #IvanStoev shows below. So, going back that route I’m not able to get the OrderBy to work exactly like it should.
If you look at the original SQL I’m trying to convert it first looks to see if the person had a previous company of “ABC” and if they do then look at the SeniorityDate (set a default date if that’s NULL) and compare it to an acquisition date. If that condition isn’t met then just use their SeniorityDate (set it’s default if NULL). Tricky….I know.
Using the suggested OrderBy in LinqPad and then looking at the returned SQL I can see that the first part of the OrderBy looks at the previous company and then the SeniorityDate and sets a value. Then it looks at the acquisition date. I need to somehow group some conditions to look at first which I don’t know it that’s possible.
SELECT t0.ABV_POSITION_TITLE, t0.BIRTH_DT, t0.CITY_CODE, t0.CO, t0.CO_SENIORITY, t0.EMP, t0.FIRST_NAME, t0.LAST_NAME, t0.PART_TIME_IND, t0.PREV_CO, t0.WORKSTAT
FROM SAP_EMPLOYEE t0
WHERE ((((t0.WORKSTAT IN (:p0, :p1) AND (t0.PERS_SUB_AREA = :p2)) AND SUBSTR(t0.CO, 0 + 1, 2) IN (:p3, :p4)) AND (t0.CO <> :p5)) AND ((t0.EVENT_TYP <> :p6) OR ((t0.EVENT_TYP = :p6) AND NOT t0.EVENT_RSN IN (:p7, :p8, :p9))))
ORDER BY (CASE WHEN ((t0.PREV_CO = :p10) AND (t0.CO_SENIORITY IS NULL)) THEN :p11 WHEN (t0.CO_SENIORITY < :p12) THEN :p13 ELSE COALESCE(t0.CO_SENIORITY, :p11) END), COALESCE(t0.CO_SENIORITY, :p11), t0.BIRTH_DT
-- p0 = [1]
-- p1 = [3]
-- p2 = [200A]
-- p3 = [EX]
-- p4 = [SM]
-- p5 = [EXGS]
-- p6 = [35]
-- p7 = [21]
-- p8 = [22]
-- p9 = [23]
-- p10 = [ABC]
-- p11 = [1/1/2099 12:00:00 AM]
-- p12 = [4/10/2001 12:00:00 AM]
-- p13 = [4/1/2001 12:00:00 AM]
I need to come up with something like
ORDER BY (CASE WHEN ((t0.PREV_CO = :p10) AND (COALESCE(t0.CO_SENIORITY, :p11) < :p12) THEN :p13 ELSE COALESCE(t0.CO_SENIORITY, :p11) END)
Here is the code I used in LinqPad.
void Main()
{
var workStatus = new string[] { "1", "3" };
var company = new string[] { "EX", "SM" };
var eventReason = new string[] { "21", "22", "23" };
var baseDate = new DateTime(2001, 4, 10); // 10-apr-2001
var minDate = new DateTime(2001, 4, 1); // 1-apr-2001
var abcDate = new DateTime(2001, 4, 10); // 10-apr-2001
var maxDate = new DateTime(2099, 1, 1); // 1-jan-2099
var data = (from e in SAP_EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& e.PERS_SUB_AREA == "200A"
&& company.Contains(e.CO.Substring(0, 2))
&& e.CO != "EXGS"
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
orderby e.PREV_CO == "ABC" && e.CO_SENIORITY == null ? maxDate : e.CO_SENIORITY < abcDate ? minDate : e.CO_SENIORITY ?? maxDate,
e.CO_SENIORITY ?? maxDate,
e.BIRTH_DT
select new Employee
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY == null ? maxDate :
e.PREV_CO == "ABC" && e.CO_SENIORITY < twaDate ? maxDate : e.CO_SENIORITY,
LOA = e.WORKSTAT == "1" ? "LOA" : "",
ABC = e.PREV_CO == "ABC" ? "ABC" : "",
BirthDate = e.BIRTH_DT,
PreviousCo = e.PREV_CO
}).ToList();
data.Dump();
}
The reason for the exception is that entity framework generates the SQL query when you execute it. In your case, this happens with the call to ToList() at the end. In order to generate the SQL query, entity framework analyzes the query and transforms it into SQL. As entity framework does not know your function, it cannot generate the SQL statements for it.
In order to solve this, you need to first execute the query and do the sort operation in memory on the results. In order to limit the amount of data that is transferred to the client, you should execute the query including the where clause and also tell EF which fields you are interested in to avoid a SELECT * FROM ... that includes all fields of the table.
You could change your query approximately as follows:
data = (from e in db.EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& company.Contains(e.CO.Substring(0, 2))
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
select new ()
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY,
BirthDate = e.BIRTH_DT,
PreviousCo = e.PREV_CO
}).ToList().Select(x => new Employee()
{
Co = x.Co,
CityCode = x.CityCode,
EmployeeNumber = x.EmployeeNumber,
LastName = x.LastName,
FirstName = x.FirstName,
Position = x.Position,
EmploymentType = x.EmploymentType,
SeniorityDate = x.SeniorityDate ?? DateTime.MaxValue,
BirthDate = x.BirthDate,
SortByDate = GetSortDate(x.PreviousCo, x.SeniorityDate),
PreviousCo = x.PreviousCo
}).OrderBy(o => o.SortByDate)
.ThenBy(o => o.SeniorityDate)
.ThenBy(o => o.BirthDate).ToList();
This query first filters the data as specified in the where clause and then uses an anonymous type to retrieve only the relevant fields - including the ones that are later used as an input to the GetSortDate method with its original values. After the first ToList the results are present in memory and you can first add a new select that creates the Employee objects including the sort date. These objects are then ordered by sort date and so on.
A small hint for the GetSortDate method: specifying DateTime constants as a string that is parsed is not a good idea as parsing is dependent on the culture of the thread (if no culture is specified).
// Culture dependent
sortDate = System.DateTime.Parse("2001-04-01");
// Better
sortDate = new DateTime(2001, 04, 01);
As you already noticed (the hard way), in LINQ to Entities query you cannot use local methods like in LINQ to Objects. If you want the whole query to be executed in the database, you need to embed the logic inside the query using only the supported constructs.
With that being said, the equivalent of your SQL query should be something like this
var baseDate = new DateTime(2001, 4, 10); // 10-apr-2001
var minDate = new DateTime(2001, 4, 1); // 1-apr-2001
var maxDate = new DateTime(2099, 1, 1); // 1-jan-2099
data = (from e in db.EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& company.Contains(e.CO.Substring(0, 2))
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
let seniorityDate = e.CO_SENIORITY ?? maxDate
let sortDate =
e.CO_SENIORITY == null ? maxDate :
e.PREV_CO == "ABC" && e.CO_SENIORITY < baseDate ? minDate :
e.CO_SENIORITY
orderby sortDate, seniorityDate, e.BIRTH_DT
select new Employee
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY,
BirthDate = e.BIRTH_DT,
PreviousCo = e.PREV_CO
}).ToList();
Update: For learning purposes I've updated the answer with using let clauses.
Now regarding the concrete ordering. I could have written the "SortDate" part exactly the way you did it, but I believe my way is a better equivalent. Why?
Here is my "SortDate" interpretation in pseudo code
if (CoSeniorityDate == null)
SortDate = #2099-01-01#
else if (PreviousCo == "ABC" && CoSeniorityDate < #2001-04-10#)
SortDate = #2001-04-01#
else
SortDate = CoSeniorityDate
And here is your function
if (CoSeniorityDate == null) CoSeniorityDate = #2099-01-01#
if (PreviousCo == "ABC" && CoSeniorityDate < #2001-04-10#)
SortDate = #2001-04-01#
else
SortDate = CoSeniorityDate
Let CoSeniorityDate == null. Then, according to your logic, let substitute CoSeniorityDate = #2099-01-01#:
if (PreviousCo == "ABC" && #2099-01-01# < #2001-04-10#)
SortDate = #2001-04-01#
else
SortDate = #2099-01-01#
Since #2099-01-01# < #2001-04-10# is always false, it becomes simple
SortDate = #2099-01-01#
i.e. exactly like the first part of my criteria. In the else part we already know CoSeniorityDate is not null and can just check the other conditions.
Anyway, doing it your way would be like this
let sortDate = e.PREV_CO == "ABC" && seniorityDate < baseDate ? minDate : seniorityDate
I'm still learning LINQ and have a task where I need to group Booking objects by four properties and then by weekly intervals depending on the input timerange.
public class Booking
{
public string Group { get; set; }
public BookingType Type { get; set; }
public BookingStatus Status { get; set; }
public decimal Price { get; set; }
public bool Notification { get; set; }
public DateTime Date { get; set; }
}
Let's say we have the following Bookings:
IList<Booking> Bookings = new List<Booking>
{
new Booking{Group = "Group1", Type = BookingType.Online, Status = BookingStatus.New, Price = 150, Date = new DateTime(2012,06,01)},
new Booking{Group = "Group1", Type = BookingType.Online, Status = BookingStatus.New, Price = 100, Date = new DateTime(2012,06,02)},
new Booking{Group = "Group1", Type = BookingType.Online, Status = BookingStatus.New, Price = 200, Date = new DateTime(2012,06,03)},
new Booking{Group = "Group2", Type = BookingType.Phone, Status = BookingStatus.Accepted, Price = 80, Date = new DateTime(2012,06,10)},
new Booking{Group = "Group2", Type = BookingType.Phone, Status = BookingStatus.Accepted, Price = 110, Date = new DateTime(2012,06,12)},
new Booking{Group = "Group3", Type = BookingType.Store, Status = BookingStatus.Accepted, Price = 225, Date = new DateTime(2012,06,20)},
new Booking{Group = "Group3", Type = BookingType.Store, Status = BookingStatus.Invoiced, Price = 300, Date = new DateTime(2012,06,21)},
new Booking{Group = "Group3", Type = BookingType.Store, Status = BookingStatus.Invoiced, Price = 140, Date = new DateTime(2012,06,22)},
};
That would result in the following lines on the final printout:
Week 22 Week 23 Week 24 Week 25 Week 26
May28-Jun3 Jun4-10 Jun11-17 Jun18-24 Jun25-Jul1
Group Type Status Cnt. Price Cnt. Price Cnt. Price Cnt. Price Cnt. Price
----------------------------------------------------------------------------------------------
Group1 Online New 3 450 0 0 0 0 0 0 0 0
Group2 Phone Accepted 0 0 1 80 1 110 0 0 0 0
Group3 Store Accepted 0 0 0 0 0 0 1 225 0 0
Group3 Store Invoiced 0 0 0 0 0 0 2 440 0 0
I have created 2 additional classes to represent a line with the possibility to include any number of weeks:
public class GroupedLine
{
public string Group { get; set; }
public BookingType Type { get; set; }
public BookingStatus Status { get; set; }
public List<WeeklyStat> WeeklyStats { get; set; }
}
public class WeeklyStat
{
public DateTime WeekStart { get; set; }
public decimal Sum { get; set; }
public int Count { get; set; }
}
If I have the following time period:
var DateFrom = new DateTime(2012, 05, 28);
var DateTo = new DateTime(2012, 7, 01);
Firstly, I need to identify what weeks are necessary in the statistics: in this case week 22-26.
For that I have the following code:
var DateFrom = new DateTime(2012, 05, 28);
var DateTo = new DateTime(2012, 7, 01);
var firstWeek = GetFirstDateOfWeek(DateFrom, DayOfWeek.Monday);
IList<DateTime> weeks = new List<DateTime> { firstWeek };
while(weeks.OrderByDescending(w => w).FirstOrDefault().AddDays(7) <= DateTo)
{
weeks.Add(weeks.OrderByDescending(w => w).FirstOrDefault().AddDays(7));
}
And now, I'd need some LINQ magic to do the grouping both by the 4 properties and the aggregation (count of bookings and sum of prices) for the weeks.
I can attach code sample of the LINQ I got so far tomorrow, as I don't have access to it now.
Sorry for the long post, hope it's clear what I mean.
Edit: 2012-11-07
I have to modify the question a bit, so that the grouped weeks only include those weeks, that actually have data.
Updated example output:
May28-Jun3 Jun4-10 Jun18-24
Group Type Status Cnt. Price Cnt. Price Cnt. Price
---------------------------------------------------------------
Group1 Online New 3 450 0 0 0 0
Group2 Phone Accepted 0 0 1 80 0 0
Group3 Store Accepted 0 0 0 0 1 225
Group3 Store Invoiced 0 0 0 0 2 440
In this case there were no Bookings in period Jun 11-17 and Jun25-Jul1 so they are omitted from the results.
This query will get all data
var query = from b in Bookings
where b.Date >= dateFrom && b.Date <= dateTo
group b by new { b.Group, b.Type, b.Status } into g
select new GroupedLine()
{
Group = g.Key.Group,
Type = g.Key.Type,
Status = g.Key.Status,
WeeklyStats = (from b in g
let startOfWeek = GetFirstDateOfWeek(b.Date)
group b by startOfWeek into weekGroup
orderby weekGroup.Key
select new WeeklyStat()
{
WeekStart = weekGroup.Key,
Count = weekGroup.Count(),
Sum = weekGroup.Sum(x => x.Price)
}).ToList()
};
I leave UI output to you :)
This will also return WeekStats for all weeks (with 0 values, if we do not have booking groups on some week):
// sequence contains start dates of all weeks
var weeks = Bookings.Select(b => GetFirstDateOfWeek(b.Date))
.Distinct().OrderBy(date => date);
var query = from b in Bookings
group b by new { b.Group, b.Type, b.Status } into bookingGroup
select new GroupedLine()
{
Group = bookingGroup.Key.Group,
Type = bookingGroup.Key.Type,
Status = bookingGroup.Key.Status,
WeeklyStats = (from w in weeks
join bg in bookingGroup
on w equals GetFirstDateOfWeek(bg.Date) into weekGroup
orderby w
select new WeeklyStat()
{
WeekStart = w,
Count = weekGroup.Count(),
Sum = weekGroup.Sum(b => b.Price)
}).ToList()
};
Keep in mind, that if you need date filter (from, to), then you need to apply it both to weeks query and bookings query.
var query = Bookings.GroupBy(book => new GroupedLine()
{
Group = book.Group,
Type = book.Type,
Status = book.Status
})
.Select(group => new
{
Line = group.Key,
Dates = group.GroupBy(book => GetWeekOfYear(book.Date))
.Select(innerGroup => new
{
Week = innerGroup.Key,
Count = innerGroup.Count(),
TotalPrice = innerGroup.Sum(book => book.Price)
})
});
public static int GetWeekOfYear(DateTime date)
{
return date.DayOfYear % 7;
}
This variant generates "empty" spots for weeks with no data:
// I group by week number, it seemed clearer to me , but you can change it
var firstWeek = cal.GetWeekOfYear(DateFrom, dfi.CalendarWeekRule, dfi.FirstDayOfWeek);
var lastWeek = cal.GetWeekOfYear(DateTo, dfi.CalendarWeekRule, dfi.FirstDayOfWeek);
var q = from b in Bookings
group b by new { b.Group, b.Type, b.Status }
into g
select new
{
Group = g.Key,
Weeks =
from x in g
select new
{
Week = cal.GetWeekOfYear(x.Date,
dfi.CalendarWeekRule,
dfi.FirstDayOfWeek),
Item = x
}
} into gw
from w in Enumerable.Range(firstWeek, lastWeek-firstWeek+1)
select new
{
gw.Group,
Week = w,
Item = from we in gw.Weeks
where we.Week == w
group we by we.Item.Group into p
select new
{
p.Key,
Sum = p.Sum (x => x.Item.Price),
Count = p.Count()
}
};