将IList <datetime>中的连续日期组合到范围</datetime>中

时间:2011-10-03 12:20:35

标签: c# linq

  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
    

    D n 是我列表中的不同日期, G m 日期是我的日期喜欢在它们中间生成。

  4. 问题

    如何将各个日期的有序列表转换为对,以便我得到如下例所示的对?我想用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 new = D from +(D to - D from )/ 2

    空范围问题

    范围D 10 -D 11 应该优选地省略。但是如果省略它会导致代码过于复杂,那么可以通过单独的检查来保留和排除代码。但如果最初可以将其排除在外那么应该做什么。因此,如果您还提供有关如何形成排除空范围的对的信息,也欢迎您添加该信息。

4 个答案:

答案 0 :(得分:5)

您可以使用Zip()

var middleDates = dates.Zip(dates.Skip(1), 
                            (a, b) => (a.AddTicks((b - a).Ticks / 2)))
                       .ToList();

答案 1 :(得分:3)

最终解决方案

根据@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();

答案 2 :(得分:1)

  

下一步是形成对(我不知道如何使用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();

答案 3 :(得分:1)

好的,我以前的想法是行不通的。但是这个会。它的输入数量为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();