计算日期时间与灵活日间时间范围之间的小时数

时间:2015-02-09 22:49:35

标签: c# algorithm datetime

我正在寻找一些优雅的算法来计算多天的小时数,预定义的“工时 - 天 - 范围”。

一个真实的例子是:只计算租借物品的工作时间。

DataTime范围可以在定义的“工时 - 日 - 范围”内部或外部开始。

示例

我创建了一个包含5种不同场景的小例子。希望这更清楚。

  • 第1行=日期
  • 第2行= Day-Time-Hours
  • 第3-7行= 5个不同的日期范围,用于计算
  • 的小时数

Time table with 5 example time spans larger image

我想到的唯一方法是在范围内每天进行一次for循环,并且在内部有多个复杂的if-trees。但我希望有人比我更聪明,能给我一些更快更优雅的提示。

非常感谢您的帮助! :)

更新1

基于Lashanes回答我是这样实现的......

public struct DateSpan
{
    public DateTime begin, end;

    public DateSpan(DateTime begin, DateTime end)
    {
        if (begin > end || end < begin)
            throw new Exception("Not possible");

        this.begin = begin;
        this.end = end;
    }

    public DateTime Begin
    {
        get
        {
            return this.begin;
        }
    }

    public DateTime End
    {
        get
        {
            return this.end;
        }
    }

    public TimeSpan TimeSpan
    {
        get
        {
            return this.End - this.Begin;
        }
    }

    public TimeSpan GetWorkTimeSpan(TimeSpan? workTimeBegin, TimeSpan? workTimeEnd)
    {
        if (this.Begin.Date == this.End.Date)
        {
            long totalWorkTimeTicks = Math.Min(workTimeEnd.Value.Ticks, this.End.Ticks) - Math.Max(workTimeBegin.Value.Ticks, this.Begin.Ticks);

            return TimeSpan.FromTicks(totalWorkTimeTicks);
        }
        else
        {
            TimeSpan dailyWorkTime = TimeSpan.FromDays(1);
            dailyWorkTime -= workTimeBegin ?? TimeSpan.Zero;
            dailyWorkTime -= TimeSpan.FromDays(1) - workTimeEnd ?? TimeSpan.FromDays(1);

            long totalDaysWorkTimeTicks = (int)(this.TimeSpan.TotalDays) * dailyWorkTime.Ticks;

            long firstDayWorkTimeTicks = Math.Min(dailyWorkTime.Ticks, Math.Max(0, workTimeEnd.Value.Ticks - this.Begin.TimeOfDay.Ticks));

            long lastDayWorkTimeTicks = Math.Min(dailyWorkTime.Ticks, Math.Max(0, this.End.TimeOfDay.Ticks - workTimeBegin.Value.Ticks));

            return TimeSpan.FromTicks(firstDayWorkTimeTicks + totalDaysWorkTimeTicks + lastDayWorkTimeTicks);
        }
    }
}

和测试用例......

 DateSpan dateRange1 = new DateSpan(new DateTime(2012, 01, 01, 07, 00, 00), new DateTime(2012, 01, 03, 15, 00, 00));
 DateSpan dateRange2 = new DateSpan(new DateTime(2012, 01, 01, 02, 00, 00), new DateTime(2012, 01, 03, 20, 00, 00));
 DateSpan dateRange3 = new DateSpan(new DateTime(2012, 01, 01, 04, 00, 00), new DateTime(2012, 01, 03, 23, 00, 00));
 DateSpan dateRange4 = new DateSpan(new DateTime(2012, 01, 01, 23, 00, 00), new DateTime(2012, 01, 03, 09, 00, 00));
 DateSpan dateRange5 = new DateSpan(new DateTime(2012, 01, 02, 12, 00, 00), new DateTime(2012, 01, 02, 20, 00, 00));

 Debug.WriteLine(String.Format("dateRange1: {0}", dateRange1.GetWorkTimeSpan(new TimeSpan(04, 00, 00), new TimeSpan(17, 00 ,00)).TotalHours));
 Debug.WriteLine(String.Format("dateRange2: {0}", dateRange2.GetWorkTimeSpan(new TimeSpan(04, 00, 00), new TimeSpan(17, 00, 00)).TotalHours));
 Debug.WriteLine(String.Format("dateRange3: {0}", dateRange3.GetWorkTimeSpan(new TimeSpan(04, 00, 00), new TimeSpan(17, 00, 00)).TotalHours));
 Debug.WriteLine(String.Format("dateRange4: {0}", dateRange4.GetWorkTimeSpan(new TimeSpan(04, 00, 00), new TimeSpan(17, 00, 00)).TotalHours));
 Debug.WriteLine(String.Format("dateRange5: {0}", dateRange5.GetWorkTimeSpan(new TimeSpan(04, 00, 00), new TimeSpan(17, 00, 00)).TotalHours));

