diff --git a/fixedcal/core/date.py b/fixedcal/core/date.py index 80b0e23..2f95c55 100644 --- a/fixedcal/core/date.py +++ b/fixedcal/core/date.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from math import floor +from fixedcal.services.leap_days import is_leap_year, gregorian_leap_days_between, fixed_leap_days_between class FixedDate: def __init__(self, date = None, day_of_year = None, year = None): @@ -40,6 +40,18 @@ class FixedDate: def today(self) -> "FixedDate": return FixedDate(date=datetime.today()) + @property + def is_leap_year(self) -> bool: + """Whether the year of this date is leap year. + + Returns: + bool: Is this leap year + """ + # if self._year % 100 == 0: + # return self._year % 4 == 0 and self._year % 400 == 0 + # return self._year % 4 == 0 + return is_leap_year(self._year) + @property def datetime(self) -> datetime: """Construct a native datetime object from fixed date. @@ -149,7 +161,10 @@ class FixedDate: With timedelta as argument, new FixedDate will be returned. """ if isinstance(o, FixedDate): - return self.datetime - o.datetime + difference = self.datetime - o.datetime + greg_leap_days = gregorian_leap_days_between(self.datetime, o.datetime) + fixed_leap_days = fixed_leap_days_between(self.datetime, o.datetime) + return difference - timedelta(greg_leap_days) + timedelta(fixed_leap_days) elif isinstance(o, timedelta): new_date = self.datetime - o return FixedDate(date=new_date) diff --git a/fixedcal/services/leap_days.py b/fixedcal/services/leap_days.py new file mode 100644 index 0000000..e225de4 --- /dev/null +++ b/fixedcal/services/leap_days.py @@ -0,0 +1,38 @@ +from datetime import datetime, timedelta + +def is_leap_year(year: int) -> bool: + if year % 100 == 0: + return year % 4 == 0 and year % 400 == 0 + return year % 4 == 0 + +def gregorian_leap_days_between(date1: datetime, date2: datetime) -> int: + """Counts the gregorian leap days (29th Feb) between given dates. + Count includes both ends (date1 and date2 themselves). + + Args: + date1 (datetime): The beginning of the count + date2 (datetime): The end of the count + + Returns: + int: Count of the leap days. + """ + count = 0 + if date1 > date2: + date1, date2 = date2, date1 + days_between = (date2 - date1).days + for plusday in range(0, days_between): + date = date1 + timedelta(plusday) + if is_leap_year(date.year) and date.month == 2 and date.day == 29: + count += 1 + return count + +def fixed_leap_days_between(date1: datetime, date2: datetime) -> int: + count = 0 + if date1 > date2: + date1, date2 = date2, date1 + days_between = (date2 - date1).days + for plusday in range(0, days_between): + date = date1 + timedelta(plusday) + if is_leap_year(date.year) and date.month == 6 and date.day == 27: + count += 1 + return count diff --git a/tests/leap_year_test.py b/tests/leap_year_test.py new file mode 100644 index 0000000..bd08169 --- /dev/null +++ b/tests/leap_year_test.py @@ -0,0 +1,61 @@ +import unittest +from datetime import datetime, timedelta +from fixedcal import FixedDate + +class TestLeapYear(unittest.TestCase): + def test_leap_year_detection_with_simple_noleap(self): + # 2022 is not divisible of four -> not a leap year + fixed_date = FixedDate(datetime(2022, 5, 4)) + self.assertFalse(fixed_date.is_leap_year) + + def test_leap_year_detection_with_complex_noleap(self): + # 1900 is divisible of four but also by 100 and not by 400 -> not a leap year + fixed_date = FixedDate(datetime(1900, 5, 4)) + self.assertFalse(fixed_date.is_leap_year) + + def test_leap_year_detection_with_common_leap_year(self): + # 2024 is divisible of four but not by 100 -> leap year + fixed_date = FixedDate(datetime(2024, 5, 4)) + self.assertTrue(fixed_date.is_leap_year) + + def test_leap_year_detection_with_centurial_leap_year(self): + # 2000 is divisible of all four, 100 and 400 -> leap year + fixed_date = FixedDate(datetime(2000, 5, 4)) + self.assertTrue(fixed_date.is_leap_year) + + def test_fixed_date_difference_over_gregorian_leap_day(self): + # in Gregorian system there are 7 days between, + # but in IFC the leap day is at the end of June + # and thus the difference should be just 6 days + date1 = FixedDate(datetime(2024, 2, 25)) + date2 = FixedDate(datetime(2024, 3, 3)) + self.assertEqual(date2-date1, timedelta(6)) + + def test_fixed_date_difference_over_fixed_leap_day(self): + # in Gregorian system there are 7 days between, + # but in IFC the leap day is also in between + # and therefore the difference should be 8 days + date1 = FixedDate(datetime(2024, 6, 27)) + date2 = FixedDate(datetime(2024, 7, 4)) + self.assertEqual(date2-date1, timedelta(8)) + + def test_fixed_date_difference_with_itself_gregorian_leap_day(self): + date = FixedDate(datetime(2024, 2, 29)) + self.assertEqual(date-date, timedelta(0)) + + def test_fixed_date_difference_with_itself_fixed_leap_day(self): + date = FixedDate(datetime(2024, 6, 27)) + self.assertEqual(date-date, timedelta(0)) + + def test_fixed_date_difference_over_both_leap_days(self): + # day count should be the same in both systems + date1 = FixedDate(datetime(2024, 2, 15)) + date2 = FixedDate(datetime(2024, 8, 3)) + self.assertEqual(date2-date1, timedelta(170)) + + def test_fixed_date_difference_over_multiple_leap_days(self): + # there are 3 Gregorian and 2 fixed leap days between + # difference is 2987 days in Gregorian including Greg leap days + date1 = FixedDate(datetime(2020, 2, 15)) + date2 = FixedDate(datetime(2028, 4, 20)) + self.assertEqual(date2-date1, timedelta(2986))