I have a List<TimeAndCode>
of objects. Every object contains a Code
and a TimeSpan
value.
public struct TimeAndCode {
public TimeSpan Time { get; set; }
public string Code { get; set; }
}
The occuring codes are pre defined as "CO"
, "GO"
and "BT"
.
When the Time
property hits the same value, the order of the list needs to be in a certan format. e.g. (JSON representation for readability)
[
{"Time" : "08:00:00", "Code" : "CO" },
{"Time" : "09:00:00", "Code" : "GO" },
{"Time" : "09:30:00", "Code" : "CO" },
{"Time" : "09:30:00", "Code" : "GO" },
{"Time" : "09:30:00", "Code" : "CO" },
{"Time" : "09:30:00", "Code" : "GO" },
{"Time" : "09:30:00", "Code" : "BT" },
{"Time" : "12:30:00", "Code" : "CO" },
{"Time" : "12:30:00", "Code" : "GO" },
{"Time" : "13:30:00", "Code" : "CO" },
{"Time" : "13:30:00", "Code" : "GO" },
{"Time" : "13:30:00", "Code" : "BT" },
{"Time" : "13:30:00", "Code" : "CO" },
{"Time" : "13:30:00", "Code" : "GO" }
]
So the pattern is either CO -> GO
or GO -> BT
or CO -> GO -> BT -> CO -> GO
.
There might be a good old if else
solution, but I'm looking for a nice and easy to use LINQ solution. (e.g. list.OrderBy( x => x.Time ).RearrangeBy( x => x.Code, pattern );
)
EDIT
The custom ordering must only take place, when the codes are in the same time slot.
The easiest pattern is:
CO -> GO -> CO -> GO -> CO -> GO
But there is also a possibility that a BT can be present:
CO -> GO -> BT -> CO -> GO -> BT -> CO -> GO -> BT -> CO
But there is also a possibility that there is only one BT:
CO -> GO -> BT -> GO -> GO -> CO -> GO -> CO -> GO
CO -> GO -> CO -> GO -> BT -> CO -> GO -> CO -> GO
CO -> GO -> CO -> GO -> CO -> GO -> BT -> CO -> GO
CO -> GO -> BT -> CO -> GO -> CO -> GO -> BT -> CO -> GO
So tha major pattern is CO -> GO -> BT
.
EDIT 2 Solved
I created an extension method. May be this code is useful for somebody. (room for improvement)
public static IEnumerable<TSource> RearrangeByPattern<TSource, TKey>(this IEnumerable<TSource> list, Func<TSource, TKey> keySelector, IEnumerable<TKey> pattern)
{
var groups = list.GroupBy(keySelector).OrderBy(x => pattern.IndexOf(x.Key));
var maxOccurences = groups.Select(x => x.Count()).Max();
var result = new List<TSource>();
for (var i = 0; i < maxOccurences; i )
{
foreach (var group in groups)
{
if (group.Count() > i)
{
result.Add(group.ElementAt(i));
}
}
}
return result;
}
Now I can use it like this:
var list = new List<TimeAndCode>();
... // add values
var ordered = list.OrderBy(x => x.Date).RearrangeByPattern( x => x.Code, new string[] { "CO", "GO", "BT" });
CodePudding user response:
Here is a suggestion where TimeAndCode.Code
is an enum
rather than a string
.
It will always order entries with indentical timespans in a repeated CO
, GO
, BT
pattern; meaning that if e.g. five entries with identical timespans has the following Code
selection: 2 x CO
, 1 x BT
, 2 x GO
, it will always order them as CO
, GO
, BT
, CO
, GO
(as opposed to CO
, GO
, CO
, GO
, BT
).
I achieve this by generating an OrderBy
property based on the timespan, an index (generated inside a nested group) and the numerical Code
value for each entry.
Using the following types:
public struct TimeAndCode
{
public TimeSpan Time { get; set; }
public Code Code { get; set; }
}
public enum Code
{
Undefined,
CO,
GO,
BT
}
we can write the following expression:
List<TimeAndCode> result = list
.GroupBy(entry => entry.Code)
.SelectMany(gr => gr
.GroupBy(entry => entry.Time)
.SelectMany(gr => gr.Select((entry, index) => (
OrderBy: entry.Time.ToString() index (int)entry.Code,
TimeAndCode: entry))))
.OrderBy(entry => entry.OrderBy)
.Select(entry => entry.TimeAndCode)
.ToList();
where list
is a List<TimeAndCode>
.
Using example input as follows:
List<TimeAndCode> list = new List<TimeAndCode>
{
new TimeAndCode { Time = new TimeSpan(09, 00, 00), Code = Code.GO },
new TimeAndCode { Time = new TimeSpan(13, 30, 00), Code = Code.BT },
new TimeAndCode { Time = new TimeSpan(09, 30, 00), Code = Code.GO },
new TimeAndCode { Time = new TimeSpan(09, 30, 00), Code = Code.GO },
new TimeAndCode { Time = new TimeSpan(13, 30, 00), Code = Code.GO },
new TimeAndCode { Time = new TimeSpan(08, 00, 00), Code = Code.CO },
new TimeAndCode { Time = new TimeSpan(09, 30, 00), Code = Code.BT },
new TimeAndCode { Time = new TimeSpan(12, 30, 00), Code = Code.CO },
new TimeAndCode { Time = new TimeSpan(09, 30, 00), Code = Code.CO },
new TimeAndCode { Time = new TimeSpan(12, 30, 00), Code = Code.GO },
new TimeAndCode { Time = new TimeSpan(13, 30, 00), Code = Code.CO },
new TimeAndCode { Time = new TimeSpan(13, 30, 00), Code = Code.CO },
new TimeAndCode { Time = new TimeSpan(09, 30, 00), Code = Code.CO },
new TimeAndCode { Time = new TimeSpan(13, 30, 00), Code = Code.GO },
};
, after applying the Linq expression, we can then print the result
foreach (var entry in result)
{
Console.WriteLine("Time: " entry.Time " Code: " entry.Code);
}
and get the following output:
Time: 08:00:00 Code: CO
Time: 09:00:00 Code: GO
Time: 09:30:00 Code: CO
Time: 09:30:00 Code: GO
Time: 09:30:00 Code: BT
Time: 09:30:00 Code: CO
Time: 09:30:00 Code: GO
Time: 12:30:00 Code: CO
Time: 12:30:00 Code: GO
Time: 13:30:00 Code: CO
Time: 13:30:00 Code: GO
Time: 13:30:00 Code: BT
Time: 13:30:00 Code: CO
Time: 13:30:00 Code: GO
CodePudding user response:
It's a variation of Astrid's:
var r = list.GroupBy(tc => tc)
.SelectMany(g => g.Select((tc, i) => (tc, i)))
.OrderBy(t => (t.tc.Time, t.i, t.tc.Code))
.Select(t => t.tc);
With the following precursor setup:
public record TimeAndCode(TimeSpan Time, Code Code);
public enum Code { CO, GO, BT }
...
var list = new List<TimeAndCode>
{
new (TimeSpan.FromHours(8), Code.CO),
new (TimeSpan.FromHours(9), Code.GO),
new (TimeSpan.FromHours(9.5), Code.CO),
new (TimeSpan.FromHours(9.5), Code.GO),
new (TimeSpan.FromHours(9.5), Code.GO),
new (TimeSpan.FromHours(9.5), Code.CO),
new (TimeSpan.FromHours(9.5), Code.BT),
new (TimeSpan.FromHours(12.5), Code.CO),
new (TimeSpan.FromHours(12.5), Code.GO),
new (TimeSpan.FromHours(13.5), Code.CO),
new (TimeSpan.FromHours(13.5), Code.BT),
new (TimeSpan.FromHours(13.5), Code.GO),
new (TimeSpan.FromHours(13.5), Code.CO),
new (TimeSpan.FromHours(13.5), Code.GO),
};
TimeAndCode is a record, which means it gains some useful properties for sorting and comparing. It can be simply grouped because it is automatically equal to another TimeAndCode with the same data
Grouping by the time and code results in a list-of-lists; the two 9:30 CO
s go in a list. Passing this to Select((tc,i)
means that i
is 0 for the first and 1 for the second etc.. All we do with this is promote it to a tuple of the tc
and the i
because we'll need them later, and we SelectMany to undo the GroupBy that allowed us to count the same Time/Code
We OrderBy another tuple; tuples sort by their values in order, so to order by 3 things a, b, and c, we can OrderBy a tuple of (a, b, c)
And all that is left to do at the end is select the tc
back to make a list of TimeAndCode, sorted something like what you want
Like Astrid's it also doesn't put the BT at the end for 9:30 - it goes in the middle, like 13:30's does.. But you haven't provided an explanation for why 9:30's BT is at the end...