Related
I have collection that I need to group if the parent key is common AND if the date field is within n (e.g. 2) hours of each other.
Sample data:
List<DummyObj> models = new List<DummyObj>()
{
new DummyObj { ParentKey = 1, ChildKey = 1, TheDate = DateTime.Parse("01/01/2020 00:00:00"), Name = "Single item - not grouped" },
new DummyObj { ParentKey = 2, ChildKey = 2, TheDate = DateTime.Parse("01/01/2020 01:00:00"), Name = "Should be grouped with line below" },
new DummyObj { ParentKey = 2, ChildKey = 3, TheDate = DateTime.Parse("01/01/2020 02:00:00"), Name = "Grouped with above" },
new DummyObj { ParentKey = 2, ChildKey = 4, TheDate = DateTime.Parse("01/01/2020 04:00:00"), Name = "Separate item as greater than 2 hours" },
new DummyObj { ParentKey = 2, ChildKey = 5, TheDate = DateTime.Parse("01/01/2020 05:00:00"), Name = "Grouped with above" },
new DummyObj { ParentKey = 3, ChildKey = 6, TheDate = DateTime.Parse("01/01/2020 05:00:00"), Name = "Single item - not grouped" }
};
private class DummyObj
{
public int ParentKey { set; get; }
public int ChildKey { set; get; }
public DateTime TheDate { set; get; }
public string Name { set; get; }
}
The resulting grouping should be (child keys):
{[1]}, {[2,3]}, {[4,5]}, {[6]}
I could group by parent key first then loop through comparing the individual items within the groups but hoping for a more elegant solution.
As always, thank you very much.
public static void Test()
{
var list = GetListFromDb(); //returns List<DummyObj>;
var sortedList = new List<DummyObj>();
foreach(var g in list.GroupBy(x => x.ParentKey))
{
if(g.Count() < 2)
{
sortedList.Add(g.First());
}
else
{
var datesInGroup = g.Select(x => x.TheDate);
var hoursDiff = (datesInGroup.Max() - datesInGroup.Min()).TotalHours;
if(hoursDiff <= 2)
{
string combinedName = string.Join("; ", g.Select(x => x.Name));
g.First().Name = combinedName;
sortedList.Add(g.First());
}
else
{
//now it's the mess
DateTime earliest = g.Select(x => x.TheDate).Min();
var subGroup = new List<DummyObj>();
foreach(var line in g)
{
if((line.TheDate - earliest).TotalHours > 2)
{
//add the current subgroup entry to the sorted group
subGroup.First().Name = string.Join("; ", subGroup.Select(x => x.Name));
sortedList.Add(subGroup.First());
//new group needed and new earliest date to start the group
sortedList = new List<DummyObj>();
sortedList.Add(line);
earliest = line.TheDate;
}
else
{
subGroup.Add(line);
}
}
//add final sub group, i.e. when there's none that are over 2 hours apart or the last sub group
if(subGroup.Count > 1)
{
subGroup.First().Name = string.Join("; ", subGroup.Select(x => x.Name));
sortedList.Add(subGroup.First());
}
else if(subGroup.Count == 1)
{
sortedList.Add(subGroup.First());
}
}
}
}
}
Here you go:
List<DummyObj> models = new List<DummyObj>()
{
new DummyObj { ParentKey = 1, ChildKey = 1, TheDate = DateTime.Parse("01/01/2020 00:00:00"), Name = "Single item - not grouped" },
new DummyObj { ParentKey = 2, ChildKey = 2, TheDate = DateTime.Parse("01/01/2020 01:00:00"), Name = "Should be grouped with line below" },
new DummyObj { ParentKey = 2, ChildKey = 3, TheDate = DateTime.Parse("01/01/2020 02:00:00"), Name = "Grouped with above" },
new DummyObj { ParentKey = 2, ChildKey = 4, TheDate = DateTime.Parse("01/01/2020 04:00:00"), Name = "Separate item as greater than 2 hours" },
new DummyObj { ParentKey = 2, ChildKey = 5, TheDate = DateTime.Parse("01/01/2020 05:00:00"), Name = "Grouped with above" },
new DummyObj { ParentKey = 3, ChildKey = 6, TheDate = DateTime.Parse("01/01/2020 05:00:00"), Name = "Single item - not grouped" }
};
List<List<DummyObj>> groups =
models
.GroupBy(x => x.ParentKey)
.Select(xs => xs.OrderBy(x => x.TheDate).ToList())
.SelectMany(xs => xs.Skip(1).Aggregate(new[] { xs.Take(1).ToList() }.ToList(), (a, x) =>
{
if (x.TheDate.Subtract(a.Last().Last().TheDate).TotalHours < 2.0)
{
a.Last().Add(x);
}
else
{
a.Add(new [] { x }.ToList());
}
return a;
}))
.ToList();
string output =
String.Join(", ",
groups.Select(x =>
$"{{[{String.Join(",", x.Select(y => $"{y.ChildKey}"))}]}}"));
That gives me:
{[1]}, {[2,3]}, {[4,5]}, {[6]}
Using C# LINQ, I would like to be able to turn the following:
[
{
Id: 1,
StartDate: '2021-03-10',
EndDate: '2021-03-21',
Quantity: 1
},
{
Id: 2,
StartDate: '2021-03-10',
EndDate: '2021-03-21',
Quantity: 1
},
{
Id: 3,
StartDate: '2021-03-10',
EndDate: '2021-03-23',
Quantity: 2
},
{
Id: 4,
StartDate: '2021-03-10',
EndDate: '2021-03-25',
Quantity: 1
}
]
Into this:
[
{
StartDate: '2021-03-10',
EndDate: '2021-03-21',
Quantity: 5,
Ids: [1, 2, 3, 4]
},
{
StartDate: '2021-03-22',
EndDate: '2021-03-23',
Quantity: 3,
Ids: [3, 4]
}, {
StartDate: '2021-03-24',
EndDate: '2021-03-25',
Quantity: 1,
Ids: [4]
}
]
In this scenario:
EndDate may change per entry on the input but StartDate will always be the same.
It is possible that two entries may have the same EndDate. In this case, the results will aggregate, with the results showing one entry for those dates and a summed quantity.
Desired solution:
The LINQ would group by unique date range, sum the quantity value and include an array of ids, indicating which date ranges have been covered.
Help is much appreciated.
This question is a step in the right direction but doesn't take care of indicating the date range as demonstrated.
Select multiple fields group by and sum
So given this class:
public class Entry
{
public int Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Quantity { get; set; }
}
And this starting point:
Entry[] entries = JsonConvert.DeserializeObject<Entry[]>(jsonText);
I first get a list of the distinct dates involved:
DateTime[] dates =
entries
.SelectMany(x => new [] { x.StartDate, x.EndDate })
.Distinct()
.OrderBy(x => x)
.ToArray();
Now I can query to split each Entry into the set of distinct date ranges:
var query =
from e in entries
let splitters =
dates
.Where(x => x >= e.StartDate)
.Where(x => x <= e.EndDate)
.ToArray()
from s in
splitters
.Skip(1)
.Zip(
splitters,
(s1, s0) => new Entry()
{
Id = e.Id,
StartDate = s0,
EndDate = s1,
Quantity = e.Quantity
})
group new { s.Id, s.Quantity } by new { s.StartDate, s.EndDate } into gss
select new
{
gss.Key.StartDate,
gss.Key.EndDate,
Quantity = gss.Sum(gs => gs.Quantity),
Ids = gss.Select(gs => gs.Id).ToArray(),
};
That gives me:
Or:
[
{
"StartDate": "2021-03-10T00:00:00",
"EndDate": "2021-03-21T00:00:00",
"Quantity": 5,
"Ids": [
1,
2,
3,
4
]
},
{
"StartDate": "2021-03-21T00:00:00",
"EndDate": "2021-03-23T00:00:00",
"Quantity": 3,
"Ids": [
3,
4
]
},
{
"StartDate": "2021-03-23T00:00:00",
"EndDate": "2021-03-25T00:00:00",
"Quantity": 1,
"Ids": [
4
]
}
]
If I'm allowed to use Microsoft's "System.Interactive" library then it can be done like this:
var query =
from e in entries
from s in
dates
.Where(x => x >= e.StartDate)
.Where(x => x <= e.EndDate)
.Buffer(2, 1)
.SkipLast(1)
.Select(x => new Entry()
{
Id = e.Id,
StartDate = x[0],
EndDate = x[1],
Quantity = e.Quantity
})
group new { s.Id, s.Quantity } by new { s.StartDate, s.EndDate } into gss
select new
{
gss.Key.StartDate,
gss.Key.EndDate,
Quantity = gss.Sum(gs => gs.Quantity),
Ids = gss.Select(gs => gs.Id).ToArray(),
};
These are a couple of LINQ extension methods that are based on the APL scan operator, extended. scan is like Aggregate, but returns the intermediate results. These are a variation that take pairs of items and then processes them.
public static class IEnumerableExt {
// TRes firstResFn(T firstValue)
// TRes combineFn(T PrevValue, T CurValue)
// returns firstResFn(items.First()) then ScanByPairs(items, combineFn)
public static IEnumerable<TRes> ScanByPairs<T, TRes>(this IEnumerable<T> items, Func<T, TRes> firstResFn, Func<T, T, TRes> combineFn) {
using (var itemsEnum = items.GetEnumerator())
if (itemsEnum.MoveNext()) {
var prev = itemsEnum.Current;
yield return firstResFn(prev);
while (itemsEnum.MoveNext())
yield return combineFn(prev, prev = itemsEnum.Current);
}
}
// THelper helperSeedFn(T FirstValue)
// TRes resSeedFn(T FirstValue)
// (THelper Helper, TRes Res) combineFn((THelper Helper, TRes PrevRes), T CurValue)
// returns resSeedFn, combineFn,...
public static IEnumerable<TRes> ScanToPairsWithHelper<T, THelper, TRes>(this IEnumerable<T> items, Func<T, THelper> helperSeedFn, Func<T, TRes> resSeedFn, Func<(THelper Helper, TRes PrevRes), T, (THelper Helper, TRes Res)> combineFn) {
using (var itemsEnum = items.GetEnumerator())
if (itemsEnum.MoveNext()) {
var seed = (Helper: helperSeedFn(itemsEnum.Current), Res: resSeedFn(itemsEnum.Current));
while (itemsEnum.MoveNext()) {
yield return seed.Res;
seed = combineFn(seed, itemsEnum.Current);
}
yield return seed.Res;
}
}
}
First, create an intermediate list that combines any identical date ranges:
var int1 = src.GroupBy(s => s.EndDate)
.Select(sg => new {
Ids = sg.Select(s => s.Id).ToList(),
StartDate = sg.First().StartDate,
EndDate = sg.Key,
Quantity = sg.Sum(s => s.Quantity)
});
Then use ScanByPairs to compute the new start dates so ranges don't overlap (this assumes EndDate is in increasing order):
var int2 = int1.ScanByPairs(s => new { s.Ids, s.Quantity, s.StartDate, s.EndDate },
(prev, next) => new { next.Ids, next.Quantity, StartDate = prev.EndDate.AddDays(1), next.EndDate });
Finally, process the previous result in reverse, aggregating the quantities and IDs as you go:
var ans = int2.Reverse()
.ScanToPairsWithHelper(first => new { first.Ids, first.Quantity },
first => new { first.Ids, first.Quantity, first.StartDate, first.EndDate },
(helpernext, prev) => (new { Ids = helpernext.Helper.Ids.Concat(prev.Ids).ToList(), Quantity = helpernext.Helper.Quantity + prev.Quantity },
new { Ids = helpernext.Helper.Ids.Concat(prev.Ids).ToList(), Quantity = helpernext.Helper.Quantity + prev.Quantity, prev.StartDate, prev.EndDate }))
.Reverse();
This is rather tough to do with LINQ. I recommend using MoreLINQ for that.
// these helpers make things much easier
using static MoreLinq.Extensions.GroupAdjacentExtension;
using static MoreLinq.Extensions.ScanRightExtension;
using static MoreLinq.Extensions.WindowRightExtension;
// ...
var output = input
// aggregate identical end dates
.GroupAdjacent(x => x.EndDate)
.Select(xs => xs.Aggregate(
new { Ids = Enumerable.Empty<int>(), Quantity = 0, StartDate = default(DateTime), EndDate = default(DateTime) },
(acc, curr) => new {
Ids = acc.Ids.Append(curr.Id),
Quantity = acc.Quantity + curr.Quantity,
curr.StartDate,
curr.EndDate
}))
// cut off overlapping start dates
.WindowRight(2)
.Select(win => win.Count == 1
? win[0]
: new {
win[1].Ids,
win[1].Quantity,
StartDate = win[0].EndDate.AddDays(1),
win[1].EndDate
})
// accumulate IDs and quantities
.ScanRight((curr, prev) => new {
Ids = curr.Ids.Concat(prev.Ids),
Quantity = curr.Quantity + prev.Quantity,
curr.StartDate,
curr.EndDate
});
This doesn't iterate multiple times and efficiently streams everything. But its a lot of code.
Working example: https://dotnetfiddle.net/YxDQEX
The main trick is to iterate the input backwards. This let's us easily accumulate the IDs and quantities, because they always grow when scanning the list from back to front. E.g. the second-to-last item's quantity will be its own quantity plus the quantity of the last item; the third-to-last item's quantity will be its own plus the quantity of the second-to-last item; etc.
ScanRight() does the backwards iteration for us. It is similar to Aggregate(), but it yields the intermediate values of the accumulate as well, thus returning an IEnumerable<TAccumulate> rather than a single TAccumulate.
It might be possible to get a cleaner version of this using a traditional foreach loop, but I kind of doubt it. With a foreach you would have to do everything at the same time (i.e. in the same loop body) in order to do it efficiently and that will probably become unreadable as well (considering all the state that has to be managed).
With the LINQ version you at least have three disparate steps that you could extract using extension functions:
public static IEnumerable<T> MergeEqualEndDates<T>(this IEnumerable<T> xs) => xs
.GroupAdjacent(x => x.EndDate)
.Select(xs => xs.Aggregate(
new { Ids = Enumerable.Empty<int>(), Quantity = 0, StartDate = default(DateTime), EndDate = default(DateTime) },
(acc, curr) => new {
Ids = acc.Ids.Append(curr.Id),
Quantity = acc.Quantity + curr.Quantity,
curr.StartDate,
curr.EndDate
}));
Which could leave you with:
var output = input
.MergeEqualEndDates()
.TrimStartDates()
.AccumulateIdsAndQuantities();
Thank you to #GoodNightNerdPride and #NetMage for their help.
This is the version I came up with. I cheated and used a for-loop. It is less elegant but I do find it easier to read.
Below is the main extention method (which borrows from #NetMage) and beneath that is an Xunit file I used to test the whole thing.
Extention Method
public static List<Response> SumBasedUponOverlappingDates(this List<Request> requests) {
var requestsWithGroupedIds = requests
.GroupBy(x => x.EndDate)
.Select(x => new {
Ids = x.Select(y => y.Id).ToArray(),
StartDate = x.First().StartDate,
EndDate = x.Key,
Quantity = x.Sum(y => y.Quantity)
})
.OrderBy(x => x.EndDate)
.ToList();
var responses = new List<Response>();
for (int i = 0; i < requestsWithGroupedIds.Count(); i++) {
var req = requestsWithGroupedIds[i];
var overlappingEntries = requestsWithGroupedIds
.Where(x => x.StartDate <= req.StartDate && x.EndDate >= req.EndDate)
.ToList();
var resp = new Response {
Ids = overlappingEntries.SelectMany(x => x.Ids.Select(y => y)).OrderBy(x => x).ToArray(),
Quantity = overlappingEntries.Sum(x => x.Quantity),
StartDate = (i == 0) ? req.StartDate : requestsWithGroupedIds[i - 1].EndDate.AddDays(1),
EndDate = req.EndDate
};
responses.Add(resp);
}
return responses;
}
XUnit Code
using System;
using System.Linq;
using Xunit;
using System.Collections.Generic;
namespace StackOverflow.Tests {
public class GroupingAndAggregationTests {
[Fact]
void ShouldAggregateDuplicateDataIntoSingle() {
var requests = new List<Request>() {
new Request{Id = 9, StartDate = new DateTime(2021, 2, 20), EndDate = new DateTime(2021,3, 5), Quantity = 6 },
new Request{Id = 345, StartDate = new DateTime(2021, 2, 20), EndDate = new DateTime(2021,3, 5), Quantity = 29 }
};
var responses = requests.SumBasedUponOverlappingDates();
Assert.Equal(1, responses.Count());
var expectedResponse =
new Response() {
StartDate = new DateTime(2021, 2, 20),
EndDate = new DateTime(2021, 3, 5),
Quantity = 35,
Ids = new int[] { 9, 345 }
};
var actualResponse = responses[0];
Assert.True(actualResponse.IsEqual(expectedResponse));
}
[Fact]
void ShouldAggregateMultipleBasedOnOverlappingDates() {
var requests = new List<Request>() {
new Request{Id = 1, StartDate = new DateTime(2021,3,10), EndDate = new DateTime(2021,3, 21), Quantity = 1 },
new Request{Id = 2, StartDate = new DateTime(2021,3,10), EndDate = new DateTime(2021,3, 21), Quantity = 1 },
new Request{Id = 3, StartDate = new DateTime(2021,3,10), EndDate = new DateTime(2021,3, 23), Quantity = 2 },
new Request{Id = 4, StartDate = new DateTime(2021,3,10), EndDate = new DateTime(2021,3, 25), Quantity = 1 }
};
var responses = requests.SumBasedUponOverlappingDates();
Assert.Equal(3, responses.Count());
var expecedResponse1 =
new Response() {
StartDate = new DateTime(2021, 3, 10),
EndDate = new DateTime(2021, 3, 21),
Quantity = 5,
Ids = new int[] { 1, 2, 3, 4 }
};
var actualResponse1 = responses[0];
Assert.True(actualResponse1.IsEqual(expecedResponse1));
var expectedResponse2 =
new Response() {
StartDate = new DateTime(2021, 3, 22),
EndDate = new DateTime(2021, 3, 23),
Quantity = 3,
Ids = new int[] { 3, 4 }
};
var actualResponse2 = responses[1];
Assert.True(actualResponse2.IsEqual(expectedResponse2));
var expectedResponse3 =
new Response() {
StartDate = new DateTime(2021, 3, 24),
EndDate = new DateTime(2021, 3, 25),
Quantity = 1,
Ids = new int[] { 4 }
};
var actualResponse3 = responses[2];
Assert.True(actualResponse3.IsEqual(expectedResponse3));
}
}
public class Request {
public int Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Quantity { get; set; }
}
public class Response {
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Quantity { get; set; }
public int[] Ids { get; set; }
public bool IsEqual(Response resp)
=>
StartDate == resp.StartDate &&
EndDate == resp.EndDate &&
Quantity == resp.Quantity &&
Ids.OrderBy(x => x).SequenceEqual(resp.Ids.OrderBy(x => x));
}
public static class ExtentionMethods {
public static List<Response> SumBasedUponOverlappingDates(this List<Request> requests) {
var requestsWithGroupedIds = requests
.GroupBy(x => x.EndDate)
.Select(x => new {
Ids = x.Select(y => y.Id).ToArray(),
StartDate = x.First().StartDate,
EndDate = x.Key,
Quantity = x.Sum(y => y.Quantity)
})
.OrderBy(x => x.EndDate)
.ToList();
var responses = new List<Response>();
for (int i = 0; i < requestsWithGroupedIds.Count(); i++) {
var req = requestsWithGroupedIds[i];
var overlappingEntries = requestsWithGroupedIds
.Where(x => x.StartDate <= req.StartDate && x.EndDate >= req.EndDate)
.ToList();
var resp = new Response {
Ids = overlappingEntries.SelectMany(x => x.Ids.Select(y => y)).OrderBy(x => x).ToArray(),
Quantity = overlappingEntries.Sum(x => x.Quantity),
StartDate = (i == 0) ? req.StartDate : requestsWithGroupedIds[i - 1].EndDate.AddDays(1),
EndDate = req.EndDate
};
responses.Add(resp);
}
return responses;
}
}
}
In my aspnet core 3.1 application I am using CQRS pattern. I have bulling summary table which is calculating in every hour (Hangifre background job) some price
Example data looks like:
Price - 10, StartDate - 2020/08/02 10:00 , EndDate - 2020/08/03 11:00
Price - 10, StartDate - 2020/08/02 11:00 , EndDate - 2020/08/03 12:00
Price - 10, StartDate - 2020/08/02 13:00 , EndDate - 2020/08/03 14:00
Price - 10, StartDate - 2020/08/02 14:00 , EndDate - 2020/08/03 15:00
Price - 10, StartDate - 2020/08/02 15:00 , EndDate - 2020/08/03 16:00
Price - 10, StartDate - 2020/08/02 16:00 , EndDate - 2020/08/03 17:00
I would like achive something like:
if I specify periodDuration=3h
Price - 30, StartDate - 2020/08/02 10:00 , EndDate - 2020/08/03 13:00
Price - 30, StartDate - 2020/08/02 13:00 , EndDate - 2020/08/03 16:00
Price - 30, StartDate - 2020/08/02 19:00 , EndDate - 2020/08/03 22:00
My method looks like:
var billing = _context.BillingSummaries.AsQueryable();
switch (request.SortBy)
{
case "createdAt" when request.SortDirection == "asc":
billing = billing.OrderBy(x => x.BeginApply);
break;
case "createdAt" when request.SortDirection == "desc":
billing = billing.OrderByDescending(x => x.BeginApply);
break;
}
if (request.StartDate.HasValue)
{
billing = billing.Where(x =>
x.BeginApply >= request.StartDate);
}
if (request.EndDate.HasValue)
{
billing = billing.Where(x =>
x.EndApply <= request.EndDate);
}
// Want to achieve this
billing = request.PeriodDuration switch
{
"3h" => "calculate 3 hours range",
"6h" => "calculate 6 hours range",
"12h" => "calculate 12 hours range",
"d" => "calculate daily range",
"w" => "calculate weekly range",
"m" => "calculate monthly range",
_ => billing
};
var billings = await billing.Skip(request.Offset ?? 0).Take(request.Limit ?? 50)
.ToListAsync(cancellationToken: cancellationToken);
if (billings == null)
throw new NotFoundException("Not found");
return new BillingInfo
{
Data = _mapper.Map<List<BillingSummary>, List<BillingSummaryDto>>(billings),
TotalCount = await billing.CountAsync(cancellationToken: cancellationToken),
AllPrice = await billing.SumAsync(x => x.Price, cancellationToken:
cancellationToken),
Currency = currency.ToString("G")
};
Suppose raw data always span one hour each.
billing = request.PeriodDuration switch
{
"3h" => (
from b in billing
group b by new Hourly { b.StartDate.Year, b.StartDate.Month, b.StartDate.Day, Hour = b.StartDate.Hour / 3 } into groups
from bs in groups
select new
{
Price = bs.Sum(b => b.Price),
StartDate = bs.Min(p => p.StartDate),
EndDate = bs.Max(p => p.EndDate)
}
),
"6h" => (
from b in billing
group b by new Hourly { b.StartDate.Year, b.StartDate.Month, b.StartDate.Day, Hour = b.StartDate.Hour / 6 } into groups
from bs in groups
select new
{
Price = bs.Sum(b => b.Price),
StartDate = bs.Min(p => p.StartDate),
EndDate = bs.Max(p => p.EndDate)
}
),
"12h" => (
from b in billing
group b by new Hourly { b.StartDate.Year, b.StartDate.Month, b.StartDate.Day, Hour = b.StartDate.Hour / 12 } into groups
from bs in groups
select new
{
Price = bs.Sum(b => b.Price),
StartDate = bs.Min(p => p.StartDate),
EndDate = bs.Max(p => p.EndDate)
}
),
"d" => (
from b in billing
group b by new Daily { b.StartDate.Year, b.StartDate.Month, b.StartDate.Day } into groups
from bs in groups
select new
{
Price = bs.Sum(b => b.Price),
StartDate = bs.Min(p => p.StartDate),
EndDate = bs.Max(p => p.EndDate)
}
),
"w" => (
from b in billing
group b by new { b.StartDate.Year, Week = SqlFunctions.DatePart("week", b.StartDate) } into groups
from bs in groups
select new
{
Price = bs.Sum(b => b.Price),
StartDate = bs.Min(p => p.StartDate),
EndDate = bs.Max(p => p.EndDate)
}
),
"m" => (
from b in billing
group b by new { b.StartDate.Year, b.StartDate.Month } into groups
from bs in groups
select new
{
Price = bs.Sum(b => b.Price),
StartDate = bs.Min(p => p.StartDate),
EndDate = bs.Max(p => p.EndDate)
}
),
_ => billing
};
first group your data by date and after try it
public class Billingdata
{
public int Price { get; set; }
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
public class GroupingData {
public int Start { get; set; }
public int End { get; set; }
}
var timerangelist = new List<GroupingData> {
new GroupingData{ Start = 10, End=13 },
new GroupingData{ Start = 13, End=16 },
new GroupingData{ Start = 16, End=19 },
new GroupingData{ Start = 19, End=22 },
};
var result = new List<Billingdata>();
var billings = new List<Billingdata> {
new Billingdata{Price = 10, Start = new DateTime(2020,08,02,10,00,00), End = new DateTime(2020,08,02,11,00,00) },
new Billingdata{Price = 10, Start = new DateTime(2020,08,02,11,00,00), End = new DateTime(2020,08,02,12,00,00) },
new Billingdata{Price = 10, Start = new DateTime(2020,08,02,12,00,00), End = new DateTime(2020,08,02,13,00,00) },
new Billingdata{Price = 10, Start = new DateTime(2020,08,02,13,00,00), End = new DateTime(2020,08,02,14,00,00) },
new Billingdata{Price = 10, Start = new DateTime(2020,08,02,14,00,00), End = new DateTime(2020,08,02,15,00,00) },
new Billingdata{Price = 10, Start = new DateTime(2020,08,02,15,00,00), End = new DateTime(2020,08,02,16,00,00) },
new Billingdata{Price = 10, Start = new DateTime(2020,08,02,16,00,00), End = new DateTime(2020,08,02,17,00,00) }
};
foreach (var item in timerangelist)
{
var data = billings.Where(w => item.End >= w.End.Hour && w.Start.Hour >= item.Start).ToList();
var p = data.Sum(s => s.Price);
var ss = data.FirstOrDefault(f => f.Start.Hour == item.Start)?.Start;
var e = data.FirstOrDefault(f => f.End.Hour == item.End)?.End;
if (ss == null || e == null) { continue; }
result.Add(
new Billingdata { Price = p,
Start = (DateTime)ss,
End = (DateTime)e,
}
);
}
enter code here
I need to return the last 30 days of a speciefic user daily appointments and check if the user made at least 8 hours of appointments for each day.
in sql i can do that with this command:
select IDAppointment,IDUser, SUM(DurationInHours) from Note where AppointmentDate > *lastmonth and IDUser = #userID group by IDUser,IDAppointment,AppointmentDate
and after that i get the result and validate the DurationInHours(double type).
Is it possible to do it using LINQ?
Get the list of the last month user appointments and validate it day by day
Thanks!
This should be roughly there although this is off the top of my head as not at an IDE.
var result = context.Notes
.Where(n => [Your where clause])
.GroupBy(n => new { n.IDUser, n.IDAppointment, n.AppointmentDate})
.Select(g => new {
g.Key.IDAppointment,
g.Key.IDUser,
g.Sum(n => n.DurationInHours)});
UPDATE:
For reference your where clause will be something like this... (again off the top of my head)
DateTime lastMonth = DateTime.Today.AddMonths(-1);
int userId = 1 // TODO: FIX
var result = context.Notes.Where(n => n.AppointmentDate > lastMonth
&& n.IDUser = userId)
Resulting in....
DateTime lastMonth = DateTime.Today.AddMonths(-1);
int userId = 1 // TODO: FIX
var result = context.Notes
.Where(n => n.AppointmentDate > lastMonth
&& n.IDUser = userId)
.GroupBy(n => new { n.IDUser, n.IDAppointment, n.AppointmentDate})
.Select(g => new {
g.Key.IDAppointment,
g.Key.IDUser,
g.Sum(n => n.DurationInHours)});
Here is a solution which I tested.
DateTime lastMonth = DateTime.Today.AddMonths(-1);
int selectedUserId = 2;
var notes = new List<Note>(
new Note[] {
new Note() {
AppointmentDate = new DateTime(2013,7,30){},
IDAppointment = 1, IDUser = 1, DurationInHours = 1
},
new Note() {
AppointmentDate = new DateTime(2013,7,30){},
IDAppointment = 1, IDUser = 1, DurationInHours = 2
},
new Note() {
AppointmentDate = new DateTime(2013,7,30){},
IDAppointment = 1, IDUser = 1, DurationInHours = 3
},
new Note() {
AppointmentDate = new DateTime(2013,7,28){},
IDAppointment = 2, IDUser = 2, DurationInHours = 2
},
new Note() {
AppointmentDate = new DateTime(2013,7,28){},
IDAppointment = 2, IDUser = 2, DurationInHours = 3
},
new Note() {
AppointmentDate = new DateTime(2013,7,27){},
IDAppointment = 2, IDUser = 2, DurationI nHours = 4
},
new Note() {
AppointmentDate = new DateTime(2013,7,26){},
IDAppointment = 3, IDUser = 3, DurationInHours = 3
},
new Note() {
AppointmentDate = new DateTime(2013,7,25){},
IDAppointment = 3, IDUser = 3, DurationInHours = 4
},
new Note() {
AppointmentDate = new DateTime(2013,7,24){},
IDAppointment = 3, IDUser = 3, DurationInHours = 5
}
}
);
var results = from n in notes
group n by new {n.IDUser, n.IDAppointment, n.AppointmentDate}
into g
where g.Key.AppointmentDate > lastMonth &&
g.Key.IDUser == selectedUserId
select new {
g.Key.IDAppointment,
g.Key.IDUser,
TotalHours = g.Sum(n => n.DurationInHours)
};
The summation property needed to be given a name explicitly (i.e. TotalHours) or else you get error CS0746: Invalid anonymous type member declarator. Anonymous type members must be declared with a member assignment, simple name or member access.
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()
}
};