Запрос Linq для объединения перекрывающихся элементов

#c# #linq

#c# #linq

Вопрос:

У меня есть этот класс, представляющий период времени:

 public class Period
{
    public Period(DateTime dateFrom)
    {
        DateFrom = dateFrom;
    }

    public Period(DateTime dateFrom, DateTime? dateTo)
    {
        DateFrom = dateFrom;
        DateTo = dateTo;
    }

    public Period() { }
    public DateTime DateFrom { get; set; }
    public DateTime? DateTo { get; set; }


    public bool IsOverlapping(Period other)
    {
        if (!DateTo.HasValue)
        {
            return DateFrom.Value <= other.DateTo.Value;
        }
        
        if (!other.DateTo.HasValue)
        {
            return other.DateFrom.Value <= DateTo.Value;
        }

        return DateFrom.Value <= other.DateTo.Value amp;amp; other.DateFrom.Value <= DateTo.Value;
    }

    public bool IsFinite => DateTo.HasValue;

    public bool IsInfinite => !IsFinite;

    // Equals/GetHashCode have been overrided
}
 

Теперь у меня есть список периодов, и для каждого из них я должен выполнить сетевой вызов, поэтому, чтобы свести их к минимуму, я решил объединить все перекрывающиеся периоды.

Итак, такой список :

  • 2020-01-01 -> 2020-01-10
  • 2020-02-05 -> 2020-02-10
  • 2020-02-07 -> 2020-02-15
  • 2020-02-13 -> 2020-02-20
  • 2020-03-01 -> 2020-03-10
  • 2020-03-25 -> 2020-03-31
  • 2020-03-30 ->

Должно стать :

  • 2020-01-01 -> 2020-01-10
  • 2020-02-05 -> 2020-02-20
  • 2020-03-01 -> 2020-03-10
  • 2020-03-25 ->

Я попробовал этот код

     periods.OrderBy(p => p.DateFrom.Value)
            .Aggregate(new List<Period>(), (ps, p) =>
            {
                if (!ps.Any())
                {
                    ps.Add(p);
                    return ps;
                }

                var last = ps.Last();
                if (last.IsOverlapping(p))
                {
                    if (last.IsInfinite || p.IsInfinite)
                    {
                        ps[ps.Count() - 1] = new Period(DateTimeHelpers.Min(last.DateFrom.Value, p.DateFrom.Value), null);
                    }
                    else
                    {
                        ps[ps.Count() - 1] = new Period(DateTimeHelpers.Min(last.DateFrom.Value, p.DateFrom.Value), DateTimeHelpers.Max(last.DateTo.Value, p.DateTo.Value));
                    }

                    return ps;
                }

                ps.Add(p);
                return ps;

            });
 

Он работает правильно, но я им не доволен, поэтому мне интересно, есть ли более эффективный / элегантный / читаемый способ сделать это?

Я говорю не просто о рефакторинге для извлечения некоторых методов, а о принципиально другом решении, возможно, я пропустил полезный оператор LINQ.

Вот мой тест, если вы хотите его воспроизвести (MSTest FluentAssertions). Период не в правильном порядке, чтобы гарантировать, что он будет обработан самим методом:

         // Arrange
        var periods = new List<Period>()
        {
            new Period(new DateTime(2020, 2, 13), new DateTime(2020, 2, 20)),
            new Period(new DateTime(2020, 3, 1), new DateTime(2020, 3, 10)),
            new Period(new DateTime(2020, 3, 25), new DateTime(2020, 3, 31)),
            new Period(new DateTime(2020, 3, 30)),
            new Period(new DateTime(2020, 1, 1), new DateTime(2020, 1, 10)),
            new Period(new DateTime(2020, 2, 5), new DateTime(2020, 2, 10)),
            new Period(new DateTime(2020, 2, 7), new DateTime(2020, 2, 15))
        };

        // Act
        var mergedPeriods = Implementation(periods);

        // Assert
        mergedPeriods.Should().HaveCount(4);
        mergedPeriods[0].Should().Be(new Period(new DateTime(2020, 1, 1), new DateTime(2020, 1, 10)));
        mergedPeriods[1].Should().Be(new Period(new DateTime(2020, 2, 5), new DateTime(2020, 2, 20)));
        mergedPeriods[2].Should().Be(new Period(new DateTime(2020, 3, 1), new DateTime(2020, 3, 10)));
        mergedPeriods[3].Should().Be(new Period(new DateTime(2020, 3, 25)));
 

Комментарии:

1. Может быть, лучше спросить о codereview.stackexchange.com . Я не вижу ничего особенно LINQ-y в этом коде, хотя агрегация пустых списков аккуратна. Более короткая версия: pastebin.com/cSyhSBkX

2. Я знаю замечательные библиотеки, которые делают это с легкостью. Перекрытие, конкатенация, пересечение и т.д. Если вас заинтересует, я подготовлю образец.

3. @SvyatoslavDanyliv Если у вас есть ссылка на библиотеку или ее документацию, это было бы полезно 🙂

4. Хорошая библиотека: github.com/rsdn/CodeJam , ключом к поиску является CompositeRange и Merge , другие доступные операции вы можете найти самостоятельно. Создать CompositeRange с перечислением диапазонов и Merge . Это обычная задача при работе с такими структурами, поэтому рождается эта часть библиотеки.