books: Start FiscalYear class.

This commit is contained in:
Brett Smith 2020-04-20 17:20:26 -04:00
parent 894f044093
commit 5c60666619
2 changed files with 210 additions and 0 deletions

View file

@ -0,0 +1,74 @@
"""books - Tools for loading the books"""
# Copyright © 2020 Brett Smith
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import datetime
from typing import (
Iterable,
NamedTuple,
Optional,
Union,
)
class FiscalYear(NamedTuple):
month: int = 3
day: int = 1
def for_date(self, date: Optional[datetime.date]=None) -> int:
if date is None:
date = datetime.date.today()
if (date.month, date.day) < self:
return date.year - 1
else:
return date.year
def range(self,
from_fy: Union[int, datetime.date],
to_fy: Union[int, datetime.date, None]=None,
) -> Iterable[int]:
"""Return a range of fiscal years
Both arguments can be either a year (represented as an integer) or a
date. Dates will be converted into a year by calling for_date() on
them.
If the first argument is negative or below 1000, it will be treated as
an offset. You'll get a range of fiscal years between the second
argument offset by this amount.
If the second argument is omitted, it defaults to the current fiscal
year.
Note that unlike normal Python ranges, these ranges include the final
fiscal year.
Examples:
range(2015) # Iterate all fiscal years from 2015 to today, inclusive
range(-1) # Iterate the previous fiscal year and current fiscal year
"""
if not isinstance(from_fy, int):
from_fy = self.for_date(from_fy)
if to_fy is None:
to_fy = self.for_date()
elif not isinstance(to_fy, int):
to_fy = self.for_date(to_fy - datetime.timedelta(days=1))
if from_fy < 1:
from_fy += to_fy
elif from_fy < 1000:
from_fy, to_fy = to_fy, from_fy + to_fy
return range(from_fy, to_fy + 1)

View file

@ -0,0 +1,136 @@
"""test_books_fiscal_year - Unit tests for books.FiscalYear"""
# Copyright © 2020 Brett Smith
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import datetime
import itertools
import pytest
from conservancy_beancount import books
@pytest.fixture(scope='module')
def conservancy_fy():
return books.FiscalYear(3, 1)
@pytest.fixture(scope='module')
def cy_fy():
return books.FiscalYear(1, 1)
# Not bothering because it's too much trouble to override __new__
# @pytest.mark.parametrize('month,day', [
# (0, 0),
# (2, 29),
# (4, 31),
# (13, 15),
# (-1, 1),
# (1, -5),
# ])
# def test_values_checked(month, day):
# with pytest.raises(ValueError):
# books.FiscalYear(month, day)
def test_attribute_access():
fy = books.FiscalYear(2, 15)
assert fy.month == 2
assert fy.day == 15
with pytest.raises(AttributeError):
fy.year
@pytest.mark.parametrize('month,day,expected', [
(1, 1, 2019),
(2, 15, 2019),
(2, 28, 2019),
(2, 29, 2019),
(3, 1, 2020),
(3, 9, 2020),
(6, 1, 2020),
(12, 31, 2020),
])
def test_for_date(conservancy_fy, month, day, expected):
date = datetime.date(2020, month, day)
assert conservancy_fy.for_date(date) == expected
def test_for_date_default_today(cy_fy):
assert cy_fy.for_date() == datetime.date.today().year
@pytest.mark.parametrize('begin_date,end_date,expected', [
((2020, 3, 1), (2021, 2, 28), [2020]),
((2020, 1, 1), (2020, 3, 1), [2019]),
((2020, 1, 1), (2020, 3, 5), [2019, 2020]),
((2019, 2, 1), (2020, 6, 1), [2018, 2019, 2020]),
])
def test_range_two_dates(conservancy_fy, begin_date, end_date, expected):
actual = list(conservancy_fy.range(datetime.date(*begin_date), datetime.date(*end_date)))
assert actual == expected
@pytest.mark.parametrize('year_offset', range(3))
def test_range_one_date(cy_fy, year_offset):
this_year = datetime.date.today().year
actual = list(cy_fy.range(datetime.date(this_year - year_offset, 1, 1)))
assert actual == list(range(this_year - year_offset, this_year + 1))
@pytest.mark.parametrize('begin_year,end_year', [
(2006, 2020),
(2019, 2020),
(2020, 2020),
])
def test_range_two_years(conservancy_fy, begin_year, end_year):
actual = list(conservancy_fy.range(begin_year, end_year))
assert actual == list(range(begin_year, end_year + 1))
@pytest.mark.parametrize('year_offset', range(3))
def test_range_one_year(cy_fy, year_offset):
this_year = datetime.date.today().year
actual = list(cy_fy.range(this_year - year_offset))
assert actual == list(range(this_year - year_offset, this_year + 1))
@pytest.mark.parametrize('year_offset,month_offset', itertools.product(
range(-3, 3),
[-1, 1]
))
def test_range_offset_and_date(conservancy_fy, year_offset, month_offset):
end_date = datetime.date(2020, conservancy_fy.month + month_offset, 10)
base_year = end_date.year
if month_offset < 0:
base_year -= 1
if year_offset < 0:
expected = range(base_year + year_offset, base_year + 1)
else:
expected = range(base_year, base_year + year_offset + 1)
actual = list(conservancy_fy.range(year_offset, end_date))
assert actual == list(expected)
@pytest.mark.parametrize('year_offset,year', itertools.product(
range(-3, 3),
[2010, 2015],
))
def test_range_offset_and_year(conservancy_fy, year_offset, year):
if year_offset < 0:
expected = range(year + year_offset, year + 1)
else:
expected = range(year, year + year_offset + 1)
actual = list(conservancy_fy.range(year_offset, year))
assert actual == list(expected)
@pytest.mark.parametrize('year_offset', range(-3, 3))
def test_range_offset_only(cy_fy, year_offset):
year = datetime.date.today().year
if year_offset < 0:
expected = range(year + year_offset, year + 1)
else:
expected = range(year, year + year_offset + 1)
actual = list(cy_fy.range(year_offset))
assert actual == list(expected)