2017-08-12 23:07:16 +02:00
|
|
|
from datetime import timedelta, datetime
|
2017-08-09 09:14:57 +02:00
|
|
|
from statistics import mean
|
2017-08-01 20:57:15 +02:00
|
|
|
|
|
|
|
SCALES = [
|
|
|
|
('minutes', timedelta(minutes=1)),
|
|
|
|
('hours', timedelta(hours=1)),
|
|
|
|
('days', timedelta(days=1)),
|
2017-08-07 14:05:38 +02:00
|
|
|
('weeks', timedelta(days=7)),
|
2017-08-09 09:14:57 +02:00
|
|
|
('months', timedelta(days=
|
|
|
|
# you, a fool: a month is 30 days
|
|
|
|
# me, wise:
|
|
|
|
mean((31,
|
|
|
|
mean((29 if year % 400 == 0
|
2017-08-09 09:22:34 +02:00
|
|
|
or (year % 100 != 0 and year % 4 == 0)
|
2017-08-09 09:14:57 +02:00
|
|
|
else 28
|
|
|
|
for year in range(400)))
|
|
|
|
,31,30,31,30,31,31,30,31,30,31))
|
|
|
|
)),
|
2017-08-09 09:19:18 +02:00
|
|
|
('years', timedelta(days=
|
|
|
|
# you, a fool: ok. a year is 365.25 days. happy?
|
|
|
|
# me, wise: absolutely not
|
|
|
|
mean((366 if year % 400 == 0
|
2017-08-09 09:22:34 +02:00
|
|
|
or (year % 100 != 0 and year % 4 == 0)
|
2017-08-09 09:19:18 +02:00
|
|
|
else 365
|
|
|
|
for year in range(400)))
|
|
|
|
)),
|
2017-08-01 20:57:15 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
def decompose_interval(attrname):
|
|
|
|
scales = [scale[1] for scale in SCALES]
|
|
|
|
scales.reverse()
|
|
|
|
|
|
|
|
def decorator(cls):
|
|
|
|
scl_name = '{}_scale'.format(attrname)
|
|
|
|
sig_name = '{}_significand'.format(attrname)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def scale(self):
|
2017-08-01 21:15:58 +02:00
|
|
|
|
|
|
|
if getattr(self, attrname) == timedelta(0):
|
2017-08-02 01:35:09 +02:00
|
|
|
return timedelta(minutes=1)
|
2017-08-01 21:15:58 +02:00
|
|
|
|
2017-08-01 20:57:15 +02:00
|
|
|
for m in scales:
|
|
|
|
if getattr(self, attrname) % m == timedelta(0):
|
|
|
|
return m
|
|
|
|
|
2017-08-02 01:35:09 +02:00
|
|
|
return timedelta(minutes=1)
|
2017-08-01 20:57:15 +02:00
|
|
|
|
|
|
|
@scale.setter
|
|
|
|
def scale(self, value):
|
|
|
|
if(type(value) != timedelta):
|
|
|
|
value = timedelta(seconds=float(value))
|
2017-08-09 09:14:42 +02:00
|
|
|
setattr(self, attrname, max(1, getattr(self, sig_name)) * value)
|
2017-08-01 20:57:15 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def significand(self):
|
|
|
|
return int(getattr(self, attrname) / getattr(self, scl_name))
|
|
|
|
|
|
|
|
@significand.setter
|
|
|
|
def significand(self, value):
|
2017-08-07 16:33:35 +02:00
|
|
|
if type(value) == str and value.strip() == '':
|
|
|
|
value = 0
|
|
|
|
|
2017-08-07 14:05:38 +02:00
|
|
|
try:
|
|
|
|
value = int(value)
|
|
|
|
assert value >= 0
|
2017-08-07 16:26:25 +02:00
|
|
|
except (ValueError, AssertionError) as e:
|
|
|
|
raise ValueError("Incorrect time interval", e)
|
2017-08-07 14:05:38 +02:00
|
|
|
setattr(self, attrname, value * getattr(self, scl_name))
|
2017-08-01 20:57:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
setattr(cls, scl_name, scale)
|
|
|
|
setattr(cls, sig_name, significand)
|
|
|
|
|
|
|
|
return cls
|
|
|
|
|
|
|
|
return decorator
|
2017-08-12 23:07:16 +02:00
|
|
|
|
|
|
|
def relative(interval):
|
|
|
|
# special cases
|
|
|
|
if interval > timedelta(seconds=-15) and interval < timedelta(0):
|
|
|
|
return "just now"
|
|
|
|
elif interval > timedelta(0) and interval < timedelta(seconds=15):
|
|
|
|
return "in a few seconds"
|
|
|
|
else:
|
|
|
|
output = None
|
|
|
|
for name, scale in reversed(SCALES):
|
|
|
|
if abs(interval) > scale:
|
|
|
|
value = abs(interval) // scale
|
|
|
|
output = '{} {}'.format(value, name)
|
|
|
|
if value == 1:
|
|
|
|
output = output[:-1]
|
|
|
|
break
|
|
|
|
if not output:
|
|
|
|
output = '{} seconds'.format(abs(interval).seconds)
|
|
|
|
if interval > timedelta(0):
|
|
|
|
return 'in {}'.format(output)
|
|
|
|
else:
|
|
|
|
return '{} ago'.format(output)
|
|
|
|
|
|
|
|
def relnow(time):
|
|
|
|
return relative(time - datetime.now())
|