组合IList<;日期时间>;进入范围

本文关键字:范围 gt 日期 IList lt 组合 时间 | 更新日期: 2023-09-27 18:10:13

  1. 我有一系列到日期的对象
  2. 使用类似的东西

    IList<DateTime> dates =
        this.DateRanges
            .SelectMany(r => new [] { r.From, r.To })
            .Distinct()
            .OrderBy(d => d)
            .ToList();
    

    我可以得到所有的日期,而不会有任何重复。范围可能完全重叠、部分重叠(上部或下部重叠(、触摸,也可能根本不重叠。

  3. 现在我需要将这个列表转换为一个不同的列表,以便每个连续的日期对在对的中间形成一个新生成的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最好应省略。但是,如果省略它会导致代码过于复杂,那么可以在之后通过单独的检查来保留和排除它。但是,如果它最初可以被排除在外,那么这就是应该做的。因此,如果您还提供了如何形成排除空范围的对的信息,欢迎您也添加这些信息。

组合IList<;日期时间>;进入范围

您可以使用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();