Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ceil a datetime to next quarter of an hour

Let's imagine this datetime

>>> import datetime
>>> dt = datetime.datetime(2012, 10, 25, 17, 32, 16)

I'd like to ceil it to the next quarter of hour, in order to get

datetime.datetime(2012, 10, 25, 17, 45)

I imagine something like

>>> quarter = datetime.timedelta(minutes=15)
>>> import math
>>> ceiled_dt = math.ceil(dt / quarter) * quarter

But of course, this does not work

like image 357
Pierre de LESPINAY Avatar asked Oct 25 '12 14:10

Pierre de LESPINAY


5 Answers

This one takes microseconds into account!

import math

def ceil_dt(dt):
    # how many secs have passed this hour
    nsecs = dt.minute*60 + dt.second + dt.microsecond*1e-6  
    # number of seconds to next quarter hour mark
    # Non-analytic (brute force is fun) way:  
    #   delta = next(x for x in xrange(0,3601,900) if x>=nsecs) - nsecs
    # analytic way:
    delta = math.ceil(nsecs / 900) * 900 - nsecs
    #time + number of seconds to quarter hour mark.
    return dt + datetime.timedelta(seconds=delta)

t1 = datetime.datetime(2017, 3, 6, 7, 0)
assert ceil_dt(t1) == t1

t2 = datetime.datetime(2017, 3, 6, 7, 1)
assert ceil_dt(t2) == datetime.datetime(2017, 3, 6, 7, 15)

t3 = datetime.datetime(2017, 3, 6, 7, 15)
assert ceil_dt(t3) == t3

t4 = datetime.datetime(2017, 3, 6, 7, 16)
assert ceil_dt(t4) == datetime.datetime(2017, 3, 6, 7, 30)

t5 = datetime.datetime(2017, 3, 6, 7, 30)
assert ceil_dt(t5) == t5

t6 = datetime.datetime(2017, 3, 6, 7, 31)
assert ceil_dt(t6) == datetime.datetime(2017, 3, 6, 7, 45)

t7 = datetime.datetime(2017, 3, 6, 7, 45)
assert ceil_dt(t7) == t7

t8 = datetime.datetime(2017, 3, 6, 7, 46)
assert ceil_dt(t8) == datetime.datetime(2017, 3, 6, 8, 0)

