"""Generic functionality for modelling growth series."""
[docs]class IndexSeries:
"""Growth rates and factors for indexation series.
The ``_rates`` dictionary is fully-populated at initialisation, but
the ``_values`` dictionary is filled lazily - values are only
calculated as needed.
The class supports a ``Mapping``-like interface; factors can be
accessed with ``value = growth_rate[year]`` or ``value =
growth_rate.get(year, default)``.
Note:
The term 'relative year' refers to the year relative to
``year_zero`` e.g. ``3``. The term 'absolute year' refers to a
calendar year, e.g. ``2013``. Relative years are used internally,
absolute years for the external interface.
Arguments:
base_year (``int``): the year in which the value is equal to the
``initial_value``
rates (``dict`` of ``int``: ``float``): the growth rates to use,
keyed by relative year
initial_value (``float``): the first value for the output series.
year_zero (``int``, optional): the zeroth year for accessing
growth rates. Defaults to :py:attr:`base_year`.
Attributes:
base_year (``int``): The base year for growth, i.e. the year in
which the value is the ``initial_value``.
year_zero (``int``): The zeroth year for growth, i.e. the year
from which the rates are selected from ``_rates``.
_rates (``dict`` of ``int``: ``float``): The growth rates, where
the key is the relative start year and the value is the rate to
apply.
_initial_rate (``float``): The growth rate corresponding to the
first year in the ``rates`` dictionary.
_final_rate (``float``): The growth rate corresponding to the
last year in the ``rates`` dictionary.
_values (``dict`` of ``int``: ``float``): The values, keyed by
year.
"""
def __init__(self, base_year, rates, initial_value, year_zero=None):
self.base_year = base_year
self._rates = rates.copy()
if year_zero is None:
year_zero = base_year
self.year_zero = year_zero
self._rates = rates.copy()
self._values = {base_year-year_zero: initial_value}
self._extend_values(0)
self._hash = None
def __getitem__(self, year):
year -= self.year_zero
if year not in self._values:
self._extend_values(year)
return self._values[year]
def __iter__(self):
return iter(self._values)
def __len__(self):
return len(self._values)
def __hash__(self):
if self._hash is None:
self._hash = (hash(self.base_year) ^
hash(self.year_zero) ^
hash(frozenset(self._rates.items())))
return self._hash
def __eq__(self, other):
# pylint: disable=protected-access
return (self.base_year == other.base_year and
self.year_zero == other.year_zero and
self._rates == other._rates)
@staticmethod
def _infill_rates(rates, initial_rate=None):
"""Fill in the rates dictionary, in-place, to cover all years.
Arguments:
rates (``dict``): The rates dictionary to fill in.
initial_rate (``float``, optional): The value to use for
years prior to the first year in ``rates``.
"""
if initial_rate is None:
initial_rate = rates[min(rates)]
min_year = min(rates)
max_year = max(rates)
rate = initial_rate
for year in range(min_year, max_year):
if year in rates:
rate = rates[year]
else:
rates[year] = rate
[docs] def get(self, year, default=None):
"""Retrieve value or supplied default for given year.
Arguments:
year (int): The year to retrieve the value for.
default (float or None, optional): The value to return if
retrieval fails. Defaults to ``None``.
Returns:
float or None: The retrieved or ``default`` value.
"""
try:
return self.__getitem__(year)
except KeyError:
return default
[docs] def rate(self, year):
"""The rate used in the specified year.
Arguments:
year (int): The year to retrieve the rate for.
Returns:
float: The rate used in that year.
"""
return self._rates[year]
def _extend_values(self, year):
"""Extend the values dictionary to cover the specified year."""
raise NotImplementedError