Merge list elements if date differs by one day

Viewed 80

I have a list that looks like this. I need to merge this list elements if the dates are 1 days apart ("2022, 01, 01" and "2022, 01, 02), their rate, roomTypeId and Accessibility is exactly the same. The DateFrom and DateTo are ALWAYS the same in the list I am given, every Availability represents one day.

             new Availability
            {
                Accessibility = 1,
                RoomTypeId = "12345",
                Rate = 50,
                DateFrom = new DateTime(2022, 01, 01),
                DateTo = new DateTime(2022, 01, 01)
            },
                new Availability
            {
                Accessibility = 1,
                RoomTypeId = "12345",
                Rate = 50,
                DateFrom = new DateTime(2022, 01, 02),
                DateTo = new DateTime(2022, 01, 02)
            },
                new Availability
            {
                Accessibility = 1,
                RoomTypeId = "12345",
                Rate = 100,
                DateFrom = new DateTime(2022, 01, 04),
                DateTo = new DateTime(2022, 01, 04)
            },
                new Availability
            {
                Accessibility = 6,
                RoomTypeId = "12345",
                Rate = 100,
                DateFrom = new DateTime(2022, 01, 05),
                DateTo = new DateTime(2022, 01, 05)
            },
                new Availability
            {
                Accessibility = 6,
                RoomTypeId = "12345",
                Rate = 100,
                DateFrom = new DateTime(2022, 01, 6),
                DateTo = new DateTime(2022, 01, 06)
            },

The result should look like this:

             new Availability
            {
                Accessibility = 1,
                RoomTypeId = "12345",
                Rate = 50,
                DateFrom = new DateTime(2022, 01, 01),
                DateTo = new DateTime(2022, 01, 02)
            },
                new Availability
            {
                Accessibility = 1,
                RoomTypeId = "12345",
                Rate = 100,
                DateFrom = new DateTime(2022, 01, 04), <- missing (2002, 01, 03) so it should crate a new group
                DateTo = new DateTime(2022, 01, 04)
            },
                new Availability
            {
                Accessibility = 6, <- different
                RoomTypeId = "12345",
                Rate = 100,
                DateFrom = new DateTime(2022, 01, 05),
                DateTo = new DateTime(2022, 01, 06)
            }

How can I do this in the most optimal way? I tried grouping the list by RoomTypeId, sorting by date and comparing dates to previous elements, but it's not that yet.

2 Answers

That is somewhat dirty but can help! You can just skip elements that you're not interested in while keeping some flag element. And it's only one enumeration, so O(n) complexity

public static IEnumerable<Availability> GetMerged(List<Availability> oldList)
{
    var oldValue = oldList[0];
    foreach (var currentValue in oldList)
    {
        if (IsDifferent(oldValue, currentValue))
        {
            yield return oldValue;
            oldValue = currentValue;
        }
    }

    yield return oldValue;
}

public static bool IsDifferent(Availability x, Availability y)
{
    if (x.Accessibility != y.Accessibility || x.Rate != y.Rate || x.RoomTypeId != y.RoomTypeId)
        return true;
    if (Math.Abs((x.DateFrom - x.DateTo).TotalDays) > 1) 
        return true;

    return false;
}

You can also go by LINQ group by method and by overriding custom comparer, but you should pick right and custom GetHashCode function and i've found that difficult for now.

If you install the Nuget package MoreLINQ you can use the Segment() method for that:

var mergedAvails = avails
    .GroupBy(x => new { x.RoomTypeId, x.Rate, x.Accessibility })
    .SelectMany(groupedAvails => groupedAvails
        .OrderBy(avail => avail.DateFrom)
        .Segment((curr, prev, _) => prev.DateTo.AddDays(1) != curr.DateFrom)
        .Select(consecutiveAvails => new Availability {
            Accessibility = consecutiveAvails.First().Accessibility,
            RoomTypeId = consecutiveAvails.First().RoomTypeId,
            Rate = consecutiveAvails.First().Rate,
            DateFrom = consecutiveAvails.First().DateFrom,
            DateTo = consecutiveAvails.Last().DateTo
        }));

The general idea is that you first group all items by the properties you specified and then order the items in each group by date. Segement() will then split each group into sub-groups where each sub-group only contains items with consecutive days.

I didn't test it, but the code should also work for items that span multiple days instead of just one (as long as the date ranges don't overlap between different items).

By the way, I also work for the tourism industry and we do the same in some places of our C# software ^^

Working example: https://dotnetfiddle.net/XVkOZD

Related