Explanation of delta:

  • 900 seconds is 15 minutes (a quarter of an hour sans leap seconds which I don't think datetime handles...)
  • nsecs / 900 is the number of quarter hour chunks that have transpired. Taking the ceil of this rounds up the number of quarter hour chunks.
  • Multiply the number of quarter hour chunks by 900 to figure out how many seconds have transpired in since the start of the hour after "rounding".
like image 105
mgilson Avatar answered Nov 15 '22 09:11

mgilson


@Mark Dickinson suggested the best formula so far:

def ceil_dt(dt, delta):
    return dt + (datetime.min - dt) % delta

In Python 3, for an arbitrary time delta (not just 15 minutes):

#!/usr/bin/env python3
import math
from datetime import datetime, timedelta

def ceil_dt(dt, delta):
    return datetime.min + math.ceil((dt - datetime.min) / delta) * delta

print(ceil_dt(datetime(2012, 10, 25, 17, 32, 16), timedelta(minutes=15)))
# -> 2012-10-25 17:45:00

To avoid intermediate floats, divmod() could be used:

def ceil_dt(dt, delta):
    q, r = divmod(dt - datetime.min, delta)
    return (datetime.min + (q + 1)*delta) if r else dt

Example:

>>> ceil_dt(datetime(2012, 10, 25, 17, 32, 16), timedelta(minutes=15))
datetime.datetime(2012, 10, 25, 17, 45)
>>> ceil_dt(datetime.min, datetime.resolution) 
datetime.datetime(1, 1, 1, 0, 0)
>>> ceil_dt(datetime.min, 2*datetime.resolution)
datetime.datetime(1, 1, 1, 0, 0)
>>> ceil_dt(datetime.max, datetime.resolution)
datetime.datetime(9999, 12, 31, 23, 59, 59, 999999)
>>> ceil_dt(datetime.max, 2*datetime.resolution)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in ceil_dt
OverflowError: date value out of range
>>> ceil_dt(datetime.min+datetime.resolution, datetime.resolution)
datetime.datetime(1, 1, 1, 0, 0, 0, 1)
>>> ceil_dt(datetime.min+datetime.resolution, 2*datetime.resolution)
datetime.datetime(1, 1, 1, 0, 0, 0, 2)
>>> ceil_dt(datetime.max-datetime.resolution, datetime.resolution)
datetime.datetime(9999, 12, 31, 23, 59, 59, 999998)
>>> ceil_dt(datetime.max-datetime.resolution, 2*datetime.resolution)
datetime.datetime(9999, 12, 31, 23, 59, 59, 999998)
>>> ceil_dt(datetime.max-2*datetime.resolution, datetime.resolution)
datetime.datetime(9999, 12, 31, 23, 59, 59, 999997)
>>> ceil_dt(datetime.max-2*datetime.resolution, 2*datetime.resolution)
datetime.datetime(9999, 12, 31, 23, 59, 59, 999998)
>>> ceil_dt(datetime.max-timedelta(1), datetime.resolution)
datetime.datetime(9999, 12, 30, 23, 59, 59, 999999)
>>> ceil_dt(datetime.max-timedelta(1), 2*datetime.resolution)
datetime.datetime(9999, 12, 31, 0, 0)
>>> ceil_dt(datetime.min, datetime.max-datetime.min)
datetime.datetime(1, 1, 1, 0, 0)
>>> ceil_dt(datetime.max, datetime.max-datetime.min)
datetime.datetime(9999, 12, 31, 23, 59, 59, 999999)
like image 15
jfs Avatar answered Nov 15 '22 10:11

jfs


def ceil(dt):
    if dt.minute % 15 or dt.second:
        return dt + datetime.timedelta(minutes = 15 - dt.minute % 15,
                                       seconds = -(dt.second % 60))
    else:
        return dt

This gives you:

>>> ceil(datetime.datetime(2012,10,25, 17,45))
datetime.datetime(2012, 10, 25, 17, 45)
>>> ceil(datetime.datetime(2012,10,25, 17,45,1))
datetime.datetime(2012, 10, 25, 18, 0)
>>> ceil(datetime.datetime(2012,12,31,23,59,0))
datetime.datetime(2013,1,1,0,0)
like image 9
Tim Pietzcker Avatar answered Nov 15 '22 08:11

Tim Pietzcker


You just need to calculate correct minutes and add them in datetime object after setting minutes, seconds to zero

import datetime

def quarter_datetime(dt):
    minute = (dt.minute//15+1)*15
    return dt.replace(minute=0, second=0)+datetime.timedelta(minutes=minute)

for minute in [12, 22, 35, 52]:
    print quarter_datetime(datetime.datetime(2012, 10, 25, 17, minute, 16))

It works for all cases:

2012-10-25 17:15:00
2012-10-25 17:30:00
2012-10-25 17:45:00
2012-10-25 18:00:00
like image 4
Anurag Uniyal Avatar answered Nov 15 '22 09:11

Anurag Uniyal


The formula proposed here by @Mark Dickinson worked beautifully, but I needed a solution that also handled timezones and Daylight Savings Time (DST).

Using pytz, I arrived at:

import pytz
from datetime import datetime, timedelta

def datetime_ceiling(dt, delta):
    # Preserve original timezone info
    original_tz = dt.tzinfo
    if original_tz:
        # If the original was timezone aware, translate to UTC.
        # This is necessary because datetime math does not take
        # DST into account, so first we normalize the datetime...
        dt = dt.astimezone(pytz.UTC)
        # ... and then make it timezone naive
        dt = dt.replace(tzinfo=None)
    # We only do math on a timezone naive object, which allows
    # us to pass naive objects directly to the function
    dt = dt + ((datetime.min - dt) % delta)
    if original_tz:
        # If the original was tz aware, we make the result aware...
        dt = pytz.UTC.localize(dt)
        # ... then translate it from UTC back its original tz.
        # This translation applies appropriate DST status.
        dt = dt.astimezone(original_tz)
    return dt

A nearly identical floor function can be made by changing one line of code:

def datetime_floor(dt, delta):
    ...
    dt = dt - ((datetime.min - dt) % delta)
    ...

The following datetime is three minutes before the transition from DST back to Standard Time (STD):

datetime.datetime(2020, 11, 1, 1, 57, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)

Assuming the above as dt, we can round down to the nearest five minute increment using our floor function:

>>> datetime_floor(dt, timedelta(minutes=5))
datetime.datetime(2020, 11, 1, 1, 55, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)

The timezone and relationship to DST is preserved. (The same would be true for the ceiling function.)

On this date DST will end at 2 am, at which point the time will "roll back" to 1am STD. If we use our ceiling function to round up from 1:57am DST, we should not end up at 2am DST, but rather at 1:00am STD, which is the result we get:

>>> datetime_ceiling(dt, timedelta(minutes=5))
datetime.datetime(2020, 11, 1, 1, 0, tzinfo=<DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>)
like image 1
Adam Lombard Avatar answered Nov 15 '22 10:11

Adam Lombard