结果是..

dateRange1: 47 // should be 37h
dateRange2: 52 // should be 42h
dateRange3: 52 // should be 42h
dateRange4: 18 // should be 20h
dateRange5: -17628067  // should be 6h

我做错了什么?我想我理解Lashane解释的方式,但没有看到我的错误...... :(

3 个答案:

答案 0 :(得分:1)

算法可能非常简单,我假设所有日期范围都舍入为小时,即没有分钟/秒

所以,要计算你需要的工作小时数:

  1. 日期时间之间的整天天数,例如int totalDaysHours = 14 * (int)((dtTwo - dtOne).TotalDays);请注意我们将整理为止只有整整一天
  2. 第一天的工作时间,例如int firstDayHours = Math.Min(14, Math.Max(0, 18-dtOne.Hour));
  3. 最后一天的工作时间,例如int lastDayHours = Math.Min(14, Math.Max(0, dtTwo.Hour - 3));
  4. 注意 - 如果开始和结束日期相同,则需要转到另一个分支:

    int totalWorkingHours = Math.min(18, dtTwo.Hour) - Math.max(4, dtOne.Hour);

    这里的常数:

    • 14 - 每天工作小时数
    • 18 - 工作时间后第一个非工作时间
    • 3 - 工作时间前一天的非工作时间

    关于最小/最大的注释:

    1. max with 0将所有负值更改为0,但保持正值,例如:最后一天我们在2(即0个工作小时)结束,所以Math.Max(0,2-3 == -1)= 0
    2. min需要将工作时数限制为每天14个,例如:最后一天我们已经完成20个,Math.min(14,20-3 == 17)将只给我们14个
    3. 这是基于您的代码的工作实现:

          if (this.Begin.Date == this.End.Date)
          {
              long totalWorkTimeHours = Math.Min(workTimeEnd.Hours+1, this.End.Hour) - Math.Max(workTimeBegin.Hours, this.Begin.Hour); // note + 1
      
              return TimeSpan.FromHours(totalWorkTimeHours);
          }
          else
          {
              TimeSpan dailyWorkTime = TimeSpan.FromDays(1);
              dailyWorkTime -= workTimeBegin;
              dailyWorkTime -= TimeSpan.FromDays(1) - workTimeEnd;
      
              long totalDaysWorkTimeHours = ((long)this.TimeSpan.TotalDays - 1) * (dailyWorkTime.Hours + 1); // note -1 for days (3rd January - 1st January = 1 whole day, not 2, +1 for hours
      
              long firstDayWorkTimeHours = Math.Min(dailyWorkTime.Hours + 1, Math.Max(0, workTimeEnd.Hours + 1 - this.Begin.Hour)); // +1 hours
      
              long lastDayWorkTimeHours = Math.Min(dailyWorkTime.Hours + 1, Math.Max(0, this.End.Hour + 1 - workTimeBegin.Hours)); // +1 hours
      
              return TimeSpan.FromHours(firstDayWorkTimeHours + totalDaysWorkTimeHours + lastDayWorkTimeHours);
          }
      

      为什么我们需要添加1到几小时,因为传递的参数是17(这是最后一个工作小时,如果开始时间也是17 - 我们应该得到1作为结果),相同的dailyWorkTime,17-4给出我们13,但实际上我们有14个工作小时

      也注意到小问题:

          public TimeSpan TimeSpan
          {
              get
              {
                  return this.End.Date - this.Begin.Date; // use dates instead of original time stamps
              }
          }
      

答案 1 :(得分:1)

我为一个previous answer编写了一个类,它定义了一个用于计算空闲时间的Period类。我已经扩展了那个类来解决这个问题。

首先,这是输入数据:

var periods = new []
{
    new Period(new DateTime(2014, 1, 1, 7, 0, 0), new DateTime(2014, 1, 3, 16, 0, 0)),
    new Period(new DateTime(2014, 1, 1, 2, 0, 0), new DateTime(2014, 1, 3, 21, 0, 0)),
    new Period(new DateTime(2014, 1, 1, 4, 0, 0), new DateTime(2014, 1, 4, 0, 0, 0)),
    new Period(new DateTime(2014, 1, 1, 23, 0, 0), new DateTime(2014, 1, 3, 10, 0, 0)),
    new Period(new DateTime(2014, 1, 2, 12, 0, 0), new DateTime(2014, 1, 2, 21, 0, 0)),
};

以下是我编写的用于计算工作期间小时数的查询:

var query =
    from period in periods
    let workingPeriods =
        Enumerable
            .Range(0, period.EndTime.Date.Subtract(period.StartTime.Date).Days + 1)
            .Select(n => period.StartTime.Date.AddDays((double)n))
            .Select(d => new Period(d.AddHours(4.0), d.AddHours(18)))
    let remainders = period.Remove(workingPeriods)
    let hoursDuringWorkingPeriods = period.TotalHours - remainders.Sum(x => x.TotalHours)
    select new { Period = period.ToString(), hoursDuringWorkingPeriods };

这给了我这个结果:

result

这是更新的Period类:

private sealed class Period : IEquatable<Period>
{
    public DateTime StartTime { get; private set; }
    public DateTime EndTime { get; private set; }

    public Period(DateTime startTime, DateTime endTime)
    {
        this.StartTime = startTime;
        this.EndTime = endTime;
    }

    public double TotalHours
    {
        get
        {
            return this.EndTime.Subtract(this.StartTime).TotalHours;
        }
    }

    public override bool Equals(object obj)
    {
        if (obj is Period)
            return Equals((Period)obj);
        return false;
    }

    public bool Equals(Period obj)
    {
        if (obj == null)
            return false;
        if (!EqualityComparer<DateTime>.Default.Equals(
                    this.StartTime, obj.StartTime))
            return false;
        if (!EqualityComparer<DateTime>.Default.Equals(
                    this.EndTime, obj.EndTime))
            return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        hash ^= EqualityComparer<DateTime>.Default
            .GetHashCode(this.StartTime);
        hash ^= EqualityComparer<DateTime>.Default
            .GetHashCode(this.EndTime);
        return hash;
    }

    public override string ToString()
    {
        return String.Format("{{ StartTime = {0}, EndTime = {1} }}",
            this.StartTime, this.EndTime);
    }

    public IEnumerable<Period> Remove(Period period)
    {
        if (period.StartTime <= this.StartTime)
        {
            if (period.EndTime <= this.StartTime)
                yield return this;
            else if (period.EndTime >= this.EndTime)
                yield break;
            else
                yield return new Period(period.EndTime, this.EndTime);
        }
        else if (period.StartTime < this.EndTime)
        {
            yield return new Period(this.StartTime, period.StartTime);
            if (period.EndTime < this.EndTime)
            {

                yield return new Period(period.EndTime, this.EndTime);
            }
        }
        else
            yield return this;
    }

    public IEnumerable<Period> Remove(IEnumerable<Period> periods)
    {
        return Remove(new [] { this }, periods);
    }

    private static IEnumerable<Period> Remove(IEnumerable<Period> selfs, IEnumerable<Period> periods)
    {
        if (periods == null || periods.IsEmpty())
        {
            return Enumerable.Empty<Period>();
        }
        else
        {
            var period = periods.First();
            var nexts =
                from s in selfs
                from ss in s.Remove(period)
                select ss;
            return periods.Skip(1).Any() ? Remove(nexts, periods.Skip(1)) : nexts;
        }
    }
}

答案 2 :(得分:0)

基于Lashanes answere我更改了代码以返回精确的TimeStamp值...

代码

public struct DateSpan
{
    public DateTime Begin
    {
        get
        {
            return this.begin;
        }
    }

    public DateTime End
    {
        get
        {
            return this.end;
        }
    }

    public TimeSpan TimeSpan
    {
        get
        {
            return this.End - this.Begin;
        }
    }

    public TimeSpan GetWorkTimeSpan(TimeSpan? workTimeBegin, TimeSpan? workTimeEnd)
    {
        if (this.Begin.Date == this.End.Date)
        {
            long totalWorkTimeTicks = Math.Min(workTimeEnd.Value.Ticks, this.End.TimeOfDay.Ticks) - Math.Max(workTimeBegin.Value.Ticks, this.Begin.TimeOfDay.Ticks);

            return TimeSpan.FromTicks(totalWorkTimeTicks);
        }
        else
        {
            TimeSpan daySpan = this.End.Date - this.Begin.Date;

            TimeSpan dailyWorkTime = TimeSpan.FromDays(1);
            dailyWorkTime -= workTimeBegin ?? TimeSpan.Zero;
            dailyWorkTime -= TimeSpan.FromDays(1) - workTimeEnd ?? TimeSpan.FromDays(1);



            long totalDaysWorkTimeTicks = (int)(daySpan.TotalDays - 1) * dailyWorkTime.Ticks;

            long firstDayWorkTimeTicks = Math.Min(dailyWorkTime.Ticks, Math.Max(0, (workTimeEnd ?? TimeSpan.FromDays(1)).Ticks - this.Begin.TimeOfDay.Ticks));

            long lastDayWorkTimeTicks = Math.Min(dailyWorkTime.Ticks, Math.Max(0, this.End.TimeOfDay.Ticks - (workTimeBegin ?? TimeSpan.Zero).Ticks));

            return TimeSpan.FromTicks(firstDayWorkTimeTicks + totalDaysWorkTimeTicks + lastDayWorkTimeTicks);
        }
    }
}

测试用例

 TimeSpan workTimeBegin = new TimeSpan(04, 00, 00);
 TimeSpan workTimeEnd = new TimeSpan(18, 00, 00);

 DateSpan dateRange1 = new DateSpan(new DateTime(2012, 01, 01, 07, 00, 00), new DateTime(2012, 01, 03, 15, 00, 00));
 DateSpan dateRange2 = new DateSpan(new DateTime(2012, 01, 01, 02, 00, 00), new DateTime(2012, 01, 03, 20, 00, 00));
 DateSpan dateRange3 = new DateSpan(new DateTime(2012, 01, 01, 04, 00, 00), new DateTime(2012, 01, 03, 23, 00, 00));
 DateSpan dateRange4 = new DateSpan(new DateTime(2012, 01, 01, 23, 00, 00), new DateTime(2012, 01, 03, 09, 00, 00));
 DateSpan dateRange5 = new DateSpan(new DateTime(2012, 01, 02, 12, 00, 00), new DateTime(2012, 01, 02, 20, 00, 00));
 DateSpan dateRange6 = new DateSpan(new DateTime(2012, 01, 02, 20, 00, 00), new DateTime(2012, 01, 03, 03, 00, 00));
 DateSpan dateRange7 = new DateSpan(new DateTime(2012, 01, 02, 15, 00, 00), new DateTime(2012, 01, 03, 00, 00, 00));

Debug.WriteLine(String.Format("dateRange1: {0} ({1})", dateRange1.GetWorkTimeSpan(workTimeBegin, workTimeEnd), dateRange1.GetWorkTimeSpan(workTimeBegin, workTimeEnd).TotalHours));
Debug.WriteLine(String.Format("dateRange2: {0} ({1})", dateRange2.GetWorkTimeSpan(workTimeBegin, workTimeEnd), dateRange2.GetWorkTimeSpan(workTimeBegin, workTimeEnd).TotalHours));
Debug.WriteLine(String.Format("dateRange3: {0} ({1})", dateRange3.GetWorkTimeSpan(workTimeBegin, workTimeEnd), dateRange3.GetWorkTimeSpan(workTimeBegin, workTimeEnd).TotalHours));
Debug.WriteLine(String.Format("dateRange4: {0} ({1})", dateRange4.GetWorkTimeSpan(workTimeBegin, workTimeEnd), dateRange4.GetWorkTimeSpan(workTimeBegin, workTimeEnd).TotalHours));
Debug.WriteLine(String.Format("dateRange5: {0} ({1})", dateRange5.GetWorkTimeSpan(workTimeBegin, workTimeEnd), dateRange5.GetWorkTimeSpan(workTimeBegin, workTimeEnd).TotalHours));
Debug.WriteLine(String.Format("dateRange6: {0} ({1})", dateRange6.GetWorkTimeSpan(workTimeBegin, workTimeEnd), dateRange6.GetWorkTimeSpan(workTimeBegin, workTimeEnd).TotalHours));
Debug.WriteLine(String.Format("dateRange7: {0} ({1})", dateRange7.GetWorkTimeSpan(workTimeBegin, workTimeEnd), dateRange7.GetWorkTimeSpan(workTimeBegin, workTimeEnd).TotalHours));

结果

dateRange1: 1.12:00:00 (36) // correct
dateRange2: 1.18:00:00 (42) // correct
dateRange3: 1.18:00:00 (42) // correct
dateRange4: 19:00:00 (19) // correct
dateRange5: 06:00:00 (6) // correct
dateRange6: 00:00:00 (0) // correct
dateRange7: 03:00:00 (3) // correct

现在可以使用,因为我希望它可以工作...... :)