from __future__ import annotations
from typing import NoReturn
import datetime
import functools
import pendulum
from . import config, types as t
from ._errors import InstantError, PeriodError
from ._parsers import parse_instant, parse_period
from .date_unit import DateUnit
from .instant_ import Instant
from .period_ import Period
[docs]
@functools.singledispatch
def instant(value: object) -> t.Instant:
"""Build a new instant, aka a triple of integers (year, month, day).
Args:
value(object): An ``instant-like`` object.
Returns:
:obj:`.Instant`: A new instant.
Raises:
:exc:`ValueError`: When the arguments were invalid, like "2021-32-13".
Examples:
>>> instant((2021,))
Instant((2021, 1, 1))
>>> instant((2021, 9))
Instant((2021, 9, 1))
>>> instant(datetime.date(2021, 9, 16))
Instant((2021, 9, 16))
>>> instant(Instant((2021, 9, 16)))
Instant((2021, 9, 16))
>>> instant(Period((DateUnit.YEAR, Instant((2021, 9, 16)), 1)))
Instant((2021, 9, 16))
>>> instant(2021)
Instant((2021, 1, 1))
>>> instant("2021")
Instant((2021, 1, 1))
>>> instant([2021])
Instant((2021, 1, 1))
>>> instant([2021, 9])
Instant((2021, 9, 1))
>>> instant(None)
Traceback (most recent call last):
openfisca_core.periods._errors.InstantError: 'None' is not a valid i...
"""
if isinstance(value, t.SeqInt):
return Instant((list(value) + [1] * 3)[:3])
raise InstantError(str(value))
@instant.register
def _(value: None) -> NoReturn:
raise InstantError(str(value))
@instant.register
def _(value: int) -> t.Instant:
return Instant((value, 1, 1))
@instant.register
def _(value: Period) -> t.Instant:
return value.start
@instant.register
def _(value: t.Instant) -> t.Instant:
return value
@instant.register
def _(value: datetime.date) -> t.Instant:
return Instant((value.year, value.month, value.day))
@instant.register
def _(value: str) -> t.Instant:
return parse_instant(value)
[docs]
def instant_date(instant: None | t.Instant) -> None | datetime.date:
"""Returns the date representation of an ``Instant``.
Args:
instant: An ``Instant``.
Returns:
None: When ``instant`` is None.
datetime.date: Otherwise.
Examples:
>>> instant_date(Instant((2021, 1, 1)))
Date(2021, 1, 1)
"""
if instant is None:
return None
instant_date = config.date_by_instant_cache.get(instant)
if instant_date is None:
config.date_by_instant_cache[instant] = instant_date = pendulum.date(*instant)
return instant_date
[docs]
@functools.singledispatch
def period(value: object) -> t.Period:
"""Build a new period, aka a triple (unit, start_instant, size).
Args:
value: A ``period-like`` object.
Returns:
:obj:`.Period`: A period.
Raises:
:exc:`ValueError`: When the arguments were invalid, like "2021-32-13".
Examples:
>>> period(Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)))
Period((<DateUnit.YEAR: 'year'>, Instant((2021, 1, 1)), 1))
>>> period(Instant((2021, 1, 1)))
Period((<DateUnit.DAY: 'day'>, Instant((2021, 1, 1)), 1))
>>> period(DateUnit.ETERNITY)
Period((<DateUnit.ETERNITY: 'eternity'>, Instant((-1, -1, -1)), -1))
>>> period(2021)
Period((<DateUnit.YEAR: 'year'>, Instant((2021, 1, 1)), 1))
>>> period("2014")
Period((<DateUnit.YEAR: 'year'>, Instant((2014, 1, 1)), 1))
>>> period("year:2014")
Period((<DateUnit.YEAR: 'year'>, Instant((2014, 1, 1)), 1))
>>> period("month:2014-02")
Period((<DateUnit.MONTH: 'month'>, Instant((2014, 2, 1)), 1))
>>> period("year:2014-02")
Period((<DateUnit.YEAR: 'year'>, Instant((2014, 2, 1)), 1))
>>> period("day:2014-02-02")
Period((<DateUnit.DAY: 'day'>, Instant((2014, 2, 2)), 1))
>>> period("day:2014-02-02:3")
Period((<DateUnit.DAY: 'day'>, Instant((2014, 2, 2)), 3))
"""
one, two, three = 1, 2, 3
# We return an "eternity-period", for example
# ``<Period(('eternity', <Instant(-1, -1, -1)>, -1))>``.
if str(value).lower() == DateUnit.ETERNITY:
return Period.eternity()
# We try to parse from an ISO format/calendar period.
if isinstance(value, t.InstantStr):
return parse_period(value)
# A complex period has a ':' in its string.
if isinstance(value, t.PeriodStr):
components = value.split(":")
# The left-most component must be a valid unit
unit = components[0]
if unit not in list(DateUnit) or unit == DateUnit.ETERNITY:
raise PeriodError(str(value))
# Cast ``unit`` to DateUnit.
unit = DateUnit(unit)
# The middle component must be a valid iso period
period = parse_period(components[1])
# Periods like year:2015-03 have a size of 1
if len(components) == two:
size = one
# if provided, make sure the size is an integer
elif len(components) == three:
try:
size = int(components[2])
except ValueError as error:
raise PeriodError(str(value)) from error
# If there are more than 2 ":" in the string, the period is invalid
else:
raise PeriodError(str(value))
# Reject ambiguous periods such as month:2014
if unit_weight(period.unit) > unit_weight(unit):
raise PeriodError(str(value))
return Period((unit, period.start, size))
raise PeriodError(str(value))
@period.register
def _(value: None) -> NoReturn:
raise PeriodError(str(value))
@period.register
def _(value: int) -> t.Period:
return Period((DateUnit.YEAR, instant(value), 1))
@period.register
def _(value: t.Period) -> t.Period:
return value
@period.register
def _(value: t.Instant) -> t.Period:
return Period((DateUnit.DAY, value, 1))
@period.register
def _(value: datetime.date) -> t.Period:
return Period((DateUnit.DAY, instant(value), 1))
[docs]
def key_period_size(period: t.Period) -> str:
"""Define a key in order to sort periods by length.
It uses two aspects: first, ``unit``, then, ``size``.
Args:
period: An :mod:`.openfisca_core` :obj:`.Period`.
Returns:
:obj:`str`: A string.
Examples:
>>> instant = Instant((2021, 9, 14))
>>> period = Period((DateUnit.DAY, instant, 1))
>>> key_period_size(period)
'100_1'
>>> period = Period((DateUnit.YEAR, instant, 3))
>>> key_period_size(period)
'300_3'
"""
return f"{unit_weight(period.unit)}_{period.size}"
[docs]
def unit_weights() -> dict[t.DateUnit, int]:
"""Assign weights to date units.
Examples:
>>> unit_weights()
{<DateUnit.WEEKDAY: 'weekday'>: 100, ...ETERNITY: 'eternity'>: 400}
"""
return {
DateUnit.WEEKDAY: 100,
DateUnit.WEEK: 200,
DateUnit.DAY: 100,
DateUnit.MONTH: 200,
DateUnit.YEAR: 300,
DateUnit.ETERNITY: 400,
}
[docs]
def unit_weight(unit: t.DateUnit) -> int:
"""Retrieves a specific date unit weight.
Examples:
>>> unit_weight(DateUnit.DAY)
100
"""
return unit_weights()[unit]
__all__ = [
"instant",
"instant_date",
"key_period_size",
"period",
"unit_weight",
"unit_weights",
]