HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonModerate

Seconds between datestimes excluding weekends and evenings

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
excludingsecondsdatestimeseveningsbetweenandweekends

Problem

The basic idea here is that I want to measure the number of seconds between two python datetime objects. However, I only want to count hours between 8:00 and 17:00, as well as skipping weekends (saturday and sunday). This works, but I wondered if anyone had clever ideas to make it cleaner.

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

Solution


  1. Issues



Your code fails in the following corner cases:

-
a and b on the same day, for example:

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


-
a or b at the weekend, for example:

>>> a = datetime(2012, 11, 17, 8)
>>> a.weekday()
5          # Saturday
>>> seconds_between(a, a + timedelta(seconds = 100))
21700.0    # Expected 0


-
a after STOP_HOUR or b before START_HOUR, for example:

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


Also, you count the weekdays by looping over all the days between the start and end of the interval. That means that the computation time is proportional to the size of the interval:

>>> from timeit import timeit
>>> a = datetime(1, 1, 1)
>>> timeit(lambda:seconds_between(a, a + timedelta(days=999999)), number=1)
1.7254137992858887


For comparison, in this extreme case the revised code below is about 100,000 times faster:

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


The break even point is about 4 days:

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


  1. Improvements



barracel's answer has two very good ideas, which I adopted:

-
compute the sum in seconds rather than days;

-
add up whole days and subtract part days if necessary.

and I made the following additional improvements:

-
handle corner cases correctly;

-
run in constant time regardless of how far apart a and b are;

-
compute the sum as a timedelta object rather than an integer;

-
move common code out into functions for clarity;

-
docstrings!

  1. Revised code



from datetime import datetime, timedelta

def clamp(t, start, end):
    "Return `t` clamped to the range [`start`, `end`]."
    return max(start, min(end, t))

def day_part(t):
    "Return timedelta between midnight and `t`."
    return t - t.replace(hour = 0, minute = 0, second = 0)

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
    days = (b - a).days + 1
    weeks = days // 7
    extra = (max(0, 5 - a.weekday()) + min(5, 1 + b.weekday())) % 5
    weekdays = weeks * 5 + extra
    total = office_day * weekdays
    if a.weekday() < 5:
        total -= clamp(day_part(a) - start, zero, office_day)
    if b.weekday() < 5:
        total -= clamp(stop - day_part(b), zero, office_day)
    return total

Code Snippets

>>> a = datetime(2012, 11, 22, 8)
>>> a.weekday()
3          # Thursday
>>> seconds_between(a, a + timedelta(seconds = 100))
54100.0    # Expected 100
>>> a = datetime(2012, 11, 17, 8)
>>> a.weekday()
5          # Saturday
>>> seconds_between(a, a + timedelta(seconds = 100))
21700.0    # Expected 0
>>> a = datetime(2012, 11, 19, 23)
>>> a.weekday()
0          # Monday
>>> seconds_between(a, a + timedelta(hours = 2))
28800.0    # Expected 0
>>> from timeit import timeit
>>> a = datetime(1, 1, 1)
>>> timeit(lambda:seconds_between(a, a + timedelta(days=999999)), number=1)
1.7254137992858887
>>> timeit(lambda:office_time_between(a, a + timedelta(days=999999)), number=100000)
1.6366889476776123

Context

StackExchange Code Review Q#18053, answer score: 15

Revisions (0)

No revisions yet.