这里的基本思想是我想测量两个python datetime对象之间的秒数。但是,我只想计算8:00到17:00之间的小时数,以及跳过周末(星期六和星期日)。这行得通,但是我想知道是否有人有聪明的主意可以使它更清洁。

START_HOUR = 8
STOP_HOUR = 17
KEEP = (STOP_HOUR - START_HOUR)/24.0

def seconds_between(a, b):

    weekend_seconds = 0

    current = a
    while current < b:
        current += timedelta(days = 1)
        if current.weekday() in (5,6):
            weekend_seconds += 24*60*60*KEEP



    a_stop_hour = datetime(a.year, a.month, a.day, STOP_HOUR)
    seconds = max(0, (a_stop_hour - a).total_seconds())
    b_stop_hour = datetime(b.year, b.month, b.day, STOP_HOUR)
    if b_stop_hour > b:
        b_stop_hour = datetime(b.year, b.month, b.day-1, STOP_HOUR)

    seconds += (b - b_stop_hour).total_seconds()

    return (b_stop_hour - a_stop_hour).total_seconds() * KEEP + seconds - weekend_seconds


评论

那其他假期呢?听起来好像排除周末意味着要排除所有空闲日。请注意,在阿拉伯国家/地区,星期五不是工作日,而是星期天。

@RolandIllig,很好,尽管我7年前的用例不会担心其他可能的假期。

#1 楼

1.问题

在以下特殊情况下,您的代码失败:



ab在同一天,例如:

>>> a = datetime(2012, 11, 22, 8)
>>> a.weekday()
3          # Thursday
>>> seconds_between(a, a + timedelta(seconds = 100))
54100.0    # Expected 100



a或周末的b,例如:
>>> a = datetime(2012, 11, 17, 8)
>>> a.weekday()
5          # Saturday
>>> seconds_between(a, a + timedelta(seconds = 100))
21700.0    # Expected 0



a STOP_HOUR之后的bSTART_HOUR之前的a,例如:

>>> a = datetime(2012, 11, 19, 23)
>>> a.weekday()
0          # Monday
>>> seconds_between(a, a + timedelta(hours = 2))
28800.0    # Expected 0



此外,您还可以通过循环计算开始日期和结束日期之间的所有天数来计算工作日间隔。这意味着计算时间与间隔的大小成比例: br />
>>> from timeit import timeit
>>> a = datetime(1, 1, 1)
>>> timeit(lambda:seconds_between(a, a + timedelta(days=999999)), number=1)
1.7254137992858887


收支平衡点大约是4天:

>>> timeit(lambda:office_time_between(a, a + timedelta(days=999999)), number=100000)
1.6366889476776123


2。改进

barracel的答案有两个很好的主意,我采纳了它们:


以秒为单位计算总和,而不是以天为单位;
将整天加起来并减去必要的工作日数。

,我还进行了以下其他改进:


正确处理拐角处的情况;
恒定时间运行,无论btimedelta相距多远;
将和计算为q4312079q对象而不是整数;
为了清楚起见将通用代码移出函数;
文档字符串!

3。修改后的代码

>>> timeit(lambda:seconds_between(a, a + timedelta(days=4)), number=100000)
1.5806620121002197
>>> timeit(lambda:office_time_between(a, a + timedelta(days=4)), number=100000)
1.5950188636779785


评论


\ $ \ begingroup \ $
在某些情况下,此代码已损坏,例如,如果您在同一周的星期一和星期五给它一个负的时间增量,则该代码将被破坏。问题是多余的计算,但我还没有解决方案。
\ $ \ endgroup \ $
– radman
17 Mar 16 '17在6:22

