Я пытаюсь выбрать подгруппу списка, где объекты имеют непрерывные даты, например.
ID StaffID Title ActivityDate -- ------- ----------------- ------------ 1 41 Meeting with John 03/06/2010 2 41 Meeting with John 08/06/2010 3 41 Meeting Continues 09/06/2010 4 41 Meeting Continues 10/06/2010 5 41 Meeting with Kay 14/06/2010 6 41 Meeting Continues 15/06/2010
Я использую точку опоры каждый раз, поэтому возьмите объект центра в качестве примера в качестве 3, я хотел бы получить следующие получающиеся непрерывные события вокруг центра:
ID StaffID Title ActivityDate -- ------- ----------------- ------------ 2 41 Meeting with John 08/06/2010 3 41 Meeting Continues 09/06/2010 4 41 Meeting Continues 10/06/2010
Моя текущая реализация является трудоемким "обходом" в прошлое, затем в будущее, для создания списка:
var activity = // item number 3: Meeting Continues (09/06/2010)
var orderedEvents = activities.OrderBy(a => a.ActivityDate).ToArray();
// Walk into the past until a gap is found
var preceedingEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID);
DateTime dayBefore;
var previousEvent = activity;
while (previousEvent != null)
{
dayBefore = previousEvent.ActivityDate.AddDays(-1).Date;
previousEvent = preceedingEvents.TakeWhile(a => a.ID != previousEvent.ID).LastOrDefault();
if (previousEvent != null)
{
if (previousEvent.ActivityDate.Date == dayBefore)
relatedActivities.Insert(0, previousEvent);
else
previousEvent = null;
}
}
// Walk into the future until a gap is found
var followingEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
DateTime dayAfter;
var nextEvent = activity;
while (nextEvent != null)
{
dayAfter = nextEvent.ActivityDate.AddDays(1).Date;
nextEvent = followingEvents.SkipWhile(a => a.ID != nextEvent.ID).Skip(1).FirstOrDefault();
if (nextEvent != null)
{
if (nextEvent.ActivityDate.Date == dayAfter)
relatedActivities.Add(nextEvent);
else
nextEvent = null;
}
}
Список relatedActivities
должен затем содержать непрерывные события, в порядке.
Существует ли лучший путь (возможно, использующий LINQ) для этого?
У меня была идея использовать .Aggregate()
но не мог думать, как заставить агрегат вспыхивать, когда он находит разрыв в последовательности.
В этом случае я думаю, что стандартный цикл foreach
, вероятно, более читабелен, чем LINQ-запрос:
var relatedActivities = new List<TActivity>();
bool found = false;
foreach (var item in activities.OrderBy(a => a.ActivityDate))
{
int count = relatedActivities.Count;
if ((count > 0) && (relatedActivities[count - 1].ActivityDate.Date.AddDays(1) != item.ActivityDate.Date))
{
if (found)
break;
relatedActivities.Clear();
}
relatedActivities.Add(item);
if (item.ID == activity.ID)
found = true;
}
if (!found)
relatedActivities.Clear();
Если уж на то пошло, вот примерно эквивалентный - и гораздо менее читабельный - LINQ-запрос:
var relatedActivities = activities
.OrderBy(x => x.ActivityDate)
.Aggregate
(
new { List = new List<TActivity>(), Found = false, ShortCircuit = false },
(a, x) =>
{
if (a.ShortCircuit)
return a;
int count = a.List.Count;
if ((count > 0) && (a.List[count - 1].ActivityDate.Date.AddDays(1) != x.ActivityDate.Date))
{
if (a.Found)
return new { a.List, a.Found, ShortCircuit = true };
a.List.Clear();
}
a.List.Add(x);
return new { a.List, Found = a.Found || (x.ID == activity.ID), a.ShortCircuit };
},
a => a.Found ? a.List : new List<TActivity>()
);
Вот реализация:
public static IEnumerable<IGrouping<int, T>> GroupByContiguous(
this IEnumerable<T> source,
Func<T, int> keySelector
)
{
int keyGroup = Int32.MinValue;
int currentGroupValue = Int32.MinValue;
return source
.Select(t => new {obj = t, key = keySelector(t))
.OrderBy(x => x.key)
.GroupBy(x => {
if (currentGroupValue + 1 < x.key)
{
keyGroup = x.key;
}
currentGroupValue = x.key;
return keyGroup;
}, x => x.obj);
}
Вы можете либо преобразовать даты в целые числа с помощью вычитания, либо представить версию DateTime (легко).
Почему-то я не думаю, что LINQ действительно предназначался для двунаправленного одномерного поиска в глубину, но я построил рабочий LINQ с использованием Aggregate. В этом примере я собираюсь использовать список вместо массива. Кроме того, я собираюсь использовать Activity
для ссылки на любой класс, в котором вы храните данные. Замените его тем, что подходит для вашего кода.
Прежде чем мы начнем, нам понадобится небольшая функция для обработки чего-либо. List.Add (T)
возвращает null, но мы хотим иметь возможность накапливать в списке и возвращать новый список для этой агрегатной функции. Итак, все, что вам нужно, это простая функция, подобная следующей.
private List<T> ListWithAdd<T>(List<T> src, T obj)
{
src.Add(obj);
return src;
}
Сначала мы получаем отсортированный список всех действий, а затем инициализируем список связанных действий. Этот первоначальный список будет содержать только целевую активность для начала.
List<Activity> orderedEvents = activities.OrderBy(a => a.ActivityDate).ToList();
List<Activity> relatedActivities = new List<Activity>();
relatedActivities.Add(activity);
Мы должны разбить это на два списка: прошлое и будущее, как вы это делаете сейчас.
Начнем с прошлого, конструкция должна выглядеть в основном знакомой. Затем мы объединим все это в relatedActivities. Здесь используется функция ListWithAdd
, которую мы написали ранее. Вы можете сжать его в одну строку и пропустить объявление previousEvents как отдельной переменной, но для этого примера я оставил его отдельно.
var previousEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID).Reverse();
relatedActivities = previousEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, prevItem) => items.OrderBy(a => a.ActivityDate).First().ActivityDate.Subtract(prevItem.ActivityDate).Days.Equals(1) ? ListWithAdd(items, prevItem) : items).ToList();
Затем мы построим следующие события аналогичным образом и аналогичным образом объединим их.
var nextEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
relatedActivities = nextEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, nextItem) => nextItem.ActivityDate.Subtract(items.OrderBy(a => a.ActivityDate).Last().ActivityDate).Days.Equals(1) ? ListWithAdd(items, nextItem) : items).ToList();
После этого вы можете правильно отсортировать результат, так как теперь relatedActivities должен содержать все действия без пробелов.Он не сломается сразу после первого пробела, нет, но я не думаю, что вы можете буквально вырваться из LINQ. Поэтому вместо этого он просто игнорирует все, что находит за промежутком.
Обратите внимание, что этот пример кода работает только с фактической разницей во времени. Выходные данные вашего примера, по-видимому, подразумевают, что вам нужны другие факторы сравнения, но этого должно быть достаточно, чтобы вы начали. Просто добавьте необходимую логику для сравнения вычитания даты в обеих записях.