【问题标题】:Count hours between datetimes with flexible day time range使用灵活的日期时间范围计算日期时间之间的小时数
【发布时间】:2015-02-10 20:36:32
【问题描述】:

我正在寻找一些优雅的算法来计算多天的小时数,并使用预定义的“work-hour-day-range”。

一个真实的例子是:只计算一个租用对象的工作时间。

DataTime 范围可以在定义的“work-hour-day-range”之内或之外开始。

示例

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

  • 第 1 行 = 日期
  • 第 2 行 = 白天-时间-小时
  • 第 3-7 行 = 5 个不同的日期范围来计算小时数

larger image

我想到的唯一方法是在范围内的每一天和多个复杂的 if-the 树内进行 for 循环。但我希望有人比我更聪明,可以给我一点提示,让我找到一些更快更优雅的方式。

非常感谢您的帮助! :)

更新 1

基于 Lashanes answere,我以这种方式实现了它...

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 解释的方式,但没有看到我的错误...... :(

【问题讨论】:

  • 这个问题对我来说不是很清楚。您能否提供使用具体数据的简单示例(最好是可编译的)?
  • 我试图使示例更精确,但我无法提供一些代码,因为这是我试图寻求帮助的原因。 ^^

标签: c# algorithm datetime


【解决方案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));

注意 - 如果开始和结束日期相同,您需要转到另一个分支:

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

这里的常量:

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

关于最小值/最大值的说明:

  1. max 为 0 将所有负值更改为 0,但保留正值,例如:最后一天我们在 2 点结束(即 0 个工作时间),所以 Math.Max(0, 2-3==-1 ) = 0
  2. 将工作时间限制为每天 14 小时所需的分钟数,例如:昨天我们完成了 20 小时,Math.min(14, 20-3==17) 将只给我们 14 小时

这是基于您的代码的工作实现:

    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
        }
    }

【讨论】:

  • 嗨 Lashane,感谢您的回答,我根据您的回答做了另一个回答。你能看看它似乎不能正常工作。 :(
  • @SteffenMangold 你有一个错误的标准
【解决方案2】:

我为 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 };

这给了我这个结果:

这是更新后的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;
        }
    }
}

【讨论】:

  • 您好,感谢您的回答。 linq 的好解决方案。但我认为它的性能要慢得多。
  • @SteffenMangold - 认真的吗?性能是个问题?这一切都发生在记忆中。除非您要处理数十万条记录,否则这应该非常快。重用和可维护性应该是王道。
  • 这正是我正在做的,对不起。我必须证明来自太阳能发电厂的数十万条记录,所以性能对我来说是一个重要的事实。确切地说,目前有 49.540.066 条记录。
  • @SteffenMangold - 很公平。这是很多记录。尽管如此,我只是做了一个测试并处理了 49,540,066 条记录 - 处理时间不到 7 分钟。我怀疑,如果您正在运行批处理,从数据库中提取记录,以及内存开销等,那么 7 分钟将只是整个处理时间的一小部分。我怀疑这段代码的性能会是一个重要因素。
  • @SteffenMangold - 你选择不同的答案我没有问题。我只是担心说我的解决方案“性能要慢得多”实际上有点误导。
【解决方案3】:

根据 Lashanes 的回答,我更改了代码以返回精确的时间戳值...

代码

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

现在可以按我的意愿工作... :)

【讨论】:

    猜你喜欢
    • 2023-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-20
    • 2022-11-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多