books: Start FiscalYear class.
This commit is contained in:
parent
894f044093
commit
5c60666619
2 changed files with 210 additions and 0 deletions
74
conservancy_beancount/books.py
Normal file
74
conservancy_beancount/books.py
Normal 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)
|
136
tests/test_books_fiscal_year.py
Normal file
136
tests/test_books_fiscal_year.py
Normal 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)
|
Loading…
Reference in a new issue