datatracker/timedelta/helpers.py

555 lines
18 KiB
Python

from __future__ import division
import re
import datetime
from decimal import Decimal
from django.utils import six
STRFDATETIME = re.compile('([dgGhHis])')
STRFDATETIME_REPL = lambda x: '%%(%s)s' % x.group()
def nice_repr(timedelta, display="long", sep=", "):
"""
Turns a datetime.timedelta object into a nice string repr.
display can be "sql", "iso8601", "minimal", "short" or "long" [default].
>>> from datetime import timedelta as td
>>> nice_repr(td(days=1, hours=2, minutes=3, seconds=4))
'1 day, 2 hours, 3 minutes, 4 seconds'
>>> nice_repr(td(days=1, seconds=1), "minimal")
'1d, 1s'
>>> nice_repr(datetime.timedelta(days=1))
'1 day'
>>> nice_repr(datetime.timedelta(days=0))
'0 seconds'
>>> nice_repr(datetime.timedelta(seconds=1))
'1 second'
>>> nice_repr(datetime.timedelta(seconds=10))
'10 seconds'
>>> nice_repr(datetime.timedelta(seconds=30))
'30 seconds'
>>> nice_repr(datetime.timedelta(seconds=60))
'1 minute'
>>> nice_repr(datetime.timedelta(seconds=150))
'2 minutes, 30 seconds'
>>> nice_repr(datetime.timedelta(seconds=1800))
'30 minutes'
>>> nice_repr(datetime.timedelta(seconds=3600))
'1 hour'
>>> nice_repr(datetime.timedelta(seconds=3601))
'1 hour, 1 second'
>>> nice_repr(datetime.timedelta(seconds=19800))
'5 hours, 30 minutes'
>>> nice_repr(datetime.timedelta(seconds=91800))
'1 day, 1 hour, 30 minutes'
>>> nice_repr(datetime.timedelta(seconds=302400))
'3 days, 12 hours'
Tests for handling zero:
>>> nice_repr(td(seconds=0), 'minimal')
'0s'
>>> nice_repr(td(seconds=0), 'short')
'0 sec'
>>> nice_repr(td(seconds=0), 'long')
'0 seconds'
"""
assert isinstance(timedelta, datetime.timedelta), "First argument must be a timedelta."
result = []
weeks = int(timedelta.days / 7)
days = timedelta.days % 7
hours = int(timedelta.seconds / 3600)
minutes = int((timedelta.seconds % 3600) / 60)
seconds = timedelta.seconds % 60
if display == "sql":
days += weeks * 7
return "%i %02i:%02i:%02i" % (days, hours, minutes, seconds)
elif display == "iso8601":
return iso8601_repr(timedelta)
elif display == 'minimal':
words = ["w", "d", "h", "m", "s"]
elif display == 'short':
words = [" wks", " days", " hrs", " min", " sec"]
elif display == 'long':
words = [" weeks", " days", " hours", " minutes", " seconds"]
else:
# Use django template-style formatting.
# Valid values are:
# d,g,G,h,H,i,s
return STRFDATETIME.sub(STRFDATETIME_REPL, display) % {
'd': days,
'g': hours,
'G': hours if hours > 9 else '0%s' % hours,
'h': hours,
'H': hours if hours > 9 else '0%s' % hours,
'i': minutes if minutes > 9 else '0%s' % minutes,
's': seconds if seconds > 9 else '0%s' % seconds
}
values = [weeks, days, hours, minutes, seconds]
for i in range(len(values)):
if values[i]:
if values[i] == 1 and len(words[i]) > 1:
result.append("%i%s" % (values[i], words[i].rstrip('s')))
else:
result.append("%i%s" % (values[i], words[i]))
# values with less than one second, which are considered zeroes
if len(result) == 0:
# display as 0 of the smallest unit
result.append('0%s' % (words[-1]))
return sep.join(result)
def iso8601_repr(timedelta, format=None):
"""
Represent a timedelta as an ISO8601 duration.
http://en.wikipedia.org/wiki/ISO_8601#Durations
>>> from datetime import timedelta as td
>>> iso8601_repr(td(days=1, hours=2, minutes=3, seconds=4))
'P1DT2H3M4S'
>>> iso8601_repr(td(hours=1, minutes=10, seconds=20), 'alt')
'PT01:10:20'
"""
years = int(timedelta.days / 365)
weeks = int((timedelta.days % 365) / 7)
days = timedelta.days % 7
hours = int(timedelta.seconds / 3600)
minutes = int((timedelta.seconds % 3600) / 60)
seconds = timedelta.seconds % 60
if format == 'alt':
if years or weeks or days:
raise ValueError('Does not support alt format for durations > 1 day')
return 'PT{0:02d}:{1:02d}:{2:02d}'.format(hours, minutes, seconds)
formatting = (
('P', (
('Y', years),
('W', weeks),
('D', days),
)),
('T', (
('H', hours),
('M', minutes),
('S', seconds),
)),
)
result = []
for category, subcats in formatting:
result += category
for format, value in subcats:
if value:
result.append('%d%c' % (value, format))
if result[-1] == 'T':
result = result[:-1]
return "".join(result)
def parse(string):
"""
Parse a string into a timedelta object.
>>> parse("1 day")
datetime.timedelta(1)
>>> parse("2 days")
datetime.timedelta(2)
>>> parse("1 d")
datetime.timedelta(1)
>>> parse("1 hour")
datetime.timedelta(0, 3600)
>>> parse("1 hours")
datetime.timedelta(0, 3600)
>>> parse("1 hr")
datetime.timedelta(0, 3600)
>>> parse("1 hrs")
datetime.timedelta(0, 3600)
>>> parse("1h")
datetime.timedelta(0, 3600)
>>> parse("1wk")
datetime.timedelta(7)
>>> parse("1 week")
datetime.timedelta(7)
>>> parse("1 weeks")
datetime.timedelta(7)
>>> parse("2 wks")
datetime.timedelta(14)
>>> parse("1 sec")
datetime.timedelta(0, 1)
>>> parse("1 secs")
datetime.timedelta(0, 1)
>>> parse("1 s")
datetime.timedelta(0, 1)
>>> parse("1 second")
datetime.timedelta(0, 1)
>>> parse("1 seconds")
datetime.timedelta(0, 1)
>>> parse("1 minute")
datetime.timedelta(0, 60)
>>> parse("1 min")
datetime.timedelta(0, 60)
>>> parse("1 m")
datetime.timedelta(0, 60)
>>> parse("1 minutes")
datetime.timedelta(0, 60)
>>> parse("1 mins")
datetime.timedelta(0, 60)
>>> parse("2 ws")
Traceback (most recent call last):
...
TypeError: '2 ws' is not a valid time interval
>>> parse("2 ds")
Traceback (most recent call last):
...
TypeError: '2 ds' is not a valid time interval
>>> parse("2 hs")
Traceback (most recent call last):
...
TypeError: '2 hs' is not a valid time interval
>>> parse("2 ms")
Traceback (most recent call last):
...
TypeError: '2 ms' is not a valid time interval
>>> parse("2 ss")
Traceback (most recent call last):
...
TypeError: '2 ss' is not a valid time interval
>>> parse("")
Traceback (most recent call last):
...
TypeError: '' is not a valid time interval
>>> parse("1.5 days")
datetime.timedelta(1, 43200)
>>> parse("3 weeks")
datetime.timedelta(21)
>>> parse("4.2 hours")
datetime.timedelta(0, 15120)
>>> parse(".5 hours")
datetime.timedelta(0, 1800)
>>> parse(" hours")
Traceback (most recent call last):
...
TypeError: 'hours' is not a valid time interval
>>> parse("1 hour, 5 mins")
datetime.timedelta(0, 3900)
>>> parse("-2 days")
datetime.timedelta(-2)
>>> parse("-1 day 0:00:01")
datetime.timedelta(-1, 1)
>>> parse("-1 day, -1:01:01")
datetime.timedelta(-2, 82739)
>>> parse("-1 weeks, 2 days, -3 hours, 4 minutes, -5 seconds")
datetime.timedelta(-5, 11045)
>>> parse("0 seconds")
datetime.timedelta(0)
>>> parse("0 days")
datetime.timedelta(0)
>>> parse("0 weeks")
datetime.timedelta(0)
>>> zero = datetime.timedelta(0)
>>> parse(nice_repr(zero))
datetime.timedelta(0)
>>> parse(nice_repr(zero, 'minimal'))
datetime.timedelta(0)
>>> parse(nice_repr(zero, 'short'))
datetime.timedelta(0)
>>> parse(' 50 days 00:00:00 ')
datetime.timedelta(50)
"""
string = string.strip()
if string == "":
raise TypeError("'%s' is not a valid time interval" % string)
# This is the format we get from sometimes Postgres, sqlite,
# and from serialization
d = re.match(r'^((?P<days>[-+]?\d+) days?,? )?(?P<sign>[-+]?)(?P<hours>\d+):'
r'(?P<minutes>\d+)(:(?P<seconds>\d+(\.\d+)?))?$',
six.text_type(string))
if d:
d = d.groupdict(0)
if d['sign'] == '-':
for k in 'hours', 'minutes', 'seconds':
d[k] = '-' + d[k]
d.pop('sign', None)
else:
# This is the more flexible format
d = re.match(
r'^((?P<weeks>-?((\d*\.\d+)|\d+))\W*w((ee)?(k(s)?)?)(,)?\W*)?'
r'((?P<days>-?((\d*\.\d+)|\d+))\W*d(ay(s)?)?(,)?\W*)?'
r'((?P<hours>-?((\d*\.\d+)|\d+))\W*h(ou)?(r(s)?)?(,)?\W*)?'
r'((?P<minutes>-?((\d*\.\d+)|\d+))\W*m(in(ute)?(s)?)?(,)?\W*)?'
r'((?P<seconds>-?((\d*\.\d+)|\d+))\W*s(ec(ond)?(s)?)?)?\W*$',
six.text_type(string))
if not d:
raise TypeError("'%s' is not a valid time interval" % string)
d = d.groupdict(0)
return datetime.timedelta(**dict(( (k, float(v)) for k,v in d.items())))
def divide(obj1, obj2, as_float=False):
"""
Allows for the division of timedeltas by other timedeltas, or by
floats/Decimals
>>> from datetime import timedelta as td
>>> divide(td(1), td(1))
1
>>> divide(td(2), td(1))
2
>>> divide(td(32), 16)
datetime.timedelta(2)
>>> divide(datetime.timedelta(1), datetime.timedelta(hours=6))
4
>>> divide(datetime.timedelta(2), datetime.timedelta(3))
0
>>> divide(datetime.timedelta(8), datetime.timedelta(3), as_float=True)
2.6666666666666665
>>> divide(datetime.timedelta(8), 2.0)
datetime.timedelta(4)
>>> divide(datetime.timedelta(8), 2, as_float=True)
Traceback (most recent call last):
...
AssertionError: as_float=True is inappropriate when dividing timedelta by a number.
"""
assert isinstance(obj1, datetime.timedelta), "First argument must be a timedelta."
assert isinstance(obj2, (datetime.timedelta, int, float, Decimal)), "Second argument must be a timedelta or number"
sec1 = obj1.days * 86400 + obj1.seconds
if isinstance(obj2, datetime.timedelta):
sec2 = obj2.days * 86400 + obj2.seconds
value = sec1 / sec2
if as_float:
return value
return int(value)
else:
if as_float:
assert None, "as_float=True is inappropriate when dividing timedelta by a number."
secs = sec1 / obj2
if isinstance(secs, Decimal):
secs = float(secs)
return datetime.timedelta(seconds=secs)
def modulo(obj1, obj2):
"""
Allows for remainder division of timedelta by timedelta or integer.
>>> from datetime import timedelta as td
>>> modulo(td(5), td(2))
datetime.timedelta(1)
>>> modulo(td(6), td(3))
datetime.timedelta(0)
>>> modulo(td(15), 4 * 3600 * 24)
datetime.timedelta(3)
>>> modulo(5, td(1))
Traceback (most recent call last):
...
AssertionError: First argument must be a timedelta.
>>> modulo(td(1), 2.8)
Traceback (most recent call last):
...
AssertionError: Second argument must be a timedelta or int.
"""
assert isinstance(obj1, datetime.timedelta), "First argument must be a timedelta."
assert isinstance(obj2, (datetime.timedelta, int)), "Second argument must be a timedelta or int."
sec1 = obj1.days * 86400 + obj1.seconds
if isinstance(obj2, datetime.timedelta):
sec2 = obj2.days * 86400 + obj2.seconds
return datetime.timedelta(seconds=sec1 % sec2)
else:
return datetime.timedelta(seconds=(sec1 % obj2))
def percentage(obj1, obj2):
"""
What percentage of obj2 is obj1? We want the answer as a float.
>>> percentage(datetime.timedelta(4), datetime.timedelta(2))
200.0
>>> percentage(datetime.timedelta(2), datetime.timedelta(4))
50.0
"""
assert isinstance(obj1, datetime.timedelta), "First argument must be a timedelta."
assert isinstance(obj2, datetime.timedelta), "Second argument must be a timedelta."
return divide(obj1 * 100, obj2, as_float=True)
def decimal_percentage(obj1, obj2):
"""
>>> decimal_percentage(datetime.timedelta(4), datetime.timedelta(2))
Decimal('200.0')
>>> decimal_percentage(datetime.timedelta(2), datetime.timedelta(4))
Decimal('50.0')
"""
return Decimal(str(percentage(obj1, obj2)))
def multiply(obj, val):
"""
Allows for the multiplication of timedeltas by float values.
>>> multiply(datetime.timedelta(seconds=20), 1.5)
datetime.timedelta(0, 30)
>>> multiply(datetime.timedelta(1), 2.5)
datetime.timedelta(2, 43200)
>>> multiply(datetime.timedelta(1), 3)
datetime.timedelta(3)
>>> multiply(datetime.timedelta(1), Decimal("5.5"))
datetime.timedelta(5, 43200)
>>> multiply(datetime.date.today(), 2.5)
Traceback (most recent call last):
...
AssertionError: First argument must be a timedelta.
>>> multiply(datetime.timedelta(1), "2")
Traceback (most recent call last):
...
AssertionError: Second argument must be a number.
"""
assert isinstance(obj, datetime.timedelta), "First argument must be a timedelta."
assert isinstance(val, (int, float, Decimal)), "Second argument must be a number."
sec = obj.days * 86400 + obj.seconds
sec *= val
if isinstance(sec, Decimal):
sec = float(sec)
return datetime.timedelta(seconds=sec)
def round_to_nearest(obj, timedelta):
"""
The obj is rounded to the nearest whole number of timedeltas.
obj can be a timedelta, datetime or time object.
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(1))
datetime.datetime(2012, 1, 1, 0, 0)
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(hours=1))
datetime.datetime(2012, 1, 1, 10, 0)
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(minutes=15))
datetime.datetime(2012, 1, 1, 9, 45)
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(minutes=1))
datetime.datetime(2012, 1, 1, 9, 43)
>>> td = datetime.timedelta(minutes=30)
>>> round_to_nearest(datetime.timedelta(minutes=0), td)
datetime.timedelta(0)
>>> round_to_nearest(datetime.timedelta(minutes=14), td)
datetime.timedelta(0)
>>> round_to_nearest(datetime.timedelta(minutes=15), td)
datetime.timedelta(0, 1800)
>>> round_to_nearest(datetime.timedelta(minutes=29), td)
datetime.timedelta(0, 1800)
>>> round_to_nearest(datetime.timedelta(minutes=30), td)
datetime.timedelta(0, 1800)
>>> round_to_nearest(datetime.timedelta(minutes=42), td)
datetime.timedelta(0, 1800)
>>> round_to_nearest(datetime.timedelta(hours=7, minutes=22), td)
datetime.timedelta(0, 27000)
>>> td = datetime.timedelta(minutes=15)
>>> round_to_nearest(datetime.timedelta(minutes=0), td)
datetime.timedelta(0)
>>> round_to_nearest(datetime.timedelta(minutes=14), td)
datetime.timedelta(0, 900)
>>> round_to_nearest(datetime.timedelta(minutes=15), td)
datetime.timedelta(0, 900)
>>> round_to_nearest(datetime.timedelta(minutes=29), td)
datetime.timedelta(0, 1800)
>>> round_to_nearest(datetime.timedelta(minutes=30), td)
datetime.timedelta(0, 1800)
>>> round_to_nearest(datetime.timedelta(minutes=42), td)
datetime.timedelta(0, 2700)
>>> round_to_nearest(datetime.timedelta(hours=7, minutes=22), td)
datetime.timedelta(0, 26100)
>>> td = datetime.timedelta(minutes=30)
>>> round_to_nearest(datetime.datetime(2010,1,1,9,22), td)
datetime.datetime(2010, 1, 1, 9, 30)
>>> round_to_nearest(datetime.datetime(2010,1,1,9,32), td)
datetime.datetime(2010, 1, 1, 9, 30)
>>> round_to_nearest(datetime.datetime(2010,1,1,9,42), td)
datetime.datetime(2010, 1, 1, 9, 30)
>>> round_to_nearest(datetime.time(0,20), td)
datetime.time(0, 30)
TODO: test with tzinfo (non-naive) datetimes/times.
"""
assert isinstance(obj, (datetime.datetime, datetime.timedelta, datetime.time)), "First argument must be datetime, time or timedelta."
assert isinstance(timedelta, datetime.timedelta), "Second argument must be a timedelta."
time_only = False
if isinstance(obj, datetime.timedelta):
counter = datetime.timedelta(0)
elif isinstance(obj, datetime.datetime):
counter = datetime.datetime.combine(obj.date(), datetime.time(0, tzinfo=obj.tzinfo))
elif isinstance(obj, datetime.time):
counter = datetime.datetime.combine(datetime.date.today(), datetime.time(0, tzinfo=obj.tzinfo))
obj = datetime.datetime.combine(datetime.date.today(), obj)
time_only = True
diff = abs(obj - counter)
while counter < obj:
old_diff = diff
counter += timedelta
diff = abs(obj - counter)
if counter == obj:
result = obj
elif diff <= old_diff:
result = counter
else:
result = counter - timedelta
if time_only:
return result.time()
else:
return result
def decimal_hours(timedelta, decimal_places=None):
"""
Return a decimal value of the number of hours that this timedelta
object refers to.
"""
hours = Decimal(timedelta.days*24) + Decimal(timedelta.seconds) / 3600
if decimal_places:
return hours.quantize(Decimal(str(10**-decimal_places)))
return hours
def week_containing(date):
if date.weekday():
date -= datetime.timedelta(date.weekday())
return date, date + datetime.timedelta(6)
try:
datetime.timedelta().total_seconds
def total_seconds(timedelta):
return timedelta.total_seconds()
except AttributeError:
def total_seconds(timedelta):
"""
Python < 2.7 does not have datetime.timedelta.total_seconds
"""
return timedelta.days * 86400 + timedelta.seconds
if __name__ == "__main__":
import doctest
doctest.testmod()