How to group by in LINQ? - c#

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.

Related

Linq group by date range

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

How to get duplicates by attribute with LINQ

I have my class HomeTeam
public class HomeTeam
{
public ID { get; set; }
public string codeTeam { get; set; }
public string codeNumber { get;set; }
}
I have my list of objects:
var listaFiltrada = new List<HomeTeam>();
I want get all items that have the same codeTeam and codeNumber parameters.
Sample:
ID CodeTeam CodeNumber
1 B538 B2235
ID CodeTeam CodeNumber
2 B538 B2235
ID CodeTeam CodeNumber
3 B333 B235
I want filter list return Item with ID 1 and 2.
i suppose that the 2 linq expressions could be combined in one but i think this works
var teams = new List<HomeTeam>()
{
new HomeTeam() {ID = 1, codeTeam = "B538", codeNumber = "B2235"},
new HomeTeam() {ID = 2, codeTeam = "B538", codeNumber = "B2235"},
new HomeTeam() {ID = 3, codeTeam = "B333", codeNumber = "B235"},
new HomeTeam() {ID = 4, codeTeam = "B333", codeNumber = "B235"},
new HomeTeam() {ID = 5, codeTeam = "B333", codeNumber = "B235"},
new HomeTeam() {ID = 6, codeTeam = "B333", codeNumber = "B235333"},
new HomeTeam() {ID = 7, codeTeam = "B3444433", codeNumber = "B23534433"}
};
var groupedData =
(from t in teams
group t by new {t.codeTeam, t.codeNumber}
into grp
where grp.Count() > 1
select new
{
grp.Key.codeTeam,
grp.Key.codeNumber,
Count = grp.Count()
}).ToList();
var ids = from team in teams
join grp in groupedData on new {team.codeNumber, team.codeTeam} equals new
{grp.codeNumber, grp.codeTeam}
select new
{
ID = team.ID
};
foreach (var id in ids)
{
Console.WriteLine(id.ID);
}
this gives back a list with lists of duplicate items
var dups = listaFiltrada.Select(list => new HomeTeam()
{
ID = list.ID,
CodeTeam = list.CodeTeam,
CodeNumber = list.CodeNumber
}).GroupBy(x => new { x.CodeNumber, x.CodeTeam })
.Where(x => x.Count() > 1).ToArray();

LINQ - Sum Based on Overlapping Dates

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

Grouping a get items between an timerange

I have this EntityFramework DatabaseContext given:
class Worker
{
int Id,
string Name,
...
ICollection<Shift> Shifts
}
class Shift
{
int Id,
DateTime ShiftStart
DateTime ShiftEnd
...
Worker Worker
}
class CustomerAction
{
int Id,
Worker Worker,
DateTime ArrivalTime,
int ActionType
...
string Comment
}
Now i want to group all Workers with their Shifts and then get all CustomerActions with ActionType 2 or 4.
Sometimes the Workers dont add their Shifts then all other CustomerActions done by the Worker should listed with empty shift informations.
The Output should look like this:
List =
{
new Item
{
WorkerId = 1, WorkerName = "Worker1" ...
ShiftId = 1, ShiftStart = 10/1/2017 1:00:00 AM, ShiftEnd = 10/1/2017 5:00:00 AM ...
// Sorted by ArrivalTime, only in between the ShiftStart / ShiftEnd Range, done by this Worker
CustomerActions =
{
new CustomerAction { ActionId = 1, ActionType = 4, ArrivalTime = 10/1/2017 1:00:00 AM, Comment = "My comment" }
new CustomerAction { ActionId = 2, ActionType = 2, ArrivalTime = 10/1/2017 1:30:00 AM, Comment = "Some other comment" }
new CustomerAction { ActionId = 3, ActionType = 4, ArrivalTime = 10/1/2017 2:00:00 AM, Comment = "No comment" }
}
}
new Item
{
WorkerId = 2, WorkerName = "Worker2" ...
ShiftId = null, ShiftStart = null, ShiftEnd = null ...
// Sorted by ArrivalTime, done by this Worker, Without an Shift
CustomerActions =
{
new CustomerAction { ActionId = 4, ActionType = 4, ArrivalTime = 10/2/2017 1:00:00 AM, Comment = "..." }
new CustomerAction { ActionId = 5, ActionType = 2, ArrivalTime = 10/3/2017 1:30:00 AM, Comment = "..." }
new CustomerAction { ActionId = 6, ActionType = 4, ArrivalTime = 10/4/2017 2:00:00 AM, Comment = "..." }
}
}
}
If I understood your question correctly, you don't need grouping. Instead, you need to expand, for each workers shift the actions worked upon. The code below should point you to the right direction:
var registeredShifts = dbContext.Workers
.SelectMany(w => w.Shifts.Select(s => new
{
WorkerId = w.Id,
WorkerName = w.Name,
ShiftId = s.Id,
ShiftStart = s.ShiftStart,
ShiftEnd = s.ShiftEnd,
CustomerActions = dbContext.CustomerActions
.Where(a => a.Worker.Id == w.Id &&
a.ArrivalTime >= s.ShiftStart &&
a.ArrivalTime <= s.ShiftEnd &&
(a.ActionType == 2 || a.ActionType == 4))
.ToList()
}))
EDIT: To the same results for actions outside the registered shifts you'll have to use grouping.
var outsideShifts = dbContrxt.CustomerActions
.Where(a => a.ActionType == 2 || a.ActionType == 4)
.Where(a => a.Worker.Shifts.All(s => a.ArrivalTime < s.ShiftStart ||
a.ArrivalTime > s.ShiftEnd))
.GroupBy(a => new
{
WorkerId = a.Worker.Id,
WorkerName = a.Worker.Name
})
.Select(g => new
{
WorkerId = g.Key.WorkerId,
WorkerName = g.Key.WorkerName,
ShiftId = null,
ShiftStart = null,
ShiftEnd = null,
CustomerActions = g.ToList()
});
Finally, to get the required data, Union() the results above:
var result = registeredShifts.Union(outsideShifts);
return result.ToArray();

