LINQ GroupBy непрерывное время

Предполагая, что у меня есть простая структура, которая выглядит так:

public class Range
{
    public DateTime Start { get; set; }
    public DateTime End { get; set; }

    public Range(DateTime start, DateTime end)
    {
        this.Start = start;
        this.End = end;
    }
}

И я создаю такую ​​коллекцию:

var dr1 = new Range(new DateTime(2011, 11, 1, 12, 0, 0), 
    new DateTime(2011, 11, 1, 13, 0, 0));
var dr2 = new Range(new DateTime(2011, 11, 1, 13, 0, 0), 
    new DateTime(2011, 11, 1, 14, 0, 0));
var dr3 = new Range(new DateTime(2011, 11, 1, 14, 0, 0), 
    new DateTime(2011, 11, 1, 15, 0, 0));
var dr4 = new Range(new DateTime(2011, 11, 1, 16, 0, 0), 
    new DateTime(2011, 11, 1, 17, 0, 0));

var ranges = new List<Range>() { dr1, dr2, dr3, dr4 };

Что я хочу сделать, это группировать диапазоны, где они непрерывны, т.е. они непрерывны, если значение End предыдущего диапазона совпадает с началом следующего.

Мы можем предположить, что в значениях Диапазона нет коллизий/дубликатов или перекрытий.

В опубликованном примере я получаю две группы:

2011-11-1 12:00:00 - 2011-11-1 15:00:00

2011-11-1 16:00:00 - 2011-11-1 17:00:00

Для этого достаточно просто найти итеративное решение. Но есть ли какая-то магия LINQ, которую я могу использовать, чтобы получить это в довольно однострочном лайнере?

Ответ 1

Лучше всего использовать yield и метод расширения:

static IEnumerable<Range> GroupContinuous(this IEnumerable<Range> ranges)
{
    // Validate parameters.

    // Can order by start date, no overlaps, no collisions
    ranges = ranges.OrderBy(r => r.Start);

    // Get the enumerator.
    using (IEnumerator<Range> enumerator = ranges.GetEnumerator();
    {
        // Move to the first item, if nothing, break.
        if (!enumerator.MoveNext()) yield break;

        // Set the previous range.
        Range previous = enumerator.Current;

        // Cycle while there are more items.
        while (enumerator.MoveNext())
        {
            // Get the current item.
            Range current = enumerator.Current;

            // If the start date is equal to the end date
            // then merge with the previous and continue.
            if (current.Start == previous.End)
            {
                // Merge.
                previous = new Range(previous.Start, current.End);

                // Continue.
                continue;
            }

            // Yield the previous item.
            yield return previous;

            // The previous item is the current item.
            previous = current;
        }

        // Yield the previous item.
        yield return previous;
    }
}

Конечно, вызов OrderBy приведет к полной итерации последовательности ranges, но этого избежать не удастся. После того, как вы его приказали, вы можете предотвратить возможность материализовать свои результаты перед их возвратом; вы просто yield результаты, если условия диктуют.

Если вы знаете, что упорядочена последовательность, то вам вообще не нужно вызывать OrderBy, и вы можете yield элементы перемещаться по списку и разрывать разные свернутые экземпляры Range.

В конечном счете, если последовательность неупорядочена, у вас есть два варианта:

  • Закажите список, а затем обработайте (помните, OrderBy также откладывается, но ему нужно будет использовать одну полную итерацию для упорядочивания последовательности), используя yield, чтобы вернуть элемент, когда у вас есть его для обработки
  • Обработать всю последовательность сразу и вернуться как целая материализованная последовательность

Ответ 2

Общая версия метода расширения casperOne, используемая как таковая:

var items = new[]
    {
        // Range 1
        new { A = 0, B = 1 },
        new { A = 1, B = 2 },
        new { A = 2, B = 3 },
        new { A = 3, B = 4 },
        // Range 2
        new { A = 5, B = 6 },
        new { A = 6, B = 7 },
        new { A = 7, B = 8 },
        new { A = 8, B = 9 },
    };

var ranges = items.ContinousRange(
    x => x.A,
    x => x.B,
    (lower, upper) => new { A = lower, B = upper });

foreach(var range in ranges)
{
    Console.WriteLine("{0} - {1}", range.A, range.B);
}

Реализация метода расширения

    /// <summary>
    /// Calculates continues ranges based on individual elements lower and upper selections. Cannot compensate for overlapping.
    /// </summary>
    /// <typeparam name="T">The type containing a range</typeparam>
    /// <typeparam name="T1">The type of range values</typeparam>
    /// <param name="source">The ranges to be combined</param>
    /// <param name="lowerSelector">The range lower bound</param>
    /// <param name="upperSelector">The range upper bound</param>
    /// <param name="factory">A factory to create a new range</param>
    /// <returns>An enumeration of continuous ranges</returns>
    public static IEnumerable<T> ContinousRange<T, T1>(this IEnumerable<T> source, Func<T, T1> lowerSelector, Func<T, T1> upperSelector, Func<T1, T1, T> factory)
    {
        //Validate parameters

        // Can order by start date, no overlaps, no collisions
        source = source.OrderBy(lowerSelector);

        // Get enumerator
        using(var enumerator = source.GetEnumerator())
        {
            // Move to the first item, if nothing, break.
            if (!enumerator.MoveNext()) yield break;

            // Set the previous range.
            var previous = enumerator.Current;

            // Cycle while there are more items
            while(enumerator.MoveNext())
            {
                // Get the current item.
                var current = enumerator.Current;

                // If the start date is equal to the end date
                // then merge with the previoud and continue
                if (lowerSelector(current).Equals(upperSelector(previous)))
                {
                    // Merge
                    previous = factory(lowerSelector(previous), upperSelector(current));

                    // Continue
                    continue;
                }

                // Yield the previous item.
                yield return previous;

                // The previous item is the current item.
                previous = current;
            }

            // Yield the previous item.
            yield return previous;
        }
    }

Ответ 3

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

// build your set of continuous ranges for results
List<Range> continuousRanges = new List<Range>();

ranges.Aggregate(continuousRanges, (con, next) => {
{
    // pull last item (or default if none) - O(1) for List<T>
    var last = continuousRanges.FirstOrDefault(r => r.End == next.Start);

    if (last != null)
        last.End = next.End;
    else
        con.Add(next);

    return con;
});   

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

// build your set of continuous ranges for results
List<Range> continuousRanges = new List<Range>();

ranges.Aggregate(continuousRanges, (con, next) => {
{
    // pull last item (or default if none) - O(1) for List<T>
    var last = continuousRanges.LastOrDefault();

    if (last != null && last.End == next.Start)
        last.End = next.End;
    else
        con.Add(next);

    return con;
});   

Ответ 4

Вот еще одно решение LINQ. Он находит начало каждого непрерывного диапазона с одним запросом, концом каждого непрерывного диапазона с другим, а затем проходит через пары для создания новых диапазонов.

var starts = ranges.Where((r, i) => i == 0 || r.Start != ranges[i - 1].End);
var ends = ranges.Where((r, i) => i == ranges.Count - 1 || r.End != ranges[i + 1].Start);
var result = starts.Zip(ends, (s, e) => new Range(s.Start, e.End));

Он может быть переписан в однострочный, но отдельная версия более понятна и удобна в обслуживании:

var result = ranges.Where((r, i) => i == 0 || r.Start != ranges[i - 1].End).Zip(ranges.Where((r, i) => i == ranges.Count - 1 || r.End != ranges[i + 1].Start), (s, e) => new Range(s.Start, e.End));

Ответ 5

Следующие действия, но это действительно злоупотребление LINQ:

// Dummy at the end to get the last continues range
ranges.Add(new Range(default(DateTime), default(DateTime)));
// The previous element in the list
Range previous = ranges.FirstOrDefault();
// The start element of the current continuous range
Range start = previous;
ranges.Skip(1).Select(x => {var result = new {current = x, previous = previous};
                            previous = x; return result;})
              .Where(x => x.previous.End != x.current.Start)
              .Select(x => { var result = new Range(start.Start, x.previous.End);
                             start = x.current; return result; });

Код выполняет следующие действия:

Сначала выберите:

  • Сохраните текущее значение и текущее предыдущее значение в экземпляре нового анонимного типа
  • Установите предыдущее значение в текущее значение для следующей итерации

Где: выберите только те элементы, которые отмечают начало нового непрерывного диапазона

Второй выбор:

  • Создайте объект Range с датой начала сохраненного начального значения и конечным значением предыдущего элемента.
  • Задайте начальное значение для текущего элемента, так как он отмечает начало нового непрерывного диапазона.
  • Возвращает диапазон, созданный в (1)

Обратите внимание:
Я бы придерживался итеративного решения, потому что вышеприведенный код нечитабельный, неподдерживаемый, и мне потребовалось значительно больше времени, чем просто ввести цикл и a if...

Ответ 6

Ниже приведен код в синтаксисе понимания запроса.

public static List<Range> Group(List<Range> dates){
    if(dates.Any()){
        var previous = dates.FirstOrDefault();
        previous = new Range(previous.Start,previous.Start);
        var last = dates.Last();
        var result = (from dt in dates
                     let isDone = dt.Start != previous.End
                     let prev = isDone || last == dt ? 
                                                previous :  
                                               (previous = new Range(previous.Start,dt.End))
                     where isDone || last == dt
                     let t = (previous = dt)
                     select prev).ToList();
        if(result.Last().End != last.End)
           result.Add(last);
        return result;
    }
    return Enumerable.Empty<Range>().ToList();
}

Я не думаю, что на самом деле это делаю в производственном коде, потому что я чувствую, что это нарушает правило наименьшего удивления. Если выражения linq обычно не имеют побочных эффектов, это работает, потому что имеет побочные эффекты. Однако я счел целесообразным опубликовать, чтобы показать, что в действительности это можно решить, используя синтаксис понимания запроса в O (n)

Ответ 7

        var ranges = new List<Range>() { dr1, dr2, dr3, dr4 };
        var starts = ranges.Select(p => p.Start);
        var ends = ranges.Select(p => p.End);
        var discreet = starts.Union(ends).Except(starts.Intersect(ends)).OrderBy(p => p).ToList();
        List<Range> result = new List<Range>();
        for (int i = 0; i < discreet.Count; i = i + 2)
        {
            result.Add(new Range(discreet[i], discreet[i + 1]));
        }
        return result;