组合IList<;日期时间>;进入范围
本文关键字:范围 gt 日期 IList lt 组合 时间 | 更新日期: 2023-09-27 18:10:13
- 我有一系列从到日期的对象
-
使用类似的东西
IList<DateTime> dates = this.DateRanges .SelectMany(r => new [] { r.From, r.To }) .Distinct() .OrderBy(d => d) .ToList();
我可以得到所有的日期,而不会有任何重复。范围可能完全重叠、部分重叠(上部或下部重叠(、触摸,也可能根本不重叠。
-
现在我需要将这个列表转换为一个不同的列表,以便每个连续的日期对在对的中间形成一个新生成的
DateTime
实例D1 D2 D3 D4 D5 G1 G2 G3 G4
其中,Dn是我在列表中的不同日期,而Gm日期是我希望在它们中间生成的日期。
问题
如何将单个日期的有序列表转换为对,以便获得如以下示例所示的对?我想使用LINQ而不是for
循环来形成这些循环,这可以完成同样的事情。由于表达式树执行延迟,使用LINQ可能会产生更高效的代码。
使用真实世界的示例进行补充说明
假设这是我的例子这样的范围:
D1 D2 D3 D4 D5 D6 D11 D12
|--------------| |------| |------| |------|
D7 D8
|--------------------------|
D9 D10
|-----------------------------------------------|
获取不同日期的第一步将产生以下日期:
D1 D7 D2 D3 D4 D5 D6 D10 D11 D12
D9和D8会脱落,因为它们是重复的。
下一步是形成对(我不知道如何使用LINQ(:
D1-D7, D7-D2, D2-D3, D3-D4, D4-D5, D5-D6, D6-D10, (D10-D11), D11-D12
最后一步必须使用计算每对的日期
D新=D+(D>到-D从(/2
空范围问题
范围D10-D11最好应省略。但是,如果省略它会导致代码过于复杂,那么可以在之后通过单独的检查来保留和排除它。但是,如果它最初可以被排除在外,那么这就是应该做的。因此,如果您还提供了如何形成排除空范围的对的信息,欢迎您也添加这些信息。
您可以使用Zip()
:
var middleDates = dates.Zip(dates.Skip(1),
(a, b) => (a.AddTicks((b - a).Ticks / 2)))
.ToList();
最终解决方案
基于@DavidB的想法和@AakashM原始答案的有趣想法,我提出了自己的解决方案,从一组日期中提取范围(同时省略空范围(并计算范围中间日期。
如果您对此解决方案有任何改进建议或意见,我们非常欢迎您对此发表评论。无论如何,这是我现在使用的最后一个代码(内联评论解释其功能(:
// counts range overlaps
int counter = 0;
// saves previous date to calculate midrange date
DateTime left = DateTime.Now;
// get mid range dates
IList<DateTime> dates = this.DateRanges
// select range starts and ends
.SelectMany(r => new[] {
new {
Date = r.From,
Counter = 1
},
new {
Date = r.To,
Counter = -1
}
})
// order dates because they come out mixed
.OrderBy(o => o.Date)
// convert dates to ranges; when non-empty & non-zero wide get mid date
.Select(o => {
// calculate middle date if range isn't empty and not zero wide
DateTime? result = null;
if ((counter != 0) && (left != o.Date))
{
result = o.Date.AddTicks(new DateTime((o.Date.Ticks - left.Ticks) / 2).Ticks);
}
// prepare for next date range
left = o.Date;
counter += o.Counter;
// return middle date when applicable otherwise null
return result;
})
// exclude empty and zero width ranges
.Where(d => d.HasValue)
// collect non nullable dates
.Select(d => d.Value)
.ToList();
下一步是形成对(我不知道如何使用LINQ(:
List<DateTime> edges = bucketOfDates
.Distinct()
.OrderBy(date => date)
.ToList();
DateTime rangeStart = edges.First(); //ps - don't forget to handle empty
List<DateRange> ranges = edges
.Skip(1)
.Select(rangeEnd =>
{
DateRange dr = new DateRange(rangeStart, rangeEnd);
rangeStart = rangeEnd;
return dr;
})
.ToList();
好吧,我以前的想法行不通。但这次会的。它是关于输入数量的O(n)
。
为了解决D10-D11问题,我们需要了解在任何给定日期有多少的原始间隔"有效"。然后,我们可以按顺序迭代抛出转换点,并在和两个转换之间发出中点,当前状态为ON。以下是完整的代码。
数据类别:
// The input type
class DateRange
{
public DateTime From { get; set; }
public DateTime To { get; set; }
}
// Captures details of a transition point
// along with how many ranges start and end at this point
class TransitionWithCounts
{
public DateTime DateTime { get; set; }
public int Starts { get; set; }
public int Finishes { get; set; }
}
处理代码:
class Program
{
static void Main(string[] args)
{
// Inputs as per question
var d1 = new DateTime(2011, 1, 1);
var d2 = new DateTime(2011, 3, 1);
var d3 = new DateTime(2011, 4, 1);
var d4 = new DateTime(2011, 5, 1);
var d5 = new DateTime(2011, 6, 1);
var d6 = new DateTime(2011, 7, 1);
var d11 = new DateTime(2011, 9, 1);
var d12 = new DateTime(2011, 10, 1);
var d7 = new DateTime(2011, 2, 1);
var d8 = d5;
var d9 = d1;
var d10 = new DateTime(2011, 8, 1);
var input = new[]
{
new DateRange { From = d1, To = d2 },
new DateRange { From = d3, To = d4 },
new DateRange { From = d5, To = d6 },
new DateRange { From = d11, To = d12 },
new DateRange { From = d7, To = d8 },
new DateRange { From = d9, To = d10 },
};
第一步是捕获输入的开始和结束作为转换点。每个原始范围变为两个过渡点,每个过渡点的计数为1。
// Transform into transition points
var inputWithBeforeAfter = input.SelectMany(
dateRange => new[]
{
new TransitionWithCounts { DateTime = dateRange.From, Starts = 1 },
new TransitionWithCounts { DateTime = dateRange.To, Finishes = 1 }
});
现在,我们按日期对这些范围进行分组,总结出在该日期开始和结束的原始范围的数量
// De-dupe by date, counting up how many starts and ends happen at each date
var deduped = (from bdta in inputWithBeforeAfter
group bdta by bdta.DateTime
into g
orderby g.Key
select new TransitionWithCounts
{
DateTime = g.Key,
Starts = g.Sum(bdta => bdta.Starts),
Finishes = g.Sum(bdta => bdta.Finishes)
}
);
为了处理这个问题,我们可以使用Aggregate
(可能(,但(对我来说(读写手动迭代要快得多:
// Iterate manually since we want to keep a current count
// and emit stuff
var output = new List<DateTime>();
var state = 0;
TransitionWithCounts prev = null;
foreach (var current in deduped)
{
// Coming to a new transition point
// If we are ON, we need to emit a new midpoint
if (state > 0)
{
// Emit new midpoint between prev and current
output.Add(prev.DateTime.AddTicks((current.DateTime - prev.DateTime).Ticks / 2));
}
// Update state
state -= current.Finishes;
state += current.Starts;
prev = current;
}
如果我们愿意的话,我们可以在最后断言state == 0
。
// And we're done
foreach (var dateTime in output)
{
Console.WriteLine(dateTime);
}
// 16/01/2011 12:00:00
// 15/02/2011 00:00:00
// 16/03/2011 12:00:00
// 16/04/2011 00:00:00
// 16/05/2011 12:00:00
// 16/06/2011 00:00:00
// 16/07/2011 12:00:00
// 16/09/2011 00:00:00
// Note: nothing around 15/08 as that is between D10 and D11,
// the only midpoint where we are OFF
Console.ReadKey();