Grouping troubles in LINQ

I have some list of structures like this:
struct va_data
{
public int item_id;
public int type_id;
public double item_value;
public DateTime value_date;
}
I trying group the list by type_id and take items where value_date is maximum then group by item_id and take items only where item_value is minimum
There is my syntax
from x in dataList
group x by x.type_id into grouped
select grouped.Where(x => x.value_date == grouped.Max(y => y.value_date))
.GroupBy(x => x.item_id) // and here i was stuck.
Example
var dataList = new []
{
new va_data {item_id = 1, type_id = 1, item_value = 0, value_date = "2013.07.29"},
new va_data {item_id = 1, type_id = 1, item_value = 1, value_date = "2013.07.30"},
new va_data {item_id = 2, type_id = 1, item_value = 0, value_date = "2013.07.29"},
new va_data {item_id = 2, type_id = 1, item_value = 1, value_date = "2013.07.29"},
new va_data {item_id = 4, type_id = 2, item_value = 5, value_date = "2013.07.29"},
new va_data {item_id = 4, type_id = 3, item_value = 9, value_date = "2013.07.30"},
};
The result must be
var dataListResult = new []
{
new va_data {item_id = 1, type_id = 1, item_value = 1, value_date = "2013.07.30"},
new va_data {item_id = 2, type_id = 1, item_value = 0, value_date = "2013.07.29"},
new va_data {item_id = 4, type_id = 2, item_value = 5, value_date = "2013.07.29"},
}
Given the following class
class va_data
{
public int item_id;
public int type_id;
public double item_value;
public DateTime value_date;
}
and your example data, you can use a query like this:
from data in dataList
group data by new {data.item_id, data.type_id} into g
let max_value_date = g.Max(x => x.value_date)
from i in g.Where(x => x.value_date == max_value_date)
group i by i.item_id into g2
let min_item_value = g2.Min(x => x.item_value)
from x in g2
where x.item_value == min_item_value
select x;
to get the following result:
Just split your query into two parts - getting latest of each type, and then getting minimal of each item:
var latestOfEachType =
from d in dataList
group d by d.type_id into typeGroup
select typeGroup.OrderByDescending(x => x.value_date).First();
var result = from d in latestOfEachType
group d by d.item_id into itemGroup
select itemGroup.OrderBy(x => x.item_value).First();
This query will be executed as single query. But in this case it looks much more readable to me. Also don't use mutable structs!. Use classes instead.
EDIT: Thus you have several items for max date, then query needs two small tweaks - select all items where date is max, and use SelectMany to iterate over them:
var latestOfEachType =
from d in dataList
group d by d.type_id into typeGroup
let maxDate = typeGroup.Max(x => x.value_date)
select typeGroup.Where(x => x.value_date == maxDate);
var result = from d in latestOfEachType.SelectMany(g => g)
group d by d.item_id into itemGroup
select itemGroup.OrderBy(x => x.item_value).First();

Categories

Resources