计算重叠日期的住宿天数

Calculating number of nights in overlapping dates

我在寻找解决此问题的合理方法时遇到了问题。 我有一个日期范围列表,例如:

01/01/15 - 11/01/15 
02/01/15 - 04/01/15 
09/01/15 - 13/01/15 
18/01/15 - 20/01/15

我需要做的是计算出所有这些日期范围内的总住宿天数。

所以对于这个例子,总共应该是 14 晚:

01/01/15 - 11/01/15 // 10 nights
02/01/15 - 04/01/15 // Ignored as nights are covered in 1-11
09/01/15 - 13/01/15 // 2 nights as 11th and 12th nights haven't been covered
18/01/15 - 20/01/15 // 2 nights

我可以使用最小-最大日期轻松计算出总晚数,但这忽略了缺失的日期(示例中为 14-17),这是我无法计算的。

是否有任何方法可以找到缺少的总天数来帮助解决这个问题?

像这样的东西应该可以工作:

internal class Range
{
    internal DateTime From, To;

    public Range(string aFrom, string aTo)
    {
        From = DateTime.ParseExact(aFrom, "dd/mm/yy", CultureInfo.InvariantCulture);
        To = DateTime.ParseExact(aTo, "dd/mm/yy", CultureInfo.InvariantCulture);
    }
}

    public static int ComputeNights(IEnumerable<Range> ranges)
    {
        var vSet = new HashSet<DateTime>();
        foreach (var range in ranges)
            for (var i = range.From; i < range.To; i = i.AddDays(1)) vSet.Add(i)
        return vSet.Count;
    }

运行 你的例子的代码:

        var vRanges = new List<Range>
        {
            new Range("01/01/15", "11/01/15"),
            new Range("02/01/15", "04/01/15"),
            new Range("09/01/15", "13/01/15"),
            new Range("18/01/15", "20/01/15"),
        };
        var v = ComputeNights(vRanges);

v 的计算结果为 14

你可以这样计算:

var laterDateTime = Convert.ToDateTime("11/01/15 ");
var earlierDateTime = Convert.ToDateTime("01/01/15");

TimeSpan dates = laterDateTime - earlierDateTime;

int nights = dates.Days - 1;

你可以将你拥有的任何东西转换成 DateTime。 然后您可以使用 - 运算符减去两个 DateTimes。 您的结果将是一种结构 TimeSpan。

时间跨度为一天 属性。从中减去 1,您将收到夜晚。

2 天之间是 1 晚
3天之间是2晚
4天之间是3晚

我相信您可以完成剩下的工作。

这是使用 HashSet 的一种方法:

public static int CountDays(IEnumerable<TimeRange> periods)
{
    var usedDays = new HashSet<DateTime>();

    foreach (var period in periods)
        for (var day = period.Start; day < period.End; day += TimeSpan.FromDays(1))
            usedDays.Add(day);

    return usedDays.Count;
}

这假设您的日期范围是半开间隔(即开始日期被视为范围的一部分,但结束日期不是)。

这里有一个完整的程序来演示。答案是14:

using System;
using System.Collections.Generic;

namespace ConsoleApplication2
{
    public sealed class TimeRange
    {
        public DateTime Start { get; private set; }
        public DateTime End   { get; private set; }

        public TimeRange(string start, string end)
        {
            Start = DateTime.Parse(start);
            End   = DateTime.Parse(end);
        }
    }

    internal class Program
    {
        public static void Main()
        {
            var periods = new []
            {
                new TimeRange("01/01/15", "11/01/15"), 
                new TimeRange("02/01/15", "04/01/15"),
                new TimeRange("09/01/15", "13/01/15"),
                new TimeRange("18/01/15", "20/01/15")
            };

            Console.WriteLine(CountDays(periods));
        }

        public static int CountDays(IEnumerable<TimeRange> periods)
        {
            var usedDays = new HashSet<DateTime>();

            foreach (var period in periods)
                for (var day = period.Start; day < period.End; day += TimeSpan.FromDays(1))
                    usedDays.Add(day);

            return usedDays.Count;
        }
    }
}

注意:这对于较大的日期范围来说效率不高!如果您要考虑较大的日期范围,将重叠范围合并为单个范围的方法会更好。

[编辑] 修复了使用半开区间而不是闭区间的代码。

假设之间有两个晚上,例如1 月 1 日和 1 月 3 日,这应该有效。如果您已经有 DateTime 值而不是字符串,则可以去掉解析位。基本上,我使用 DateTime.Subtract() 来计算两个日期之间的天数(即夜数)。

