4
\$\begingroup\$

I have this ordered list of datetimes that comprises of 30 minute slots:

availability = [
    datetime.datetime(2010, 1, 1, 9, 0), 
    datetime.datetime(2010, 1, 1, 9, 30),
    datetime.datetime(2010, 1, 1, 10, 0), 
    datetime.datetime(2010, 1, 1, 10, 30), 
    datetime.datetime(2010, 1, 1, 13, 0), # gap
    datetime.datetime(2010, 1, 1, 13, 30), 
    datetime.datetime(2010, 1, 1, 15, 30), # gap
    datetime.datetime(2010, 1, 1, 16, 0), 
    datetime.datetime(2010, 1, 1, 16, 30)
]

And I would to split the ones with time gaps in between into a dictionary with start and end values, like so:

[
    {'start': datetime.datetime(2010, 1, 1, 9, 0), 'end': datetime.datetime(2010, 1, 1, 10, 30)},
    {'start': datetime.datetime(2010, 1, 1, 13, 0), 'end': datetime.datetime(2010, 1, 1, 13, 30)},         
    {'start': datetime.datetime(2010, 1, 1, 15, 30), 'end': datetime.datetime(2010, 1, 1, 16, 30)}
]

So I threw this snippet of code together that does exactly that.

result = []
row = {}
for index, date in enumerate(available):
    exists = False
    if not row:
        row['start'] = date
    try:
        if date + timedelta(minutes = 30) != available[index + 1]:
            exists = True
    except IndexError:
        exists = True
    if exists:
        row['end'] = date
        result.append(row)
        row = {}

But needless to say this is super ugly and there just has to be a better, shorter and more elegant way to do this.

\$\endgroup\$
  • \$\begingroup\$ @Peilonrayz in the results array they are not present. I am only interested in the date spans. \$\endgroup\$ – dan-klasson Jul 12 '17 at 20:29
  • \$\begingroup\$ Thank you, I noticed what you meant by 'gap' after posting that comment. And so I deleted it, :) \$\endgroup\$ – Peilonrayz Jul 12 '17 at 20:31
5
\$\begingroup\$

It is possible to utilize itertools.groupby() using indexes to calculate if there is a gap:

import datetime
from datetime import timedelta
from functools import partial
from itertools import groupby
from operator import itemgetter


def is_consecutive(start_date, item):
    """A grouping key function that highlights the gaps in a list of datetimes with a 30 minute interval."""
    index, value = item
    return (start_date + index * timedelta(minutes=30)) - value


available = [
    datetime.datetime(2010, 1, 1, 9, 0),
    datetime.datetime(2010, 1, 1, 9, 30),
    datetime.datetime(2010, 1, 1, 10, 0),
    datetime.datetime(2010, 1, 1, 10, 30),
    datetime.datetime(2010, 1, 1, 13, 0),  # gap
    datetime.datetime(2010, 1, 1, 13, 30),
    datetime.datetime(2010, 1, 1, 15, 30),  # gap
    datetime.datetime(2010, 1, 1, 16, 0),
    datetime.datetime(2010, 1, 1, 16, 30)
]

start_date = available[0]

result = []
for _, group in groupby(enumerate(available), key=partial(is_consecutive, start_date)):
    current_range = list(map(itemgetter(1), group))

    result.append({
        'start': current_range[0],
        'end': current_range[-1]
    })
print(result)

Prints:

[
    {'start': datetime.datetime(2010, 1, 1, 9, 0), 'end': datetime.datetime(2010, 1, 1, 10, 30)}, 
    {'start': datetime.datetime(2010, 1, 1, 13, 0), 'end': datetime.datetime(2010, 1, 1, 13, 30)}, 
    {'start': datetime.datetime(2010, 1, 1, 15, 30), 'end': datetime.datetime(2010, 1, 1, 16, 30)}
]
\$\endgroup\$
  • 1
    \$\begingroup\$ I'd never have thought of this. Sweet use of groupby. \$\endgroup\$ – Peilonrayz Jul 12 '17 at 21:06
  • 1
    \$\begingroup\$ This is definitely elegant, but bordering on complex. \$\endgroup\$ – dan-klasson Jul 13 '17 at 4:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.