【问题标题】:Combining consecutive dates in IList<DateTime> into ranges将 IList<DateTime> 中的连续日期组合成范围
【发布时间】:2011-10-03 12:20:34
【问题描述】:
  1. 我有一系列具有 fromto 日期的对象。
  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

最后一步必须使用以下方法计算每对的日期:

Dnew = Dfrom + (Dto - Dfrom)/2

空范围问题

范围 D10-D11 最好省略。但是,如果省略它会导致代码过于复杂,则可以保留并排除它,然后再进行单独的检查。但是,如果最初可以排除它,那么这就是应该做的。因此,如果您还提供了有关如何形成排除空范围的对的信息,欢迎您也添加该信息。

【问题讨论】:

  • 啊啊啊我无法理解!不连续的日期会发生什么? 1-Jan 5-Jan 和 8-Jan 25-Jan...我认为您应该尝试解释您要解决的问题...事实上,它非常复杂,因为您有重叠的范围并且范围可能是另一个范围的子集。
  • Nitpick:我会使用 ....OrderBy(d =&gt; d).ToList() 而不是 ....ToList(); dates.Sort();
  • @Robert(有更正)您将如何处理不连贯的日期集?您的示例具有完全重叠的日期集。在 D1 和 D10 之间,没有一天不被“覆盖”。例如,尝试在 D10 的右侧添加一个 D11-D12,其中 D11 > D10。
  • @xanatos:非常好的观点。我改变了我的帖子。
  • @KirkBroadhurst 我显然没有把我的情况说得太好,因为那不是我想要的。我想从连续的项目中形成对......计算中间太简单了,你不觉得吗?

标签: c# linq


【解决方案1】:

你可以使用Zip():

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

【讨论】:

  • 这非常聪明,完全符合我的需要。当不调用ToList() 时,我可以将循环范围延迟到Skip。这是非常聪明的。您能否也看看非重叠范围的第一部分,因为最好排除空范围(D10 和 D11 之间)
  • @RobertKoritnik:是的——你可以编写一个只枚举一次的自定义扩展方法(或常规迭代代码)——在这种情况下,Zip 总是会枚举两次——我可以在一段时间内尝试一下。另外,您所说的“空”日期范围到底是什么意思?
  • 无赖。 Zip 仅在 .net 4+ 中可用...我需要 3.5 中的东西所以我发现something useful...
  • @Robert 如果你真的需要Zip,你可以看看Mono的源代码:-)
  • @xanatos 我直接在.Net中查看了它(反射器等......)。但我最终使用了自己的解决方案,一次完成所有操作。检查最终解决方案的答案,如果有任何改进建议。
【解决方案2】:

最终解决方案

基于@DavidB 的想法和@AakashM 的原始答案的有趣想法,我提出了自己的解决方案,从一组日期中提取范围(同时也省略空范围)并计算范围中间日期。

如果您对此解决方案有任何改进建议或 cmet,欢迎您发表评论。无论如何,这是我现在使用的最终代码(内联 cmets 解释了它的功能):

// 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();

【讨论】:

  • @DavidB:谢谢。我用它来轻松排除空范围,所以我一次性完成。
【解决方案3】:

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

【讨论】:

  • +1 这是将 LINQ 与一些常规的 foreach 循环代码相结合的好方法。但这很简单,这很棒。谢谢你。也有可能发生,我会以不同的方式来做,并将一些步骤组合在一起以使代码性能更好。
  • 我在最终解决方案中添加了自己的答案,该解决方案部分使用了您将一组日期组合成范围的想法。
【解决方案4】:

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

【讨论】:

  • 这实际上与我的最终解决方案相似,但我根据使用左右布尔值阅读您的第一个答案的想法编写了更少的代码。让我把我的解决方案放在一个答案中。
  • Actaully 我的最终解决方案使用@DavidB 范围生成和计数器的想法,其工作方式类似于您的StartsFinishes,但代码更少更简单。我没有对日期进行分组(如你所做的那样),因为没有必要这样做,我只是确保我计算了非空和非零宽度范围内的中点。就是这样。我仍然奖励您 +1 以感谢您在此答案以及有效的解决方案中付出的努力。
猜你喜欢
  • 1970-01-01
  • 2014-12-16
  • 2013-03-24
  • 1970-01-01
  • 1970-01-01
  • 2017-02-12
  • 1970-01-01
  • 2013-06-29
  • 1970-01-01
相关资源
最近更新 更多