\ $ \ begingroup \ $
@radman对Gareth Rees错误的评论是由“额外”值引起的。当第一天在星期一开始并在星期五或星期六结束时,会发生此问题,因此需要进行相应调整。因此将Extra =(max(0,5-a.weekday())+ min(5,1 + b.weekday()))%5更改为a.weekday()== 0和(b.weekday() == 4或b.weekday()== 5):额外= 5其他:额外=(max(0,5-a.weekday())+ min(5,1 + b.weekday()))%5
\ $ \ endgroup \ $
– JK Seo
20 Jan 15'0:52



#2 楼

我认为使用生成器表达式+总和,这两个日期之间的初始计算看起来更清晰。如果您在一天中的几秒钟内思考几个小时,就更容易理解后验矫正

from datetime import datetime
from datetime import timedelta

START_HOUR = 8 * 60 * 60
STOP_HOUR = 17 * 60 * 60
KEEP = STOP_HOUR - START_HOUR

def seconds_between(a, b):
    days = (a + timedelta(x + 1) for x in xrange((b - a).days))
    total = sum(KEEP for day in days if day.weekday() < 5)

    aseconds = (a - a.replace(hour=0, minute=0, second=0)).seconds
    bseconds = (b - b.replace(hour=0, minute=0, second=0)).seconds

    if aseconds > START_HOUR:
        total -= min(KEEP, aseconds - START_HOUR)

    if bseconds < STOP_HOUR:
        total -= min(KEEP, STOP_HOUR - bseconds)

    return total


#3 楼

下面的代码是上述两种方法的混合体。我认为它应该适用于所有情况。不计入工作时间以外的工作。

from datetime import datetime
from datetime import timedelta

def adjust_hour_delta(t, start, stop):

    start_hour = start.seconds//3600
    end_hour = stop.seconds//3600
    zero = timedelta(0)

    if t - t.replace(hour = start_hour, minute = 0, second = 0) < zero:
        t = t.replace(hour = start_hour, minute = 0, second = 0)
    elif t - t.replace(hour = end_hour, minute = 0, second = 0) > zero:
        t = t.replace(hour = end_hour, minute = 0, second = 0)
    # Now get the delta
    delta = timedelta(hours=t.hour, minutes=t.minute, seconds = t.second)

    return delta    

def full_in_between_working_days(a, b):
    working = 0
    b = b - timedelta(days=1)
    while b.date() > a.date():
        if b.weekday() < 5:
            working += 1
        b = b - timedelta(days=1)
    return working

def office_time_between(a, b, start = timedelta(hours = 8),
                        stop = timedelta(hours = 17)):
    """
    Return the total office time between `a` and `b` as a timedelta
    object. Office time consists of weekdays from `start` to `stop`
    (default: 08:00 to 17:00).
    """
    zero = timedelta(0)
    assert(zero <= start <= stop <= timedelta(1))
    office_day = stop - start
    working_days = full_in_between_working_days(a, b)

    total = office_day * working_days
    # Calculate the time adusted deltas for the the start and end days
    a_delta = adjust_hour_delta(a, start, stop)
    b_delta = adjust_hour_delta(b, start, stop)


    if a.date() == b.date():
        # If this was a weekend, ignore
        if a.weekday() < 5:
            total = total + b_delta - a_delta
    else:
        # We now consider if the start day was a weekend
        if a.weekday() > 4:
            a_worked = zero
        else:
            a_worked = stop - a_delta
        # And if the end day was a weekend
        if b.weekday() > 4:
            b_worked = zero
        else:
            b_worked = b_delta - start
        total = total + a_worked + b_worked

    return total


评论


\ $ \ begingroup \ $
测试这两个日期,我得到1个工作日,应该是4:2018-02-26 18:07:17和2018-03-04 14:41:04
\ $ \ endgroup \ $
– oxtay
18 Mar 6 '18 at 18:52



\ $ \ begingroup \ $
您提出了替代解决方案,但尚未检查代码。请编辑以显示问题代码的哪些方面促使您编写此版本,以及对原始版本进行了哪些改进。也许(重新)阅读“如何回答”是值得的。
\ $ \ endgroup \ $
– Toby Speight
20年1月15日在8:10