- commit
- c05b095fdf33b32fcfd455fd0996b4d9b7edfcae
- parent
- 63ca397657ff8ea15d841403495b10d6762b608c
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2024-08-31 00:42
init
Diffstat
| A | nominaldelta.py | 150 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | test.py | 171 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 321 insertions, 0 deletions
diff --git a/nominaldelta.py b/nominaldelta.py
@@ -0,0 +1,150 @@
-1 1 from datetime import date
-1 2 from datetime import datetime
-1 3 from typing import Self
-1 4
-1 5
-1 6 def date_to_timestamp(d):
-1 7 return datetime(d.year, d.month, d.day).timestamp()
-1 8
-1 9
-1 10 def date_add(dt, delta):
-1 11 total_months = dt.year * 12 + dt.month + delta.months
-1 12 year, month = divmod(total_months, 12)
-1 13 if month == 0:
-1 14 year -= 1
-1 15 month = 12
-1 16
-1 17 # clip day to month
-1 18 day = dt.day
-1 19 while day > 0:
-1 20 try:
-1 21 tmp = date(year, month, day)
-1 22 break
-1 23 except ValueError:
-1 24 day -= 1
-1 25
-1 26 return date.fromordinal(tmp.toordinal() + delta.days)
-1 27
-1 28
-1 29 def dt_add(dt, delta):
-1 30 d = date_add(dt.date(), delta)
-1 31 offset = dt.timestamp() - date_to_timestamp(dt.date())
-1 32 return datetime.fromtimestamp(
-1 33 date_to_timestamp(d) + offset + delta.seconds, tz=dt.tzinfo
-1 34 )
-1 35
-1 36
-1 37 def binary_search(a, b, delta):
-1 38 lower = 0
-1 39 upper = 1
-1 40 while a + delta * upper <= b:
-1 41 lower = upper
-1 42 upper <<= 1
-1 43 while lower + 1 < upper:
-1 44 tmp = (lower + upper) // 2
-1 45 if a + delta * tmp <= b:
-1 46 lower = tmp
-1 47 else:
-1 48 upper = tmp
-1 49 return lower * delta
-1 50
-1 51
-1 52 def date_diff(a, b):
-1 53 if a > b:
-1 54 return -date_diff(b, a)
-1 55 delta = binary_search(a, b, NominalDelta(months=1))
-1 56 days = b.toordinal() - (a + delta).toordinal()
-1 57 return delta + NominalDelta(days=days)
-1 58
-1 59
-1 60 def dt_diff(a, b):
-1 61 delta = date_diff(a, b)
-1 62 seconds = b.timestamp() - (a + delta).timestamp()
-1 63 return delta + NominalDelta(seconds=seconds)
-1 64
-1 65
-1 66 class NominalDelta:
-1 67 def __init__(
-1 68 self: Self,
-1 69 *,
-1 70 years: int = 0,
-1 71 months: int = 0,
-1 72 weeks: int = 0,
-1 73 days: int = 0,
-1 74 hours: int = 0,
-1 75 minutes: int = 0,
-1 76 seconds: float = 0,
-1 77 ):
-1 78 self.months = years * 12 + months
-1 79 self.days = weeks * 7 + days
-1 80 self.seconds = hours * 3600 + minutes * 60 + seconds
-1 81
-1 82 def __repr__(self):
-1 83 return (
-1 84 f'NominalDelta(months={self.months}, days={self.days}, '
-1 85 f'seconds={self.seconds})'
-1 86 )
-1 87
-1 88 @property
-1 89 def years(self: Self) -> int:
-1 90 return self.months // 12
-1 91
-1 92 def __eq__(self, other: Self) -> Self:
-1 93 if isinstance(other, NominalDelta):
-1 94 return (
-1 95 self.months == other.months
-1 96 and self.days == other.days
-1 97 and self.seconds == other.seconds
-1 98 )
-1 99 return NotImplemented
-1 100
-1 101 def __add__(self: Self, other: Self) -> Self:
-1 102 if isinstance(other, NominalDelta):
-1 103 return NominalDelta(
-1 104 months=self.months + other.months,
-1 105 days=self.days + other.days,
-1 106 seconds=self.seconds + other.seconds,
-1 107 )
-1 108 return NotImplemented
-1 109
-1 110 def __sub__(self: Self, other: Self) -> Self:
-1 111 if isinstance(other, NominalDelta):
-1 112 return NominalDelta(
-1 113 months=self.months - other.months,
-1 114 days=self.days - other.days,
-1 115 seconds=self.seconds - other.seconds,
-1 116 )
-1 117 return NotImplemented
-1 118
-1 119 def __neg__(self: Self) -> Self:
-1 120 return NominalDelta() - self
-1 121
-1 122 def __mul__(self: Self, factor: int) -> Self:
-1 123 if isinstance(factor, int):
-1 124 return NominalDelta(
-1 125 months=self.months * factor,
-1 126 days=self.days * factor,
-1 127 seconds=self.seconds * factor,
-1 128 )
-1 129 return NotImplemented
-1 130
-1 131 def __rmul__(self: Self, factor: int) -> Self:
-1 132 return self * factor
-1 133
-1 134 def __radd__(self: Self, other: date) -> date:
-1 135 if isinstance(other, datetime):
-1 136 return dt_add(other, self)
-1 137 elif isinstance(other, date):
-1 138 return date_add(other, self)
-1 139 return NotImplemented
-1 140
-1 141 def __rsub__(self: Self, other: date) -> date:
-1 142 return (-self).__radd__(other)
-1 143
-1 144 @classmethod
-1 145 def diff(cls, a: date, b: date):
-1 146 if isinstance(a, datetime) and isinstance(b, datetime):
-1 147 return dt_diff(a, b)
-1 148 elif isinstance(a, date) and isinstance(b, date):
-1 149 return date_diff(a, b)
-1 150 raise TypeError('Unsupported types')
diff --git a/test.py b/test.py
@@ -0,0 +1,171 @@
-1 1 import unittest
-1 2 from datetime import date
-1 3 from datetime import datetime
-1 4 from zoneinfo import ZoneInfo
-1 5
-1 6 from nominaldelta import NominalDelta
-1 7
-1 8
-1 9 class TestNominalDelta(unittest.TestCase):
-1 10 def test_years(self):
-1 11 self.assertEqual(NominalDelta(years=3, months=15).years, 4)
-1 12 self.assertEqual(NominalDelta(years=3, months=-1).years, 2)
-1 13
-1 14 def test_weeks(self):
-1 15 self.assertEqual(NominalDelta(weeks=2, days=2).days, 16)
-1 16 self.assertEqual(NominalDelta(weeks=2, days=-1).days, 13)
-1 17
-1 18 def test_add(self):
-1 19 self.assertEqual(
-1 20 NominalDelta(months=1) + NominalDelta(days=2),
-1 21 NominalDelta(months=1, days=2),
-1 22 )
-1 23 self.assertEqual(
-1 24 NominalDelta(months=1) + NominalDelta(months=2),
-1 25 NominalDelta(months=3),
-1 26 )
-1 27
-1 28 def test_sub(self):
-1 29 self.assertEqual(
-1 30 NominalDelta(months=1) - NominalDelta(days=2),
-1 31 NominalDelta(months=1, days=-2),
-1 32 )
-1 33 self.assertEqual(
-1 34 NominalDelta(months=1) - NominalDelta(months=2),
-1 35 NominalDelta(months=-1),
-1 36 )
-1 37
-1 38 def test_neg(self):
-1 39 self.assertEqual(
-1 40 -NominalDelta(months=1, days=-2),
-1 41 NominalDelta(months=-1, days=2),
-1 42 )
-1 43
-1 44 def test_mul(self):
-1 45 self.assertEqual(
-1 46 NominalDelta(months=1, days=-2) * 2,
-1 47 NominalDelta(months=2, days=-4),
-1 48 )
-1 49
-1 50 def test_rmul(self):
-1 51 self.assertEqual(
-1 52 2 * NominalDelta(months=1, days=-2),
-1 53 NominalDelta(months=2, days=-4),
-1 54 )
-1 55
-1 56 def test_radd_str_type_error(self):
-1 57 with self.assertRaises(TypeError):
-1 58 'asd' + NominalDelta(month=1)
-1 59
-1 60 with self.assertRaises(TypeError):
-1 61 NominalDelta(month=1) + 'asd'
-1 62
-1 63 def test_radd_date(self):
-1 64 self.assertEqual(
-1 65 date(1970, 1, 30) + NominalDelta(months=1),
-1 66 date(1970, 2, 28),
-1 67 )
-1 68
-1 69 def test_radd_datetime(self):
-1 70 self.assertEqual(
-1 71 datetime(1970, 1, 30, 13) + NominalDelta(months=1),
-1 72 datetime(1970, 2, 28, 13),
-1 73 )
-1 74 self.assertEqual(
-1 75 datetime(1970, 1, 30, 13) + NominalDelta(hours=2),
-1 76 datetime(1970, 1, 30, 15),
-1 77 )
-1 78 self.assertEqual(
-1 79 datetime(1970, 1, 30, 13) + NominalDelta(hours=24),
-1 80 datetime(1970, 1, 31, 13),
-1 81 )
-1 82 self.assertEqual(
-1 83 datetime(1970, 1, 30, 13) + NominalDelta(seconds=80),
-1 84 datetime(1970, 1, 30, 13, 1, 20),
-1 85 )
-1 86
-1 87 def test_radd_datetime_dst(self):
-1 88 tz = ZoneInfo('Europe/Berlin')
-1 89 self.assertEqual(
-1 90 datetime(2019, 3, 31, 1, 59, tzinfo=tz) + NominalDelta(minutes=2),
-1 91 datetime(2019, 3, 31, 3, 1, tzinfo=tz),
-1 92 )
-1 93 self.assertEqual(
-1 94 datetime(2019, 10, 27, 2, 59, tzinfo=tz) + NominalDelta(minutes=2),
-1 95 datetime(2019, 10, 27, 2, 1, fold=True, tzinfo=tz),
-1 96 )
-1 97
-1 98 def test_rsub_date(self):
-1 99 self.assertEqual(
-1 100 date(1970, 1, 30) - NominalDelta(days=5),
-1 101 date(1970, 1, 25),
-1 102 )
-1 103
-1 104 def test_rsub_datetime(self):
-1 105 self.assertEqual(
-1 106 datetime(1970, 1, 30, 13) - NominalDelta(days=1),
-1 107 datetime(1970, 1, 29, 13),
-1 108 )
-1 109 self.assertEqual(
-1 110 datetime(1970, 1, 30, 13) - NominalDelta(hours=2),
-1 111 datetime(1970, 1, 30, 11),
-1 112 )
-1 113 self.assertEqual(
-1 114 datetime(1970, 1, 30, 13) - NominalDelta(hours=24),
-1 115 datetime(1970, 1, 29, 13),
-1 116 )
-1 117 self.assertEqual(
-1 118 datetime(1970, 1, 30, 13) - NominalDelta(seconds=80),
-1 119 datetime(1970, 1, 30, 12, 58, 40),
-1 120 )
-1 121
-1 122 def test_rsub_datetime_dst(self):
-1 123 tz = ZoneInfo('Europe/Berlin')
-1 124 self.assertEqual(
-1 125 datetime(2019, 3, 31, 3, 1, tzinfo=tz) - NominalDelta(minutes=2),
-1 126 datetime(2019, 3, 31, 1, 59, tzinfo=tz),
-1 127 )
-1 128 self.assertEqual(
-1 129 datetime(2019, 10, 27, 2, 1, fold=True, tzinfo=tz)
-1 130 - NominalDelta(minutes=2),
-1 131 datetime(2019, 10, 27, 2, 59, tzinfo=tz),
-1 132 )
-1 133
-1 134 def test_diff_date(self):
-1 135 self.assertEqual(
-1 136 NominalDelta.diff(date(1970, 1, 15), date(1970, 2, 15)),
-1 137 NominalDelta(months=1),
-1 138 )
-1 139 self.assertEqual(
-1 140 NominalDelta.diff(date(1970, 2, 15), date(1970, 1, 15)),
-1 141 NominalDelta(months=-1),
-1 142 )
-1 143 self.assertEqual(
-1 144 NominalDelta.diff(date(1000, 1, 1), date(2000, 1, 1)),
-1 145 NominalDelta(months=12_000),
-1 146 )
-1 147 self.assertEqual(
-1 148 NominalDelta.diff(date(1970, 1, 15), date(1970, 1, 16)),
-1 149 NominalDelta(days=1),
-1 150 )
-1 151
-1 152 def test_diff_datetime(self):
-1 153 self.assertEqual(
-1 154 NominalDelta.diff(datetime(1970, 1, 30, 13), datetime(1970, 2, 28, 13)),
-1 155 NominalDelta(months=1),
-1 156 )
-1 157 self.assertEqual(
-1 158 NominalDelta.diff(datetime(1970, 1, 30, 13), datetime(1970, 1, 31, 13)),
-1 159 NominalDelta(days=1),
-1 160 )
-1 161 self.assertEqual(
-1 162 NominalDelta.diff(datetime(1970, 1, 30, 13), datetime(1970, 1, 30, 15)),
-1 163 NominalDelta(hours=2),
-1 164 )
-1 165 self.assertEqual(
-1 166 NominalDelta.diff(
-1 167 datetime(1970, 1, 30, 13),
-1 168 datetime(1970, 1, 30, 13, 1, 20),
-1 169 ),
-1 170 NominalDelta(seconds=80),
-1 171 )