Home > Software engineering >  C# rearrange List by pattern
C# rearrange List by pattern

Time:02-15

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 COs 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...

  • Related