2

I am trying to convert default timezone datetime to localtime and round the time to 15min slots in Django views. I have the following roundTime function:

def roundTime(dt=None, dateDelta=timedelta(minutes=1)):
    """Round a datetime object to a multiple of a timedelta
    dt : datetime.datetime object, default now.
    dateDelta : timedelta object, we round to a multiple of this, default 1 minute.
    Author: Thierry Husson 2012 - Use it as you want but don't blame me.
            Stijn Nevens 2014 - Changed to use only datetime objects as variables
    """
    roundTo = dateDelta.total_seconds()

    if dt == None:
        dt = datetime.now()
    seconds = (dt - dt.min).seconds
    # // is a floor division, not a comment on following line:
    rounding = (seconds+roundTo/2) // roundTo * roundTo
    return dt + timedelta(0,rounding-seconds,-dt.microsecond)

And here is what I have tried so far:

mytime = roundTime(datetime.now(),timedelta(minutes=15)).strftime('%H:%M:%S') #works OK
mytime = datetime.strptime(str(mytime), '%H:%M:%S') #works OK
mytime = timezone.localtime(mytime) 

But the last line gives me this error:

error: astimezone() cannot be applied to a naive datetime

And when I use:

local_time = timezone.localtime(timezone.now()) 

I do get the correct local time, but for some reason I'm unable to round the time by doing:

mytime = roundTime(local_time,timedelta(minutes=15)).strftime('%H:%M:%S') 

which works with the datetime.now() above.

I was able to come up with this not pretty but working code:

mytime = timezone.localtime(timezone.now())
mytime = datetime.strftime(mytime, '%Y-%m-%d %H:%M:%S')
mytime = datetime.strptime(str(mytime), '%Y-%m-%d %H:%M:%S')
mytime = roundTime(mytime,timedelta(minutes=15)).strftime('%H:%M:%S')
mytime = datetime.strptime(str(mytime), '%H:%M:%S')

Is there a better solution?

Djizeus
  • 4,161
  • 1
  • 24
  • 42
WayBehind
  • 1,607
  • 3
  • 39
  • 58

2 Answers2

3

The roundTime function that you are using does not work with timezone-aware dates. To support it, you could modify it like this:

def roundTime(dt=None, dateDelta=timedelta(minutes=1)):
    """Round a datetime object to a multiple of a timedelta
    dt : datetime.datetime object, default now.
    dateDelta : timedelta object, we round to a multiple of this, default 1 minute.
    Author: Thierry Husson 2012 - Use it as you want but don't blame me.
            Stijn Nevens 2014 - Changed to use only datetime objects as variables
    """
    roundTo = dateDelta.total_seconds()

    if dt == None : 
        dt = datetime.now()
    #Make sure dt and datetime.min have the same timezone
    tzmin = dt.min.replace(tzinfo=dt.tzinfo)

    seconds = (dt - tzmin).seconds
    # // is a floor division, not a comment on following line:
    rounding = (seconds+roundTo/2) // roundTo * roundTo
    return dt + timedelta(0,rounding-seconds,-dt.microsecond)

That way the function works with both naive and TZ-aware dates. Then you can proceed as in your second attempt:

local_time = timezone.localtime(timezone.now()) 
mytime = roundTime(local_time, timedelta(minutes=15))
Djizeus
  • 4,161
  • 1
  • 24
  • 42
  • Thank you for the help. I did not want to mess with he `roundTime` function as is is being used elsewhere as well. Perhaps I can do another version for the time zone needs. Thanks again for your help! – WayBehind Oct 07 '15 at 20:38
  • This new version will behave as before in the rest of your code, it just supports a new use case that you were not using before (you can be sure that you were not using it as you would have got an error) – Djizeus Oct 07 '15 at 20:40
  • @WayBehind: the rounding may cross a DST boundary and therefore [the attached tzinfo should be updated](http://stackoverflow.com/a/33015258/4279). – jfs Oct 08 '15 at 12:04
2

To round a timezone-aware datetime object, make it a naive datetime object, round it, and attach the correct time zone info for the rounded time:

from datetime import timedelta
from django.utils import timezone

def round_time(dt=None, delta=timedelta(minutes=1)):
    if dt is None:
        dt = timezone.localtime(timezone.now()) # assume USE_TZ=True
    tzinfo, is_dst = dt.tzinfo, bool(dt.dst())
    dt = dt.replace(tzinfo=None)
    f = delta.total_seconds()
    rounded_ordinal_seconds = f * round((dt - dt.min).total_seconds() / f)
    rounded_dt = dt.min + timedelta(seconds=rounded_ordinal_seconds)
    localize = getattr(tzinfo, 'localize', None)
    if localize:
        rounded_dt = localize(rounded_dt, is_dst=is_dst)
    else:
        rounded_dt = rounded_dt.replace(tzinfo=tzinfo)
    return rounded_dt

To avoid floating point issues, all computations could be rewritten using integer microseconds (dt.resolution).

Example:

>>> round_time(delta=timedelta(minutes=15))
Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670