Skip to content

Instantly share code, notes, and snippets.

@waylan
Created February 22, 2021 21:10
Show Gist options
  • Save waylan/2cf4351c6a7cf0c39ee41e7209e78bec to your computer and use it in GitHub Desktop.
Save waylan/2cf4351c6a7cf0c39ee41e7209e78bec to your computer and use it in GitHub Desktop.
A Python object which represents a year that runs from Sept to Aug.
import datetime
class ServiceYear():
'''
A year which runs from Sept to Aug.
Accepts a year in any format which can be converted to an integer.
>>> year = ServiceYear(2021)
Instances can also be created using the classmethods `today`,
`from_date` and `from_str`.
>>> ServiceYear.today()
>>> ServiceYear.from_date(datetime.date(2021, 2, 1))
>>> ServiceYear.from_str('2021-02-15')
A service year coresponds to the year of Jan - Aug of the calendar year.
Therefore, the service year 2021 runs from Sept 2020 to Aug 2021.
ServiceYear instances work with and understand datetime date objects.
To confirm whether a date exists within a year use `in`:
>>> datetime.date(2020, 11, 24) in ServiceYear(2021)
True
>>> datetime.date(2021, 9, 1) in ServiceYear(2021)
False
Iterating over ServiceYear returns a sequence of datetime.date objects
of the first day of each month of the year.
>>> for month in ServiceYear(2021):
>>> print(month)
Add or subtract integers to get next or previous years. A new instance is
returned.
>>> thisyear = ServiceYear.today()
>>> nextyear = thisyear + 1
>>> two_years_ago = thisyear - 2
To calculate the difference (numbr of years) between two instances,
subtract the earlier instance from the latter instance. An integer is
returned.
>>> nextyear - two-years_ago
3
All of the comparison operators work when comparing ServiceYear instances.
>>> thisyear == ServiceYear.today()
True
>>> nextyear > thisyear
True
>>> thisyear < nextyear
True
>>> thisyear != nextyear
True
>>> thisyear <= nextyear
True
>>> thisyear >= nextyear
False
The built in functions `str()` and `int()` will return a string or integer
respectively when passed an instance of ServiceYear.
ServiceYear instances are hashable by year. Hashes of seperate instances
of the same year are equal. Therefore, a set will contain no more than one
instance of any given year.
A ServiceYear instance contains two attributes:
* ServiceYear.start: datetime.date object of the first day of the year
(first day of Sept).
* ServiceYear.end: datetime.date object of the last day of the year
(last day of Aug).
'''
def __init__(self, year):
self._year = int(year)
self.start = datetime.date(self._year - 1, 9, 1)
self.end = datetime.date(self._year, 8, 31)
def __contains__(self, date):
return date >= self.start and date <= self.end
def __iter__(self):
for m in range(9, 13):
yield datetime.date(int(self) - 1, m, 1)
for m in range(1, 9):
yield datetime.date(int(self), m, 1)
def __add__(self, other):
return self.__class__(int(self) + other)
def __sub__(self, other):
if isinstance(other, ServiceYear):
return int(self) - int(other)
return self.__class__(int(self) - other)
def __str__(self):
return str(int(self))
def __repr__(self):
return f'{self.__class__.__name__}({self})'
def __int__(self):
return self._year
def __hash__(self):
return hash((int(self), self.__class__.__name__))
def __eq__(self, other):
return isinstance(other, ServiceYear) and int(self) == int(other)
def __lt__(self, other):
if not isinstance(other, ServiceYear):
return NotImplemented
return int(self) < int(other)
def __le__(self, other):
return self == other or self < other
def __gt__(self, other):
if not isinstance(other, ServiceYear):
return NotImplemented
return int(self) > int(other)
def __ge__(self, other):
return self == other or self > other
def iter_strings(self, format='%Y-%m'):
''' Return an iterator of months as strings using format. '''
return [m.strftime(format) for m in self]
@classmethod
def from_date(cls, date):
''' Return a new ServiceYear instance based on year of given date. '''
if date.month > 8:
return cls(date.year + 1)
return cls(date.year)
@classmethod
def today(cls):
''' Return a new ServiceYear instance based on year of today's date. '''
return cls.from_date(datetime.date.today())
@classmethod
def from_str(cls, date_str, format='%Y-%m-%d'):
''' Return a new ServiceYear instance based on year of given date string. '''
return cls.from_date(datetime.datetime.strptime(date_str, format))
import unittest
import datetime
from service_year import ServiceYear
class TestServiceYear(unittest.TestCase):
def test_service_year_from_int(self):
year = ServiceYear(2021)
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, 2021)
self.assertEqual(year.start, datetime.date(2020, 9, 1))
self.assertEqual(year.end, datetime.date(2021, 8, 31))
self.assertIn(datetime.date(2020, 11, 15), year)
self.assertIn(datetime.date(2021, 5, 15), year)
self.assertNotIn(datetime.date(2020, 8, 31), year)
self.assertNotIn(datetime.date(2021, 9, 1), year)
self.assertEqual(len(list(year)), 12)
self.assertEqual(str(year), '2021')
self.assertEqual(repr(year), 'ServiceYear(2021)')
self.assertEqual(int(year), 2021)
self.assertEqual(year, ServiceYear(2021))
self.assertNotEqual(year, ServiceYear(2022))
self.assertListEqual(
year.iter_strings(),
['2020-09', '2020-10', '2020-11', '2020-12', '2021-01', '2021-02',
'2021-03', '2021-04', '2021-05', '2021-06', '2021-07', '2021-08']
)
def test_service_year_from_str(self):
year = ServiceYear('2015')
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, 2015)
self.assertEqual(year.start, datetime.date(2014, 9, 1))
self.assertEqual(year.end, datetime.date(2015, 8, 31))
self.assertIn(datetime.date(2014, 11, 15), year)
self.assertIn(datetime.date(2015, 5, 15), year)
self.assertNotIn(datetime.date(2014, 8, 31), year)
self.assertNotIn(datetime.date(2015, 9, 1), year)
self.assertEqual(len(list(year)), 12)
self.assertEqual(str(year), '2015')
self.assertEqual(repr(year), 'ServiceYear(2015)')
self.assertEqual(int(year), 2015)
self.assertListEqual(
year.iter_strings(),
['2014-09', '2014-10', '2014-11', '2014-12', '2015-01', '2015-02',
'2015-03', '2015-04', '2015-05', '2015-06', '2015-07', '2015-08']
)
def test_service_year_from_date(self):
year = ServiceYear.from_date(datetime.date(2025, 10, 12))
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, 2026)
self.assertEqual(year.start, datetime.date(2025, 9, 1))
self.assertEqual(year.end, datetime.date(2026, 8, 31))
self.assertIn(datetime.date(2025, 11, 15), year)
self.assertIn(datetime.date(2026, 5, 15), year)
self.assertNotIn(datetime.date(2025, 8, 31), year)
self.assertNotIn(datetime.date(2026, 9, 1), year)
self.assertEqual(len(list(year)), 12)
self.assertEqual(str(year), '2026')
self.assertEqual(repr(year), 'ServiceYear(2026)')
self.assertEqual(int(year), 2026)
self.assertListEqual(
year.iter_strings(),
['2025-09', '2025-10', '2025-11', '2025-12', '2026-01', '2026-02',
'2026-03', '2026-04', '2026-05', '2026-06', '2026-07', '2026-08']
)
def test_service_year_today(self):
year = ServiceYear.today()
today = datetime.date.today()
if today.month > 8:
thisyear = today.year + 1
else:
thisyear = today.year
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, thisyear)
self.assertEqual(year.start, datetime.date(thisyear - 1, 9, 1))
self.assertEqual(year.end, datetime.date(thisyear, 8, 31))
def test_service_year_from_date_format_default(self):
year = ServiceYear.from_str('2026-01-15')
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, 2026)
self.assertEqual(year.start, datetime.date(2025, 9, 1))
self.assertEqual(year.end, datetime.date(2026, 8, 31))
self.assertIn(datetime.date(2025, 11, 15), year)
self.assertIn(datetime.date(2026, 5, 15), year)
self.assertNotIn(datetime.date(2025, 8, 31), year)
self.assertNotIn(datetime.date(2026, 9, 1), year)
self.assertEqual(len(list(year)), 12)
self.assertEqual(str(year), '2026')
self.assertEqual(repr(year), 'ServiceYear(2026)')
self.assertEqual(int(year), 2026)
self.assertListEqual(
year.iter_strings(),
['2025-09', '2025-10', '2025-11', '2025-12', '2026-01', '2026-02',
'2026-03', '2026-04', '2026-05', '2026-06', '2026-07', '2026-08']
)
def test_service_year_from_date_format_custom(self):
year = ServiceYear.from_str('15-01-2026', format='%d-%m-%Y')
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, 2026)
self.assertEqual(year.start, datetime.date(2025, 9, 1))
self.assertEqual(year.end, datetime.date(2026, 8, 31))
self.assertIn(datetime.date(2025, 11, 15), year)
self.assertIn(datetime.date(2026, 5, 15), year)
self.assertNotIn(datetime.date(2025, 8, 31), year)
self.assertNotIn(datetime.date(2026, 9, 1), year)
self.assertEqual(len(list(year)), 12)
self.assertEqual(str(year), '2026')
self.assertEqual(repr(year), 'ServiceYear(2026)')
self.assertEqual(int(year), 2026)
self.assertListEqual(
year.iter_strings(),
['2025-09', '2025-10', '2025-11', '2025-12', '2026-01', '2026-02',
'2026-03', '2026-04', '2026-05', '2026-06', '2026-07', '2026-08']
)
def test_service_year_add(self):
year = ServiceYear(2019) + 2
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, 2021)
self.assertEqual(year.start, datetime.date(2020, 9, 1))
self.assertEqual(year.end, datetime.date(2021, 8, 31))
self.assertIn(datetime.date(2020, 11, 15), year)
self.assertIn(datetime.date(2021, 5, 15), year)
self.assertNotIn(datetime.date(2020, 8, 31), year)
self.assertNotIn(datetime.date(2021, 9, 1), year)
self.assertEqual(len(list(year)), 12)
self.assertEqual(str(year), '2021')
self.assertEqual(repr(year), 'ServiceYear(2021)')
self.assertEqual(int(year), 2021)
self.assertListEqual(
year.iter_strings(),
['2020-09', '2020-10', '2020-11', '2020-12', '2021-01', '2021-02',
'2021-03', '2021-04', '2021-05', '2021-06', '2021-07', '2021-08']
)
def test_service_year_subtract(self):
year = ServiceYear(2022) - 1
self.assertIsInstance(year, ServiceYear)
self.assertEqual(year._year, 2021)
self.assertEqual(year.start, datetime.date(2020, 9, 1))
self.assertEqual(year.end, datetime.date(2021, 8, 31))
self.assertIn(datetime.date(2020, 11, 15), year)
self.assertIn(datetime.date(2021, 5, 15), year)
self.assertNotIn(datetime.date(2020, 8, 31), year)
self.assertNotIn(datetime.date(2021, 9, 1), year)
self.assertEqual(len(list(year)), 12)
self.assertEqual(str(year), '2021')
self.assertEqual(repr(year), 'ServiceYear(2021)')
self.assertEqual(int(year), 2021)
self.assertListEqual(
year.iter_strings(),
['2020-09', '2020-10', '2020-11', '2020-12', '2021-01', '2021-02',
'2021-03', '2021-04', '2021-05', '2021-06', '2021-07', '2021-08']
)
def test_service_year_diff(self):
year = ServiceYear.today()
self.assertIsInstance(year - year, int)
self.assertEqual((year + 1) - year, 1)
self.assertEqual((year + 15) - year, 15)
self.assertEqual(year - (year + 1), -1)
self.assertEqual(year - year, 0)
def test_service_year_comparisons(self):
thisyear = ServiceYear.today()
nextyear = thisyear + 1
lastyear = thisyear - 1
self.assertTrue(thisyear == ServiceYear.today())
self.assertFalse(thisyear == datetime.date.today().year)
self.assertFalse(thisyear == nextyear)
self.assertTrue(thisyear != nextyear)
self.assertFalse(thisyear != ServiceYear.today())
self.assertTrue(thisyear > lastyear)
self.assertFalse(thisyear > nextyear)
self.assertTrue(thisyear >= ServiceYear.today())
self.assertTrue(thisyear >= lastyear)
self.assertFalse(thisyear >= nextyear)
self.assertTrue(thisyear < nextyear)
self.assertFalse(thisyear < lastyear)
self.assertTrue(thisyear <= ServiceYear.today())
self.assertTrue(thisyear <= nextyear)
self.assertFalse(thisyear <= lastyear)
yearlist = [nextyear, lastyear, thisyear]
self.assertNotEqual(
yearlist,
[lastyear, thisyear, nextyear]
)
yearlist.sort()
self.assertListEqual(
yearlist,
[lastyear, thisyear, nextyear]
)
def test_service_year_hash(self):
thisyear = ServiceYear.today()
nextyear = thisyear + 1
lastyear = thisyear - 1
self.assertEqual(hash(thisyear), hash(ServiceYear.today()))
self.assertNotEqual(hash(thisyear), hash(nextyear))
self.assertEqual(len({thisyear, thisyear, nextyear, lastyear}), 3)
self.assertEqual(len({thisyear, datetime.date.today().year}), 2)
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment