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;