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…
	
	Add table
		
		Reference in a new issue