from __future__ import annotations
import calendar
from . import config, helpers
from .instant_ import Instant
[docs]class Period(tuple):
"""
Toolbox to handle date intervals.
A period is a triple (unit, start, size), where unit is either "month" or "year", where start format is a
(year, month, day) triple, and where size is an integer > 1.
Since a period is a triple it can be used as a dictionary key.
"""
def __repr__(self):
"""
Transform period to to its Python representation as a string.
>>> repr(period('year', 2014))
"Period(('year', Instant((2014, 1, 1)), 1))"
>>> repr(period('month', '2014-2'))
"Period(('month', Instant((2014, 2, 1)), 1))"
>>> repr(period('day', '2014-2-3'))
"Period(('day', Instant((2014, 2, 3)), 1))"
"""
return '{}({})'.format(self.__class__.__name__, super(self.__class__, self).__repr__())
def __str__(self):
"""
Transform period to a string.
>>> str(period(YEAR, 2014))
'2014'
>>> str(period(YEAR, '2014-2'))
'year:2014-02'
>>> str(period(MONTH, '2014-2'))
'2014-02'
>>> str(period(YEAR, 2012, size = 2))
'year:2012:2'
>>> str(period(MONTH, 2012, size = 2))
'month:2012-01:2'
>>> str(period(MONTH, 2012, size = 12))
'2012'
>>> str(period(YEAR, '2012-3', size = 2))
'year:2012-03:2'
>>> str(period(MONTH, '2012-3', size = 2))
'month:2012-03:2'
>>> str(period(MONTH, '2012-3', size = 12))
'year:2012-03'
"""
unit, start_instant, size = self
if unit == config.ETERNITY:
return 'ETERNITY'
year, month, day = start_instant
# 1 year long period
if (unit == config.MONTH and size == 12 or unit == config.YEAR and size == 1):
if month == 1:
# civil year starting from january
return str(year)
else:
# rolling year
return '{}:{}-{:02d}'.format(config.YEAR, year, month)
# simple month
if unit == config.MONTH and size == 1:
return '{}-{:02d}'.format(year, month)
# several civil years
if unit == config.YEAR and month == 1:
return '{}:{}:{}'.format(unit, year, size)
if unit == config.DAY:
if size == 1:
return '{}-{:02d}-{:02d}'.format(year, month, day)
else:
return '{}:{}-{:02d}-{:02d}:{}'.format(unit, year, month, day, size)
# complex period
return '{}:{}-{:02d}:{}'.format(unit, year, month, size)
@property
def date(self):
assert self.size == 1, '"date" is undefined for a period of size > 1: {}'.format(self)
return self.start.date
@property
def days(self):
"""
Count the number of days in period.
>>> period('day', 2014).days
365
>>> period('month', 2014).days
365
>>> period('year', 2014).days
365
>>> period('day', '2014-2').days
28
>>> period('month', '2014-2').days
28
>>> period('year', '2014-2').days
365
>>> period('day', '2014-2-3').days
1
>>> period('month', '2014-2-3').days
28
>>> period('year', '2014-2-3').days
365
"""
return (self.stop.date - self.start.date).days + 1
def intersection(self, start, stop):
if start is None and stop is None:
return self
period_start = self[1]
period_stop = self.stop
if start is None:
start = period_start
if stop is None:
stop = period_stop
if stop < period_start or period_stop < start:
return None
intersection_start = max(period_start, start)
intersection_stop = min(period_stop, stop)
if intersection_start == period_start and intersection_stop == period_stop:
return self
if intersection_start.day == 1 and intersection_start.month == 1 \
and intersection_stop.day == 31 and intersection_stop.month == 12:
return self.__class__((
'year',
intersection_start,
intersection_stop.year - intersection_start.year + 1,
))
if intersection_start.day == 1 and intersection_stop.day == calendar.monthrange(intersection_stop.year,
intersection_stop.month)[1]:
return self.__class__((
'month',
intersection_start,
(
(intersection_stop.year - intersection_start.year) * 12
+ intersection_stop.month
- intersection_start.month
+ 1
),
))
return self.__class__((
'day',
intersection_start,
(intersection_stop.date - intersection_start.date).days + 1,
))
[docs] def get_subperiods(self, unit):
"""
Return the list of all the periods of unit ``unit`` contained in self.
Examples:
>>> period('2017').get_subperiods(MONTH)
>>> [period('2017-01'), period('2017-02'), ... period('2017-12')]
>>> period('year:2014:2').get_subperiods(YEAR)
>>> [period('2014'), period('2015')]
"""
if helpers.unit_weight(self.unit) < helpers.unit_weight(unit):
raise ValueError('Cannot subdivide {0} into {1}'.format(self.unit, unit))
if unit == config.YEAR:
return [self.this_year.offset(i, config.YEAR) for i in range(self.size)]
if unit == config.MONTH:
return [self.first_month.offset(i, config.MONTH) for i in range(self.size_in_months)]
if unit == config.DAY:
return [self.first_day.offset(i, config.DAY) for i in range(self.size_in_days)]
[docs] def offset(self, offset, unit = None):
"""
Increment (or decrement) the given period with offset units.
>>> period('day', 2014).offset(1)
Period(('day', Instant((2014, 1, 2)), 365))
>>> period('day', 2014).offset(1, 'day')
Period(('day', Instant((2014, 1, 2)), 365))
>>> period('day', 2014).offset(1, 'month')
Period(('day', Instant((2014, 2, 1)), 365))
>>> period('day', 2014).offset(1, 'year')
Period(('day', Instant((2015, 1, 1)), 365))
>>> period('month', 2014).offset(1)
Period(('month', Instant((2014, 2, 1)), 12))
>>> period('month', 2014).offset(1, 'day')
Period(('month', Instant((2014, 1, 2)), 12))
>>> period('month', 2014).offset(1, 'month')
Period(('month', Instant((2014, 2, 1)), 12))
>>> period('month', 2014).offset(1, 'year')
Period(('month', Instant((2015, 1, 1)), 12))
>>> period('year', 2014).offset(1)
Period(('year', Instant((2015, 1, 1)), 1))
>>> period('year', 2014).offset(1, 'day')
Period(('year', Instant((2014, 1, 2)), 1))
>>> period('year', 2014).offset(1, 'month')
Period(('year', Instant((2014, 2, 1)), 1))
>>> period('year', 2014).offset(1, 'year')
Period(('year', Instant((2015, 1, 1)), 1))
>>> period('day', '2011-2-28').offset(1)
Period(('day', Instant((2011, 3, 1)), 1))
>>> period('month', '2011-2-28').offset(1)
Period(('month', Instant((2011, 3, 28)), 1))
>>> period('year', '2011-2-28').offset(1)
Period(('year', Instant((2012, 2, 28)), 1))
>>> period('day', '2011-3-1').offset(-1)
Period(('day', Instant((2011, 2, 28)), 1))
>>> period('month', '2011-3-1').offset(-1)
Period(('month', Instant((2011, 2, 1)), 1))
>>> period('year', '2011-3-1').offset(-1)
Period(('year', Instant((2010, 3, 1)), 1))
>>> period('day', '2014-1-30').offset(3)
Period(('day', Instant((2014, 2, 2)), 1))
>>> period('month', '2014-1-30').offset(3)
Period(('month', Instant((2014, 4, 30)), 1))
>>> period('year', '2014-1-30').offset(3)
Period(('year', Instant((2017, 1, 30)), 1))
>>> period('day', 2014).offset(-3)
Period(('day', Instant((2013, 12, 29)), 365))
>>> period('month', 2014).offset(-3)
Period(('month', Instant((2013, 10, 1)), 12))
>>> period('year', 2014).offset(-3)
Period(('year', Instant((2011, 1, 1)), 1))
>>> period('day', '2014-2-3').offset('first-of', 'month')
Period(('day', Instant((2014, 2, 1)), 1))
>>> period('day', '2014-2-3').offset('first-of', 'year')
Period(('day', Instant((2014, 1, 1)), 1))
>>> period('day', '2014-2-3', 4).offset('first-of', 'month')
Period(('day', Instant((2014, 2, 1)), 4))
>>> period('day', '2014-2-3', 4).offset('first-of', 'year')
Period(('day', Instant((2014, 1, 1)), 4))
>>> period('month', '2014-2-3').offset('first-of')
Period(('month', Instant((2014, 2, 1)), 1))
>>> period('month', '2014-2-3').offset('first-of', 'month')
Period(('month', Instant((2014, 2, 1)), 1))
>>> period('month', '2014-2-3').offset('first-of', 'year')
Period(('month', Instant((2014, 1, 1)), 1))
>>> period('month', '2014-2-3', 4).offset('first-of')
Period(('month', Instant((2014, 2, 1)), 4))
>>> period('month', '2014-2-3', 4).offset('first-of', 'month')
Period(('month', Instant((2014, 2, 1)), 4))
>>> period('month', '2014-2-3', 4).offset('first-of', 'year')
Period(('month', Instant((2014, 1, 1)), 4))
>>> period('year', 2014).offset('first-of')
Period(('year', Instant((2014, 1, 1)), 1))
>>> period('year', 2014).offset('first-of', 'month')
Period(('year', Instant((2014, 1, 1)), 1))
>>> period('year', 2014).offset('first-of', 'year')
Period(('year', Instant((2014, 1, 1)), 1))
>>> period('year', '2014-2-3').offset('first-of')
Period(('year', Instant((2014, 1, 1)), 1))
>>> period('year', '2014-2-3').offset('first-of', 'month')
Period(('year', Instant((2014, 2, 1)), 1))
>>> period('year', '2014-2-3').offset('first-of', 'year')
Period(('year', Instant((2014, 1, 1)), 1))
>>> period('day', '2014-2-3').offset('last-of', 'month')
Period(('day', Instant((2014, 2, 28)), 1))
>>> period('day', '2014-2-3').offset('last-of', 'year')
Period(('day', Instant((2014, 12, 31)), 1))
>>> period('day', '2014-2-3', 4).offset('last-of', 'month')
Period(('day', Instant((2014, 2, 28)), 4))
>>> period('day', '2014-2-3', 4).offset('last-of', 'year')
Period(('day', Instant((2014, 12, 31)), 4))
>>> period('month', '2014-2-3').offset('last-of')
Period(('month', Instant((2014, 2, 28)), 1))
>>> period('month', '2014-2-3').offset('last-of', 'month')
Period(('month', Instant((2014, 2, 28)), 1))
>>> period('month', '2014-2-3').offset('last-of', 'year')
Period(('month', Instant((2014, 12, 31)), 1))
>>> period('month', '2014-2-3', 4).offset('last-of')
Period(('month', Instant((2014, 2, 28)), 4))
>>> period('month', '2014-2-3', 4).offset('last-of', 'month')
Period(('month', Instant((2014, 2, 28)), 4))
>>> period('month', '2014-2-3', 4).offset('last-of', 'year')
Period(('month', Instant((2014, 12, 31)), 4))
>>> period('year', 2014).offset('last-of')
Period(('year', Instant((2014, 12, 31)), 1))
>>> period('year', 2014).offset('last-of', 'month')
Period(('year', Instant((2014, 1, 31)), 1))
>>> period('year', 2014).offset('last-of', 'year')
Period(('year', Instant((2014, 12, 31)), 1))
>>> period('year', '2014-2-3').offset('last-of')
Period(('year', Instant((2014, 12, 31)), 1))
>>> period('year', '2014-2-3').offset('last-of', 'month')
Period(('year', Instant((2014, 2, 28)), 1))
>>> period('year', '2014-2-3').offset('last-of', 'year')
Period(('year', Instant((2014, 12, 31)), 1))
"""
return self.__class__((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2]))
[docs] def contains(self, other: Period) -> bool:
"""
Returns ``True`` if the period contains ``other``. For instance, ``period(2015)`` contains ``period(2015-01)``
"""
return self.start <= other.start and self.stop >= other.stop
@property
def size(self):
"""
Return the size of the period.
>>> period('month', '2012-2-29', 4).size
4
"""
return self[2]
@property
def size_in_months(self):
"""
Return the size of the period in months.
>>> period('month', '2012-2-29', 4).size_in_months
4
>>> period('year', '2012', 1).size_in_months
12
"""
if (self[0] == config.MONTH):
return self[2]
if(self[0] == config.YEAR):
return self[2] * 12
raise ValueError("Cannot calculate number of months in {0}".format(self[0]))
@property
def size_in_days(self):
"""
Return the size of the period in days.
>>> period('month', '2012-2-29', 4).size_in_days
28
>>> period('year', '2012', 1).size_in_days
366
"""
unit, instant, length = self
if unit == config.DAY:
return length
if unit in [config.MONTH, config.YEAR]:
last_day = self.start.offset(length, unit).offset(-1, config.DAY)
return (last_day.date - self.start.date).days + 1
raise ValueError("Cannot calculate number of days in {0}".format(unit))
@property
def start(self) -> Instant:
"""
Return the first day of the period as an Instant instance.
>>> period('month', '2012-2-29', 4).start
Instant((2012, 2, 29))
"""
return self[1]
@property
def stop(self) -> Instant:
"""
Return the last day of the period as an Instant instance.
>>> period('year', 2014).stop
Instant((2014, 12, 31))
>>> period('month', 2014).stop
Instant((2014, 12, 31))
>>> period('day', 2014).stop
Instant((2014, 12, 31))
>>> period('year', '2012-2-29').stop
Instant((2013, 2, 28))
>>> period('month', '2012-2-29').stop
Instant((2012, 3, 28))
>>> period('day', '2012-2-29').stop
Instant((2012, 2, 29))
>>> period('year', '2012-2-29', 2).stop
Instant((2014, 2, 28))
>>> period('month', '2012-2-29', 2).stop
Instant((2012, 4, 28))
>>> period('day', '2012-2-29', 2).stop
Instant((2012, 3, 1))
"""
unit, start_instant, size = self
year, month, day = start_instant
if unit == config.ETERNITY:
return Instant((float("inf"), float("inf"), float("inf")))
if unit == 'day':
if size > 1:
day += size - 1
month_last_day = calendar.monthrange(year, month)[1]
while day > month_last_day:
month += 1
if month == 13:
year += 1
month = 1
day -= month_last_day
month_last_day = calendar.monthrange(year, month)[1]
else:
if unit == 'month':
month += size
while month > 12:
year += 1
month -= 12
else:
assert unit == 'year', 'Invalid unit: {} of type {}'.format(unit, type(unit))
year += size
day -= 1
if day < 1:
month -= 1
if month == 0:
year -= 1
month = 12
day += calendar.monthrange(year, month)[1]
else:
month_last_day = calendar.monthrange(year, month)[1]
if day > month_last_day:
month += 1
if month == 13:
year += 1
month = 1
day -= month_last_day
return Instant((year, month, day))
@property
def unit(self) -> str:
return self[0]
# Reference periods
@property
def last_month(self) -> Period:
return self.first_month.offset(-1)
@property
def last_3_months(self) -> Period:
start: Instant = self.first_month.start
return self.__class__((config.MONTH, start, 3)).offset(-3)
@property
def last_year(self) -> Period:
start: Instant = self.start.offset("first-of", config.YEAR)
return self.__class__((config.YEAR, start, 1)).offset(-1)
@property
def n_2(self) -> Period:
start: Instant = self.start.offset("first-of", config.YEAR)
return self.__class__((config.YEAR, start, 1)).offset(-2)
@property
def this_year(self) -> Period:
start: Instant = self.start.offset("first-of", config.YEAR)
return self.__class__((config.YEAR, start, 1))
@property
def first_month(self) -> Period:
start: Instant = self.start.offset("first-of", config.MONTH)
return self.__class__((config.MONTH, start, 1))
@property
def first_day(self) -> Period:
return self.__class__((config.DAY, self.start, 1))