Используя некоторые методы расширения, это можно сделать довольно понятно.
Во-первых, вариант Aggregate
, который является версией оператора сканирования APL, перемещается вдоль IEnumerable
, возвращая промежуточные результаты, ноэтот вариант объединяет пару за один раз, текущий и предыдущий элемент:
// TKey combineFn((TKey Key, T Value) CurKeyItem, T nextItem):
// CurKeyItem.Key = Current Key
// CurKeyItem.Value = Current Item
// NextItem = Next Item
// returns (Key, Current Item)
public static IEnumerable<(TKey Key, T Value)> ScanToPairs<T, TKey>(this IEnumerable<T> src, TKey seedKey, Func<(TKey Key, T Value), T, TKey> combineFn) {
using (var srce = src.GetEnumerator())
if (srce.MoveNext()) {
var curkv = (seedKey, srce.Current);
while (srce.MoveNext()) {
yield return curkv;
curkv = (combineFn(curkv, srce.Current), srce.Current);
}
yield return curkv;
}
}
Объяснение: ScanToPairs
проходит через IEnumerable
, начиная с первого и второго значений и значения seedKey.Он передает ValueTuple
, содержащий текущий ключ и текущий элемент и (отдельно) следующий элемент, в combFn и возвращает ValueTuple
ключа, текущего элемента.Итак, первый результат - (seedKey, FirstItem).Второй результат будет (комбинироватьFn ((seedKey, FirstItem), SecondItem), SecondItem).И т. Д.
Затем оператор GroupBy
, который группирует путем тестирования пар с помощью логической тестовой функции:
// bool testFn(T prevItem, T curItem)
// returns groups by runs of matching bool
public static IEnumerable<IGrouping<int, T>> GroupByPairsWhile<T>(this IEnumerable<T> src, Func<T, T, bool> testFn) =>
src.ScanToPairs(1, (kvp, cur) => testFn(kvp.Value, cur) ? kvp.Key : kvp.Key + 1)
.GroupBy(kvp => kvp.Key, kvp => kvp.Value);
Объяснение: При использовании метода ScanToPairs
этот метод группируетIEnumerable
в кортежи, где ключ представляет собой целое число, начиная с 1
, представляющего номер прогона true
testFn
, является результатом сравнения предыдущего элемента с текущим элементом.Как только все серии были пронумерованы, они сгруппированы вместе с GroupBy
в группы элементов, которые относятся к серии.
С этими помощниками это относительно просто.Добавьте SelectMany
после первой группировки, чтобы разбить каждую группу на подгруппы по условию времени:
var playersGroupList = orderedResults.GroupBy(x => new { x.SessionDate, x.GameId })
.SelectMany(g => g.GroupByPairsWhile((p, c) => c.SessionStartTime-p.SessionEndTime <= TimeSpan.FromMinutes(35)))
.Select(group => new GroupedEvents {
GameName = group.Select(n => n.SessionGameName).FirstOrDefault(),
GameId = group.Select(c => c.GameId).FirstOrDefault().ToString(),
PlayDate = group.Select(d => d.SessionDate).FirstOrDefault(),
Names = String.Join("; ", group.Select(g => g.SessionEventName).ToArray()),
PlayDuration = group.Select(g => g.SessionStartTime).First() + " - " + group.Select(g => g.SessionEndTime).Last(),
})
.ToList();
Таким образом, SelectMany
берет каждую группу для данного SessionDate
, иразбивает их на группы, где каждый участник находится менее чем в 35 минутах от следующего.Из-за SelectMany
все подгруппы повышаются до групп конечного результата.Итак, теперь у вас есть группы, в каждой из которых есть серия сеансов, где от SessionEndTime
до следующего SessionStartTime
меньше 35 минут.Обратите внимание, что запуск закончится в конце дня независимо от того, поэтому, если у вас могут быть запуски, которые идут в полночь, вам нужно будет изменить группировку.
Примечание: Если это возможно для сеансов, начинающихся св то же время, чтобы иметь разные длительности (т.е. время окончания), вам нужно добавить ThenBy(tsa => tsa.SessionEndTime)
к вашей orderedResults
сортировке.