LINQ Group连续时间

假设我有一个看起来像这样的简单结构:

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() { dr1, dr2, dr3, dr4 }; 

我想要做的是将它们连续的范围分组 – 即如果前一个范围的结束值与下一个范围的开始相同,它们是连续的。

我们可以假设Range值中没有碰撞/重复或重叠。

在发布的示例中,我最终会得到两个组:

 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魔术可以用来在一个漂亮的单行中获得这个吗?

您最好的选择是使用yield和扩展方法:

 static IEnumerable GroupContinuous(this IEnumerable ranges) { // Validate parameters. // Can order by start date, no overlaps, no collisions ranges = ranges.OrderBy(r => r.Start); // Get the enumerator. using (IEnumerator 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 ,并且可以在遍历列表时生成项目并在不同的折叠Range实例上中断。

最终,如果序列是无序的,那么您有两个选择:

  • 订购列表然后处理(记住, OrderBy也是延迟的,但是必须使用一个完整的迭代来订购序列),当你有一个要处理的时候使用yield返回项目
  • 立即处理整个序列并作为整个物化序列返回

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 => xA, x => xB, (lower, upper) => new { A = lower, B = upper }); foreach(var range in ranges) { Console.WriteLine("{0} - {1}", range.A, range.B); } 

实施扩展方法

  ///  /// Calculates continues ranges based on individual elements lower and upper selections. Cannot compensate for overlapping. ///  /// The type containing a range /// The type of range values /// The ranges to be combined /// The range's lower bound /// The range's upper bound /// A factory to create a new range /// An enumeration of continuous ranges public static IEnumerable ContinousRange(this IEnumerable source, Func lowerSelector, Func upperSelector, Func 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; } } 

您可以使用Aggregate()方法和lambda将它们组合在一起。 正如您所说,这是假设没有重复或重叠:

 // build your set of continuous ranges for results List continuousRanges = new List(); ranges.Aggregate(continuousRanges, (con, next) => { { // pull last item (or default if none) - O(1) for List 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 continuousRanges = new List(); ranges.Aggregate(continuousRanges, (con, next) => { { // pull last item (or default if none) - O(1) for List var last = continuousRanges.LastOrDefault(); if (last != null && last.End == next.Start) last.End = next.End; else con.Add(next); return con; }); 

这是另一个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)); 

以下工作,但它真的是滥用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; }); 

代码执行以下操作:

首先选择:

  1. 将当前值和当前先前值保存在新匿名类型的实例中
  2. 将上一个值设置为下一次迭代的当前值

其中:仅选择标记新连续范围开始的元素

第二个选择:

  1. 使用保存的起始值的开始日期和上一项的结束值创建Range对象。
  2. 将起始值设置为当前项目,因为它标记了新连续范围的开始。
  3. 返回(1)中创建的范围

请注意:
我会坚持使用迭代解决方案,因为上面的代码是不可读的不可 维护的 ,而且只需输入一个循环就可以花费更多的时间

以下代码在查询理解语法中执行此操作。

 public static List Group(List 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().ToList(); } 

我不认为我会在生产代码中实际执行此操作,因为我觉得它违反了最不惊讶的规则。 linq语句通常没有副作用,这是有效的,因为它副作用。 但是我认为值得发帖表明确实可以使用O(n)中的查询理解语法来解决它

  var ranges = new List() { 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 result = new List(); for (int i = 0; i < discreet.Count; i = i + 2) { result.Add(new Range(discreet[i], discreet[i + 1])); } return result;