I've got an IQueryable(Of Job)
where Job has, amongst other things:
Property CreatedOn as DateTime
Property JobType as JobTypes
Enum JobTypes
JobType1
JobType2
JobType3
End Enum
What I want to get out of it is a list, ordered by CreatedOn
, then Grouped by JobType
with a count
Eg Say i've got (abbreviated dates)
11:00 JobType1
11:01 JobType2
11:02 JobType2
11:03 JobType2
11:04 JobType2
11:05 JobType3
11:06 JobType1
11:07 JobType1
I want
JobType1 1
JobType2 4
JobType3 1
JobType1 2
I don't know how to take ordering into account when grouping. can someone point me at the right way to do this? By preference, I'd prefer fluent Syntax. VB.Net or C# is fine.
This trick is fairly easy to train LinqToObjects to do:
public static IEnumerable<IGrouping<TKey, TSource>> GroupContiguous<TKey, TSource>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
bool firstIteration = true;
MyCustomGroupImplementation<TKey, TSource> currentGroup = null;
foreach (TSource item in source)
{
TKey key = keySelector(item);
if (firstIteration)
{
currentGroup = new MyCustomGroupImplementation<TKey, TSource>();
currentGroup.Key = key;
firstIteration = false;
}
else if (!key.Equals(currentGroup.Key))
{
yield return currentGroup;
currentGroup = new MyCustomGroupImplementation<TKey, TSource>();
currentGroup.Key = key;
}
currentGroup.Add(item);
}
if (currentGroup != null)
{
yield return currentGroup;
}
}
public class MyCustomGroupImplementation<TKey, TSource> : IGrouping<TKey, TSource>
{
//TODO implement IGrouping and Add
}
Used by
IEnumerable<IGrouping<JobType, Job> query = Jobs
.OrderBy(j => j.CreatedOn)
.GroupContiguous(j => j.JobType);
It's not so easy to do a "look at the previous row" with just any old linq provider. I hope you don't have to teach LinqToSql or LinqToEntities how to do this.
Here's a reasonable approach that uses the Aggregrate
method.
If you start with a list of JobTypes
like this:
var jobTypes = new []
{
JobTypes.JobType1,
JobTypes.JobType2,
JobTypes.JobType2,
JobTypes.JobType2,
JobTypes.JobType2,
JobTypes.JobType3,
JobTypes.JobType1,
JobTypes.JobType1,
};
You can use Aggregate
by first defining the accumulator like so:
var accumulator = new List<KeyValuePair<JobTypes, int>>()
{
new KeyValuePair<JobTypes, int>(jobTypes.First(), 0),
};
Then the Aggregate
method call looks like this:
var results = jobTypes.Aggregate(accumulator, (a, x) =>
{
if (a.Last().Key == x)
{
a[a.Count - 1] =
new KeyValuePair<JobTypes, int>(x, a.Last().Value + 1);
}
else
{
a.Add(new KeyValuePair<JobTypes, int>(x, 1));
}
return a;
});
And finally calling this give you this result:
Simple, sort of...
This updated version uses a subroutine to do the same thing as before, but doesn't need the extra internal field. (I have kept my earlier version, which, to avoid using a Zip
routine, needed the extra OrDer
field.)
Option Explicit On
Option Strict On
Option Infer On
Imports so11310237.JobTypes
Module so11310237
Enum JobTypes
JobType1
JobType2
JobType3
End Enum
Sub Main()
Dim data = {New With{.CO=#11:00#, .JT=JobType1, .OD=0},
New With{.CO=#11:03#, .JT=JobType2, .OD=0},
New With{.CO=#11:05#, .JT=JobType3, .OD=0},
New With{.CO=#11:02#, .JT=JobType2, .OD=0},
New With{.CO=#11:06#, .JT=JobType1, .OD=0},
New With{.CO=#11:01#, .JT=JobType2, .OD=0},
New With{.CO=#11:04#, .JT=JobType2, .OD=0},
New With{.CO=#11:07#, .JT=JobType1, .OD=0}}
' Check that there's any data to process
If Not data.Any Then Exit Sub
' Both versions include a normal ordering first.
Dim odata = From q In data Order By q.CO
' First version here (and variables reused below):
Dim ljt = odata.First.JT
Dim c = 0
For Each o In odata
If ljt <> o.JT Then
ljt = o.JT
c += 1
End If
o.OD = c
Next
For Each p In From q In data Group By r=q.JT, d=q.OD Into Count()
Console.WriteLine(p)
Next
Console.WriteLine()
' New version from here:
' Reset variables (still needed :-()
ljt = odata.First.JT
c = 0
For Each p In From q In odata Group By r=q.JT, d=IncIfNotEqual(c,q.JT,ljt) Into Count()
Console.WriteLine(p)
Next
End Sub
Function IncIfNotEqual(Of T)(ByRef c As Integer, ByVal Value As T, ByRef Cmp As T) As Integer
If Not Object.Equals(Value, Cmp) Then
Cmp = Value
c += 1
End If
Return c
End Function
End Module
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With