480 lines
13 KiB
Python
480 lines
13 KiB
Python
from __future__ import absolute_import
|
|
from __future__ import division
|
|
|
|
from datetime import timedelta
|
|
|
|
import pendulum
|
|
|
|
from pendulum.utils._compat import PYPY
|
|
from pendulum.utils._compat import decode
|
|
|
|
from .constants import SECONDS_PER_DAY
|
|
from .constants import SECONDS_PER_HOUR
|
|
from .constants import SECONDS_PER_MINUTE
|
|
from .constants import US_PER_SECOND
|
|
|
|
|
|
def _divide_and_round(a, b):
|
|
"""divide a by b and round result to the nearest integer
|
|
|
|
When the ratio is exactly half-way between two integers,
|
|
the even integer is returned.
|
|
"""
|
|
# Based on the reference implementation for divmod_near
|
|
# in Objects/longobject.c.
|
|
q, r = divmod(a, b)
|
|
# round up if either r / b > 0.5, or r / b == 0.5 and q is odd.
|
|
# The expression r / b > 0.5 is equivalent to 2 * r > b if b is
|
|
# positive, 2 * r < b if b negative.
|
|
r *= 2
|
|
greater_than_half = r > b if b > 0 else r < b
|
|
if greater_than_half or r == b and q % 2 == 1:
|
|
q += 1
|
|
|
|
return q
|
|
|
|
|
|
class Duration(timedelta):
|
|
"""
|
|
Replacement for the standard timedelta class.
|
|
|
|
Provides several improvements over the base class.
|
|
"""
|
|
|
|
_y = None
|
|
_m = None
|
|
_w = None
|
|
_d = None
|
|
_h = None
|
|
_i = None
|
|
_s = None
|
|
_invert = None
|
|
|
|
def __new__(
|
|
cls,
|
|
days=0,
|
|
seconds=0,
|
|
microseconds=0,
|
|
milliseconds=0,
|
|
minutes=0,
|
|
hours=0,
|
|
weeks=0,
|
|
years=0,
|
|
months=0,
|
|
):
|
|
if not isinstance(years, int) or not isinstance(months, int):
|
|
raise ValueError("Float year and months are not supported")
|
|
|
|
self = timedelta.__new__(
|
|
cls,
|
|
days + years * 365 + months * 30,
|
|
seconds,
|
|
microseconds,
|
|
milliseconds,
|
|
minutes,
|
|
hours,
|
|
weeks,
|
|
)
|
|
|
|
# Intuitive normalization
|
|
total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY
|
|
self._total = total
|
|
|
|
m = 1
|
|
if total < 0:
|
|
m = -1
|
|
|
|
self._microseconds = round(total % m * 1e6)
|
|
self._seconds = abs(int(total)) % SECONDS_PER_DAY * m
|
|
|
|
_days = abs(int(total)) // SECONDS_PER_DAY * m
|
|
self._days = _days
|
|
self._remaining_days = abs(_days) % 7 * m
|
|
self._weeks = abs(_days) // 7 * m
|
|
self._months = months
|
|
self._years = years
|
|
|
|
return self
|
|
|
|
def total_minutes(self):
|
|
return self.total_seconds() / SECONDS_PER_MINUTE
|
|
|
|
def total_hours(self):
|
|
return self.total_seconds() / SECONDS_PER_HOUR
|
|
|
|
def total_days(self):
|
|
return self.total_seconds() / SECONDS_PER_DAY
|
|
|
|
def total_weeks(self):
|
|
return self.total_days() / 7
|
|
|
|
if PYPY:
|
|
|
|
def total_seconds(self):
|
|
days = 0
|
|
|
|
if hasattr(self, "_years"):
|
|
days += self._years * 365
|
|
|
|
if hasattr(self, "_months"):
|
|
days += self._months * 30
|
|
|
|
if hasattr(self, "_remaining_days"):
|
|
days += self._weeks * 7 + self._remaining_days
|
|
else:
|
|
days += self._days
|
|
|
|
return (
|
|
(days * SECONDS_PER_DAY + self._seconds) * US_PER_SECOND
|
|
+ self._microseconds
|
|
) / US_PER_SECOND
|
|
|
|
@property
|
|
def years(self):
|
|
return self._years
|
|
|
|
@property
|
|
def months(self):
|
|
return self._months
|
|
|
|
@property
|
|
def weeks(self):
|
|
return self._weeks
|
|
|
|
if PYPY:
|
|
|
|
@property
|
|
def days(self):
|
|
return self._years * 365 + self._months * 30 + self._days
|
|
|
|
@property
|
|
def remaining_days(self):
|
|
return self._remaining_days
|
|
|
|
@property
|
|
def hours(self):
|
|
if self._h is None:
|
|
seconds = self._seconds
|
|
self._h = 0
|
|
if abs(seconds) >= 3600:
|
|
self._h = (abs(seconds) // 3600 % 24) * self._sign(seconds)
|
|
|
|
return self._h
|
|
|
|
@property
|
|
def minutes(self):
|
|
if self._i is None:
|
|
seconds = self._seconds
|
|
self._i = 0
|
|
if abs(seconds) >= 60:
|
|
self._i = (abs(seconds) // 60 % 60) * self._sign(seconds)
|
|
|
|
return self._i
|
|
|
|
@property
|
|
def seconds(self):
|
|
return self._seconds
|
|
|
|
@property
|
|
def remaining_seconds(self):
|
|
if self._s is None:
|
|
self._s = self._seconds
|
|
self._s = abs(self._s) % 60 * self._sign(self._s)
|
|
|
|
return self._s
|
|
|
|
@property
|
|
def microseconds(self):
|
|
return self._microseconds
|
|
|
|
@property
|
|
def invert(self):
|
|
if self._invert is None:
|
|
self._invert = self.total_seconds() < 0
|
|
|
|
return self._invert
|
|
|
|
def in_weeks(self):
|
|
return int(self.total_weeks())
|
|
|
|
def in_days(self):
|
|
return int(self.total_days())
|
|
|
|
def in_hours(self):
|
|
return int(self.total_hours())
|
|
|
|
def in_minutes(self):
|
|
return int(self.total_minutes())
|
|
|
|
def in_seconds(self):
|
|
return int(self.total_seconds())
|
|
|
|
def in_words(self, locale=None, separator=" "):
|
|
"""
|
|
Get the current interval in words in the current locale.
|
|
|
|
Ex: 6 jours 23 heures 58 minutes
|
|
|
|
:param locale: The locale to use. Defaults to current locale.
|
|
:type locale: str
|
|
|
|
:param separator: The separator to use between each unit
|
|
:type separator: str
|
|
|
|
:rtype: str
|
|
"""
|
|
periods = [
|
|
("year", self.years),
|
|
("month", self.months),
|
|
("week", self.weeks),
|
|
("day", self.remaining_days),
|
|
("hour", self.hours),
|
|
("minute", self.minutes),
|
|
("second", self.remaining_seconds),
|
|
]
|
|
|
|
if locale is None:
|
|
locale = pendulum.get_locale()
|
|
|
|
locale = pendulum.locale(locale)
|
|
parts = []
|
|
for period in periods:
|
|
unit, count = period
|
|
if abs(count) > 0:
|
|
translation = locale.translation(
|
|
"units.{}.{}".format(unit, locale.plural(abs(count)))
|
|
)
|
|
parts.append(translation.format(count))
|
|
|
|
if not parts:
|
|
if abs(self.microseconds) > 0:
|
|
unit = "units.second.{}".format(locale.plural(1))
|
|
count = "{:.2f}".format(abs(self.microseconds) / 1e6)
|
|
else:
|
|
unit = "units.microsecond.{}".format(locale.plural(0))
|
|
count = 0
|
|
translation = locale.translation(unit)
|
|
parts.append(translation.format(count))
|
|
|
|
return decode(separator.join(parts))
|
|
|
|
def _sign(self, value):
|
|
if value < 0:
|
|
return -1
|
|
|
|
return 1
|
|
|
|
def as_timedelta(self):
|
|
"""
|
|
Return the interval as a native timedelta.
|
|
|
|
:rtype: timedelta
|
|
"""
|
|
return timedelta(seconds=self.total_seconds())
|
|
|
|
def __str__(self):
|
|
return self.in_words()
|
|
|
|
def __repr__(self):
|
|
rep = "{}(".format(self.__class__.__name__)
|
|
|
|
if self._years:
|
|
rep += "years={}, ".format(self._years)
|
|
|
|
if self._months:
|
|
rep += "months={}, ".format(self._months)
|
|
|
|
if self._weeks:
|
|
rep += "weeks={}, ".format(self._weeks)
|
|
|
|
if self._days:
|
|
rep += "days={}, ".format(self._remaining_days)
|
|
|
|
if self.hours:
|
|
rep += "hours={}, ".format(self.hours)
|
|
|
|
if self.minutes:
|
|
rep += "minutes={}, ".format(self.minutes)
|
|
|
|
if self.remaining_seconds:
|
|
rep += "seconds={}, ".format(self.remaining_seconds)
|
|
|
|
if self.microseconds:
|
|
rep += "microseconds={}, ".format(self.microseconds)
|
|
|
|
rep += ")"
|
|
|
|
return rep.replace(", )", ")")
|
|
|
|
def __add__(self, other):
|
|
if isinstance(other, timedelta):
|
|
return self.__class__(seconds=self.total_seconds() + other.total_seconds())
|
|
|
|
return NotImplemented
|
|
|
|
__radd__ = __add__
|
|
|
|
def __sub__(self, other):
|
|
if isinstance(other, timedelta):
|
|
return self.__class__(seconds=self.total_seconds() - other.total_seconds())
|
|
|
|
return NotImplemented
|
|
|
|
def __neg__(self):
|
|
return self.__class__(
|
|
years=-self._years,
|
|
months=-self._months,
|
|
weeks=-self._weeks,
|
|
days=-self._remaining_days,
|
|
seconds=-self._seconds,
|
|
microseconds=-self._microseconds,
|
|
)
|
|
|
|
def _to_microseconds(self):
|
|
return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds
|
|
|
|
def __mul__(self, other):
|
|
if isinstance(other, int):
|
|
return self.__class__(
|
|
years=self._years * other,
|
|
months=self._months * other,
|
|
seconds=self._total * other,
|
|
)
|
|
|
|
if isinstance(other, float):
|
|
usec = self._to_microseconds()
|
|
a, b = other.as_integer_ratio()
|
|
|
|
return self.__class__(0, 0, _divide_and_round(usec * a, b))
|
|
|
|
return NotImplemented
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __floordiv__(self, other):
|
|
if not isinstance(other, (int, timedelta)):
|
|
return NotImplemented
|
|
|
|
usec = self._to_microseconds()
|
|
if isinstance(other, timedelta):
|
|
return usec // other._to_microseconds()
|
|
|
|
if isinstance(other, int):
|
|
return self.__class__(
|
|
0,
|
|
0,
|
|
usec // other,
|
|
years=self._years // other,
|
|
months=self._months // other,
|
|
)
|
|
|
|
def __truediv__(self, other):
|
|
if not isinstance(other, (int, float, timedelta)):
|
|
return NotImplemented
|
|
|
|
usec = self._to_microseconds()
|
|
if isinstance(other, timedelta):
|
|
return usec / other._to_microseconds()
|
|
|
|
if isinstance(other, int):
|
|
return self.__class__(
|
|
0,
|
|
0,
|
|
_divide_and_round(usec, other),
|
|
years=_divide_and_round(self._years, other),
|
|
months=_divide_and_round(self._months, other),
|
|
)
|
|
|
|
if isinstance(other, float):
|
|
a, b = other.as_integer_ratio()
|
|
|
|
return self.__class__(
|
|
0,
|
|
0,
|
|
_divide_and_round(b * usec, a),
|
|
years=_divide_and_round(self._years * b, a),
|
|
months=_divide_and_round(self._months, other),
|
|
)
|
|
|
|
__div__ = __floordiv__
|
|
|
|
def __mod__(self, other):
|
|
if isinstance(other, timedelta):
|
|
r = self._to_microseconds() % other._to_microseconds()
|
|
|
|
return self.__class__(0, 0, r)
|
|
|
|
return NotImplemented
|
|
|
|
def __divmod__(self, other):
|
|
if isinstance(other, timedelta):
|
|
q, r = divmod(self._to_microseconds(), other._to_microseconds())
|
|
|
|
return q, self.__class__(0, 0, r)
|
|
|
|
return NotImplemented
|
|
|
|
|
|
Duration.min = Duration(days=-999999999)
|
|
Duration.max = Duration(
|
|
days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999
|
|
)
|
|
Duration.resolution = Duration(microseconds=1)
|
|
|
|
|
|
class AbsoluteDuration(Duration):
|
|
"""
|
|
Duration that expresses a time difference in absolute values.
|
|
"""
|
|
|
|
def __new__(
|
|
cls,
|
|
days=0,
|
|
seconds=0,
|
|
microseconds=0,
|
|
milliseconds=0,
|
|
minutes=0,
|
|
hours=0,
|
|
weeks=0,
|
|
years=0,
|
|
months=0,
|
|
):
|
|
if not isinstance(years, int) or not isinstance(months, int):
|
|
raise ValueError("Float year and months are not supported")
|
|
|
|
self = timedelta.__new__(
|
|
cls, days, seconds, microseconds, milliseconds, minutes, hours, weeks
|
|
)
|
|
|
|
# We need to compute the total_seconds() value
|
|
# on a native timedelta object
|
|
delta = timedelta(
|
|
days, seconds, microseconds, milliseconds, minutes, hours, weeks
|
|
)
|
|
|
|
# Intuitive normalization
|
|
self._total = delta.total_seconds()
|
|
total = abs(self._total)
|
|
|
|
self._microseconds = round(total % 1 * 1e6)
|
|
self._seconds = int(total) % SECONDS_PER_DAY
|
|
|
|
days = int(total) // SECONDS_PER_DAY
|
|
self._days = abs(days + years * 365 + months * 30)
|
|
self._remaining_days = days % 7
|
|
self._weeks = days // 7
|
|
self._months = abs(months)
|
|
self._years = abs(years)
|
|
|
|
return self
|
|
|
|
def total_seconds(self):
|
|
return abs(self._total)
|
|
|
|
@property
|
|
def invert(self):
|
|
if self._invert is None:
|
|
self._invert = self._total < 0
|
|
|
|
return self._invert
|