namespace DateTest1
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;

    class Program
    {
        static void Main(string[] args)
        {
            var intervals = new List<Tuple<string, string>>
                                {
                                    new Tuple<string, string>("01/01/15", "11/01/15"),
                                    new Tuple<string, string>("02/01/15", "04/01/15"),
                                    new Tuple<string, string>("09/01/15", "13/01/15"),
                                    new Tuple<string, string>("18/01/15", "20/01/15")
                                };

            var totalNights = 0;

            foreach (var interval in intervals)
            {
                var dateFrom = DateTime.ParseExact(interval.Item1, "dd/MM/yy", CultureInfo.InvariantCulture);
                var dateTo = DateTime.ParseExact(interval.Item2, "dd/MM/yy", CultureInfo.InvariantCulture);

                var nights = dateTo.Subtract(dateFrom).Days;

                Console.WriteLine("{0} - {1}: {2} nights", interval.Item1, interval.Item2, nights);

                totalNights += nights;
            }

            Console.WriteLine("Total nights: {0}", totalNights);
        }
    }
}

01/01/15 - 11/01/15: 10 nights
02/01/15 - 04/01/15: 2 nights
09/01/15 - 13/01/15: 4 nights
18/01/15 - 20/01/15: 2 nights
Total nights: 18
Press any key to continue . . .

为了防止您在这里没有足够的答案,这里还有一个使用 Linq 和 Aggregate 的答案。 Returns 14 晚。

List<Tuple<DateTime, DateTime>> dates = new List<Tuple<DateTime, DateTime>>
    {
        Tuple.Create(new DateTime(2015, 1,1), new DateTime(2015, 1,11)),
        Tuple.Create(new DateTime(2015, 1,2), new DateTime(2015, 1,4)),
        Tuple.Create(new DateTime(2015, 1,9), new DateTime(2015, 1,13)),
        Tuple.Create(new DateTime(2015, 1,18), new DateTime(2015, 1,20))
    };

    var availableDates = 
          dates.Aggregate<Tuple<DateTime, DateTime>, 
                          IEnumerable<DateTime>, 
                          IEnumerable<DateTime>>
                (new List<DateTime>(),
                (allDates, nextRange) => allDates.Concat(Enumerable.Range(0, (nextRange.Item2 - nextRange.Item1).Days)
                                                 .Select(e => nextRange.Item1.AddDays(e))),
                 allDates => allDates);


    var numDays = 
          availableDates.Aggregate<DateTime, 
                                  Tuple<DateTime, int>, 
                                  int> 
                         (Tuple.Create(DateTime.MinValue, 0),                                                                                               
                         (acc, nextDate) =>
                         {
                             int daysSoFar = acc.Item2;                                                                                    
                             if ((nextDate - acc.Item1).Days == 1)                                                                                                
                             {                                                                                      
                                daysSoFar++;                                                                                                                                                                
                             }                                                                                                                                                                                                                                                                                                                        

                             return Tuple.Create(nextDate, daysSoFar);                                                                          
                          },
                          acc => acc.Item2);

我认为此解决方案比使用内部循环遍历范围以在列表中插入天数更快。此解决方案不需要额外的 space。它是 O(1),它只在范围内循环一次,所以它的复杂度是 O(n)。但它假定您的范围按 startdate 排序。如果没有,您可以随时轻松订购:

var p = new[]
{
    new Tuple<DateTime, DateTime>(DateTime.ParseExact("01/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("11/01/15", "dd/MM/yy", CultureInfo.InvariantCulture)),
    new Tuple<DateTime, DateTime>(DateTime.ParseExact("02/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("04/01/15", "dd/MM/yy", CultureInfo.InvariantCulture)),
    new Tuple<DateTime, DateTime>(DateTime.ParseExact("09/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("13/01/15", "dd/MM/yy", CultureInfo.InvariantCulture)),
    new Tuple<DateTime, DateTime>(DateTime.ParseExact("18/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("20/01/15", "dd/MM/yy", CultureInfo.InvariantCulture))
};

int days = (p[0].Item2 - p[0].Item1).Days;
var endDate = p[0].Item2;

for(int i = 1; i < p.Length; i++)
{
    if(p[i].Item2 > endDate)
    {
        days += (p[i].Item2 - (p[i].Item1 > endDate ? p[i].Item1 : endDate)).Days;
        endDate = p[i].Item2;
    }
}