dvrd-pydate 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.1
2
+ Name: dvrd_pydate
3
+ Version: 1.0.0
4
+ Summary: Dave van Rijn Development date extension
5
+ Author-email: Dave van Rijn Development <develop@davevanrijn.nl>
6
+ Project-URL: Homepage, https://github.com/Dave-van-Rijn-Development/dvrd-pydate.git
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Provides-Extra: test
10
+ Requires-Dist: coverage; extra == "test"
11
+
12
+ # dvrd_pydate
13
+
14
+ This package provides `date` and `datetime` extensions with useful extra utility functions.
15
+ The extensions are provided as the `PyDate(date)` and `PyDateTime(datetime, PyDate)` classes. All built-in `date` and
16
+ `datetime` functions are still available through inheritance.
17
+
18
+ ## Initialization
19
+
20
+ ### from_value
21
+
22
+ `PyDate` and `PyDateTime` objects can be constructed using the default `date`/`datetime` constructors or initializing
23
+ functions. They can also easily be constructed from existing `date`/`datetime` objects or their `Py*` variants, using
24
+ the staticmethod `from_value`.
25
+
26
+ ```python
27
+ from datetime import datetime, date
28
+ from dvrd_pydate import PyDate, PyDateTime
29
+
30
+ date_value = date(2024, 1, 1)
31
+ datetime_value = datetime(2024, 1, 1, 12, 0, 0, 0)
32
+
33
+ # All valid initializers
34
+ pydate_value = PyDate(2024, 1, 1)
35
+ pydate_value = PyDate.fromisoformat('2024-01-01')
36
+ pydate_value = PyDate.from_value(date_value)
37
+ pydate_value = PyDate(pydate_value)
38
+
39
+ pydatetime_value = PyDateTime(2024, 1, 1, 12, 0, 0, 0)
40
+ pydatetime_value = PyDateTime.fromisoformat('2024-01-01:12:00:00.000')
41
+ pydatetime_value = PyDateTime.from_value(datetime_value)
42
+ pydatetime_value = PyDateTime.from_value(pydatetime_value)
43
+ ```
44
+
45
+ | **Argument** | Type | Required | Default | **Description** |
46
+ |--------------|---------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------|
47
+ | value | `date \| str` | No | `None` | Construct a new PyDate(Time) object from the given value. If value is `None`, `date.today()` or `datetime.now()` is used instead. |
48
+
49
+ ### clone
50
+
51
+ Both classes provide a `clone` function, which simply clones the object into a new one. This function takes no
52
+ arguments.
53
+
54
+ ## Iteration
55
+
56
+ Both classes provide a staticmethod `iter` which returns a generator. The generator generates PyDate(Time)s with given
57
+ interval. It is possible to supply a start date, end date and max amounts of steps to take. If both `end` and
58
+ `max_steps` are given, the generator stops at whichever argument is reached first.
59
+
60
+ ```python
61
+ from dvrd_pydate import PyDate, DatePart
62
+
63
+ for date_value in PyDate.iter(end=PyDate.now().add(7, DatePart.DAYS)):
64
+ pass
65
+ for date_value in PyDate.iter(max_steps=7):
66
+ # Does the same as the loop above
67
+ pass
68
+ ```
69
+
70
+ | **Argument** | Type | Required | Default | **Description** |
71
+ |--------------|------------------------------------------------------------|----------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------|
72
+ | start | `date \| str` | No | `None` | Start iterating from date(time). If `None`, uses date.today()` or `datetime.now()` |
73
+ | end | `date \| str` | No | `None` | Optional date(time) to end the iteration at. |
74
+ | step | `DatePart \| TimePart \| tuple[int, DatePart \| TimePart]` | No | `DatePart.DAY` | Interval to determine each new date(time) with. `PyDate` can only use `DatePart`, while `PyDateTime` can use both `DatePart` as `TimePart`. |
75
+ | max_steps | `int` | No | None | Max amount of date(time)s to generate. |
76
+
77
+ ## Mutations
78
+
79
+ Both classes provide functions to alter the date or time. All functions return a new instance, mutations are not done
80
+ in-place. All functions can therefore also be chained together.
81
+
82
+ ### Add
83
+
84
+ Add an amount of date/time part. `PyDate` only supports `DatePart` parts, while `PyDateTime` supports both `DatePart` (
85
+ through inheritance) and `TimePart`.
86
+
87
+ ```python
88
+ from dvrd_pydate import PyDate, PyDateTime, DatePart, TimePart
89
+
90
+ date_value = PyDate.today()
91
+ date_value = date_value.add(7, DatePart.DAYS)
92
+ date_value = date_value.add(1, DatePart.WEEK) # Same as above
93
+
94
+ datetime_value = PyDateTime.now()
95
+ datetime_value = datetime_value.add(1, DatePart.MONTH).add(30, TimePart.SECONDS)
96
+ ```
97
+
98
+ | **Argument** | Type | Required | Default | **Description** |
99
+ |--------------|------------------------|----------|---------|--------------------------------------------|
100
+ | value | `int` | Yes | N/A | The value to add to the current date(time) |
101
+ | key | `DatePart \| TimePart` | Yes | N/A | The part to add the value to |
102
+
103
+ ### Subtract
104
+
105
+ Subtract an amount of date/time part. `PyDate` only supports `DatePart` parts, while `PyDateTime` supports both
106
+ `DatePart` (through inheritance) and `TimePart`.
107
+
108
+ ```python
109
+ from dvrd_pydate import PyDate, PyDateTime, DatePart, TimePart
110
+
111
+ date_value = PyDate.today()
112
+ date_value = date_value.subtract(7, DatePart.DAYS)
113
+ date_value = date_value.subtract(1, DatePart.WEEK) # Does the same as above
114
+
115
+ datetime_value = PyDateTime.now()
116
+ datetime_value = datetime_value.subtract(1, DatePart.MONTH).subtract(30, TimePart.SECONDS)
117
+ ```
118
+
119
+ #### Add/Subtract parts
120
+
121
+ Each part also has its own `add` and `subtract` function. E.g. `add_days(2)`, `add_hours(3)`, `subtract_months(4)`, etc.
122
+ Adding or subtracting with value `1` can also be achieved by using the utility functions `add_day()`, `add_hour()`,
123
+ `subtract_month()`, etc. which calls the `add`/`subtract` functions with value `1`.
124
+
125
+ ## start_of / end_of
126
+
127
+ Both classes provide the `start_of` and `end_of` functions to conveniently set the date(time) to the start of the given
128
+ date/time part.
129
+
130
+ ```python
131
+ from dvrd_pydate import PyDate, DatePart
132
+
133
+ date_value = PyDate(2024, 2, 5) # 5th of February 2024
134
+ start_of_month = date_value.start_of(DatePart.MONTH) # 1st of February 2024
135
+ end_of_month = date_value.end_of(DatePart.MONTH) # 29th of February 2024
136
+ ```
137
+
138
+ | **Argument** | Type | Required | Default | Description |
139
+ |--------------|------------------------|----------|---------|-------------------------------------------------------|
140
+ | part | `DatePart \| TimePart` | Yes | N/A | Determines to which part the date(time) is mutated to |
141
+
142
+ ## Comparison
143
+
144
+ Both classes provide convenient function to compare itself to another date(time). The following functions can be used:
145
+
146
+ - `is_before`
147
+ - `is_same_or_before`
148
+ - `is_same`
149
+ - `is_same_or_after`
150
+ - `is_after`
151
+ - `is_between`
152
+
153
+ ```python
154
+ from dvrd_pydate import PyDate, DatePart
155
+
156
+ date1 = PyDate(2024, 1, 1)
157
+ date2 = PyDate(2024, 1, 15)
158
+
159
+ date1.is_same(date2) # Granularity defaults to DatePart.DAY, returns False
160
+ date1.is_same(date2, DatePart.MONTH) # True
161
+ ```
162
+
163
+ All function return a `bool`. All functions except `is_between` take the following arguments:
164
+
165
+ | **Argument** | Type | Required | Default | Description |
166
+ |--------------|------------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
167
+ | other | `date(time) \| str` | Yes | N/A | Date(time) to compare to. Can also be a ISO date(time) string |
168
+ | granularity | `DatePart \| TimePart` | No | `DatePart.DAY` | Determines the exactness of the comparison. For example, this makes it easy to test if two dates are in the same month of the same year. |
169
+
170
+ The `is_between` function tests if the object is in between given date(time)s. It is possible to exclude the given start
171
+ and end date.
172
+
173
+ ```python
174
+ from dvrd_pydate import PyDate, DatePart
175
+ date1 = PyDate(2024, 1, 1)
176
+ date2 = PyDate(2024, 1, 15)
177
+ date3 = PyDate(2024, 1, 12)
178
+
179
+ date3.is_between(date1, date2) # True
180
+ date2.is_between(date1, date2) # True
181
+ date2.is_between(date1, date2, to_inclusive=False) # False
182
+ ```
@@ -0,0 +1,171 @@
1
+ # dvrd_pydate
2
+
3
+ This package provides `date` and `datetime` extensions with useful extra utility functions.
4
+ The extensions are provided as the `PyDate(date)` and `PyDateTime(datetime, PyDate)` classes. All built-in `date` and
5
+ `datetime` functions are still available through inheritance.
6
+
7
+ ## Initialization
8
+
9
+ ### from_value
10
+
11
+ `PyDate` and `PyDateTime` objects can be constructed using the default `date`/`datetime` constructors or initializing
12
+ functions. They can also easily be constructed from existing `date`/`datetime` objects or their `Py*` variants, using
13
+ the staticmethod `from_value`.
14
+
15
+ ```python
16
+ from datetime import datetime, date
17
+ from dvrd_pydate import PyDate, PyDateTime
18
+
19
+ date_value = date(2024, 1, 1)
20
+ datetime_value = datetime(2024, 1, 1, 12, 0, 0, 0)
21
+
22
+ # All valid initializers
23
+ pydate_value = PyDate(2024, 1, 1)
24
+ pydate_value = PyDate.fromisoformat('2024-01-01')
25
+ pydate_value = PyDate.from_value(date_value)
26
+ pydate_value = PyDate(pydate_value)
27
+
28
+ pydatetime_value = PyDateTime(2024, 1, 1, 12, 0, 0, 0)
29
+ pydatetime_value = PyDateTime.fromisoformat('2024-01-01:12:00:00.000')
30
+ pydatetime_value = PyDateTime.from_value(datetime_value)
31
+ pydatetime_value = PyDateTime.from_value(pydatetime_value)
32
+ ```
33
+
34
+ | **Argument** | Type | Required | Default | **Description** |
35
+ |--------------|---------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------|
36
+ | value | `date \| str` | No | `None` | Construct a new PyDate(Time) object from the given value. If value is `None`, `date.today()` or `datetime.now()` is used instead. |
37
+
38
+ ### clone
39
+
40
+ Both classes provide a `clone` function, which simply clones the object into a new one. This function takes no
41
+ arguments.
42
+
43
+ ## Iteration
44
+
45
+ Both classes provide a staticmethod `iter` which returns a generator. The generator generates PyDate(Time)s with given
46
+ interval. It is possible to supply a start date, end date and max amounts of steps to take. If both `end` and
47
+ `max_steps` are given, the generator stops at whichever argument is reached first.
48
+
49
+ ```python
50
+ from dvrd_pydate import PyDate, DatePart
51
+
52
+ for date_value in PyDate.iter(end=PyDate.now().add(7, DatePart.DAYS)):
53
+ pass
54
+ for date_value in PyDate.iter(max_steps=7):
55
+ # Does the same as the loop above
56
+ pass
57
+ ```
58
+
59
+ | **Argument** | Type | Required | Default | **Description** |
60
+ |--------------|------------------------------------------------------------|----------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------|
61
+ | start | `date \| str` | No | `None` | Start iterating from date(time). If `None`, uses date.today()` or `datetime.now()` |
62
+ | end | `date \| str` | No | `None` | Optional date(time) to end the iteration at. |
63
+ | step | `DatePart \| TimePart \| tuple[int, DatePart \| TimePart]` | No | `DatePart.DAY` | Interval to determine each new date(time) with. `PyDate` can only use `DatePart`, while `PyDateTime` can use both `DatePart` as `TimePart`. |
64
+ | max_steps | `int` | No | None | Max amount of date(time)s to generate. |
65
+
66
+ ## Mutations
67
+
68
+ Both classes provide functions to alter the date or time. All functions return a new instance, mutations are not done
69
+ in-place. All functions can therefore also be chained together.
70
+
71
+ ### Add
72
+
73
+ Add an amount of date/time part. `PyDate` only supports `DatePart` parts, while `PyDateTime` supports both `DatePart` (
74
+ through inheritance) and `TimePart`.
75
+
76
+ ```python
77
+ from dvrd_pydate import PyDate, PyDateTime, DatePart, TimePart
78
+
79
+ date_value = PyDate.today()
80
+ date_value = date_value.add(7, DatePart.DAYS)
81
+ date_value = date_value.add(1, DatePart.WEEK) # Same as above
82
+
83
+ datetime_value = PyDateTime.now()
84
+ datetime_value = datetime_value.add(1, DatePart.MONTH).add(30, TimePart.SECONDS)
85
+ ```
86
+
87
+ | **Argument** | Type | Required | Default | **Description** |
88
+ |--------------|------------------------|----------|---------|--------------------------------------------|
89
+ | value | `int` | Yes | N/A | The value to add to the current date(time) |
90
+ | key | `DatePart \| TimePart` | Yes | N/A | The part to add the value to |
91
+
92
+ ### Subtract
93
+
94
+ Subtract an amount of date/time part. `PyDate` only supports `DatePart` parts, while `PyDateTime` supports both
95
+ `DatePart` (through inheritance) and `TimePart`.
96
+
97
+ ```python
98
+ from dvrd_pydate import PyDate, PyDateTime, DatePart, TimePart
99
+
100
+ date_value = PyDate.today()
101
+ date_value = date_value.subtract(7, DatePart.DAYS)
102
+ date_value = date_value.subtract(1, DatePart.WEEK) # Does the same as above
103
+
104
+ datetime_value = PyDateTime.now()
105
+ datetime_value = datetime_value.subtract(1, DatePart.MONTH).subtract(30, TimePart.SECONDS)
106
+ ```
107
+
108
+ #### Add/Subtract parts
109
+
110
+ Each part also has its own `add` and `subtract` function. E.g. `add_days(2)`, `add_hours(3)`, `subtract_months(4)`, etc.
111
+ Adding or subtracting with value `1` can also be achieved by using the utility functions `add_day()`, `add_hour()`,
112
+ `subtract_month()`, etc. which calls the `add`/`subtract` functions with value `1`.
113
+
114
+ ## start_of / end_of
115
+
116
+ Both classes provide the `start_of` and `end_of` functions to conveniently set the date(time) to the start of the given
117
+ date/time part.
118
+
119
+ ```python
120
+ from dvrd_pydate import PyDate, DatePart
121
+
122
+ date_value = PyDate(2024, 2, 5) # 5th of February 2024
123
+ start_of_month = date_value.start_of(DatePart.MONTH) # 1st of February 2024
124
+ end_of_month = date_value.end_of(DatePart.MONTH) # 29th of February 2024
125
+ ```
126
+
127
+ | **Argument** | Type | Required | Default | Description |
128
+ |--------------|------------------------|----------|---------|-------------------------------------------------------|
129
+ | part | `DatePart \| TimePart` | Yes | N/A | Determines to which part the date(time) is mutated to |
130
+
131
+ ## Comparison
132
+
133
+ Both classes provide convenient function to compare itself to another date(time). The following functions can be used:
134
+
135
+ - `is_before`
136
+ - `is_same_or_before`
137
+ - `is_same`
138
+ - `is_same_or_after`
139
+ - `is_after`
140
+ - `is_between`
141
+
142
+ ```python
143
+ from dvrd_pydate import PyDate, DatePart
144
+
145
+ date1 = PyDate(2024, 1, 1)
146
+ date2 = PyDate(2024, 1, 15)
147
+
148
+ date1.is_same(date2) # Granularity defaults to DatePart.DAY, returns False
149
+ date1.is_same(date2, DatePart.MONTH) # True
150
+ ```
151
+
152
+ All function return a `bool`. All functions except `is_between` take the following arguments:
153
+
154
+ | **Argument** | Type | Required | Default | Description |
155
+ |--------------|------------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
156
+ | other | `date(time) \| str` | Yes | N/A | Date(time) to compare to. Can also be a ISO date(time) string |
157
+ | granularity | `DatePart \| TimePart` | No | `DatePart.DAY` | Determines the exactness of the comparison. For example, this makes it easy to test if two dates are in the same month of the same year. |
158
+
159
+ The `is_between` function tests if the object is in between given date(time)s. It is possible to exclude the given start
160
+ and end date.
161
+
162
+ ```python
163
+ from dvrd_pydate import PyDate, DatePart
164
+ date1 = PyDate(2024, 1, 1)
165
+ date2 = PyDate(2024, 1, 15)
166
+ date3 = PyDate(2024, 1, 12)
167
+
168
+ date3.is_between(date1, date2) # True
169
+ date2.is_between(date1, date2) # True
170
+ date2.is_between(date1, date2, to_inclusive=False) # False
171
+ ```
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "dvrd_pydate"
3
+ version = "1.0.0"
4
+ description = "Dave van Rijn Development date extension"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ authors = [{ name = "Dave van Rijn Development", email = "develop@davevanrijn.nl" }]
8
+
9
+ [project.urls]
10
+ Homepage = "https://github.com/Dave-van-Rijn-Development/dvrd-pydate.git"
11
+
12
+ [project.optional-dependencies]
13
+ test = [
14
+ 'coverage'
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["setuptools>=62.6", "wheel"]
19
+ build-backend = "setuptools.build_meta"
20
+
21
+ [tool.coverage.run]
22
+ omit = [
23
+ "*/tests/*"
24
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .pydate import PyDate
2
+ from .pydatetime import PyDateTime
3
+ from .enums import DatePart, TimePart
@@ -0,0 +1,23 @@
1
+ from enum import Enum
2
+
3
+
4
+ class DatePart(Enum):
5
+ YEAR = 'year'
6
+ YEARS = 'years'
7
+ MONTH = 'month'
8
+ MONTHS = 'months'
9
+ DAY = 'day'
10
+ DAYS = 'days'
11
+ WEEK = 'week'
12
+ WEEKS = 'weeks'
13
+
14
+
15
+ class TimePart(Enum):
16
+ HOUR = 'hour'
17
+ HOURS = 'hours'
18
+ MINUTE = 'minute'
19
+ MINUTES = 'minutes'
20
+ SECOND = 'second'
21
+ SECONDS = 'seconds'
22
+ MICROSECOND = 'microsecond'
23
+ MICROSECONDS = 'microseconds'
@@ -0,0 +1,215 @@
1
+ from calendar import monthrange
2
+ from datetime import date, timedelta
3
+ from typing import Self, Generator
4
+
5
+ from dvrd_pydate.enums import DatePart, TimePart
6
+
7
+ days_in_week = 7
8
+ months_in_year = 12
9
+
10
+
11
+ class PyDate(date):
12
+ @staticmethod
13
+ def from_value(value: date | str = None):
14
+ if isinstance(value, str):
15
+ value = date.fromisoformat(value)
16
+ elif value is None:
17
+ value = date.today()
18
+ return PyDate(value.year, value.month, value.day)
19
+
20
+ @staticmethod
21
+ def iter(*, start: date | str = None, end: date | str | None = None,
22
+ step: DatePart | TimePart | tuple[int, DatePart | TimePart] = DatePart.DAY, max_steps: int = None) -> \
23
+ Generator["PyDate", None, None]:
24
+ if max_steps == 0:
25
+ # Raises StopIteration
26
+ return
27
+ if isinstance(step, TimePart):
28
+ raise KeyError('Cannot use time parts in PyDate')
29
+ elif isinstance(step, tuple):
30
+ if isinstance((step_key := step[1]), TimePart):
31
+ raise KeyError('Cannot use time parts in PyDate')
32
+ step_value = step[0]
33
+ else:
34
+ step_value = 1
35
+ step_key = step
36
+
37
+ if start is None:
38
+ start = date.today()
39
+ current = PyDate.from_value(start)
40
+ end_value = None if end is None else PyDate.from_value(end)
41
+ current_step = 0
42
+ while end_value is None or current < end_value:
43
+ yield current
44
+ current_step += 1
45
+ if max_steps is not None and current_step == max_steps:
46
+ break
47
+ current = current.add(value=step_value, key=step_key)
48
+
49
+ @property
50
+ def max_day(self) -> int:
51
+ return monthrange(self.year, self.month)[1]
52
+
53
+ def add(self, value: int, key: DatePart) -> Self:
54
+ if key in [DatePart.YEAR, DatePart.YEARS]:
55
+ return self.add_years(value)
56
+ elif key in [DatePart.MONTH, DatePart.MONTHS]:
57
+ return self.add_months(value)
58
+ elif key in [DatePart.WEEK, DatePart.WEEKS]:
59
+ return self.add_weeks(value)
60
+ elif key in [DatePart.DAY, DatePart.DAYS]:
61
+ return self.add_days(value)
62
+ else:
63
+ raise KeyError(f'Key "{key}" cannot be used in PyDate')
64
+
65
+ def subtract(self, value: int, key: DatePart) -> Self:
66
+ if key in [DatePart.YEAR, DatePart.YEARS]:
67
+ return self.subtract_years(value)
68
+ elif key in [DatePart.MONTH, DatePart.MONTHS]:
69
+ return self.subtract_months(value)
70
+ elif key in [DatePart.WEEK, DatePart.WEEKS]:
71
+ return self.subtract_weeks(value)
72
+ elif key in [DatePart.DAY, DatePart.DAYS]:
73
+ return self.subtract_days(value)
74
+ else:
75
+ raise KeyError(f'Key "{key}" cannot be used in PyDate')
76
+
77
+ def add_years(self, value: int) -> Self:
78
+ return self.replace(year=self.year + value)
79
+
80
+ def add_year(self) -> Self:
81
+ return self.add_years(1)
82
+
83
+ def subtract_years(self, value: int) -> Self:
84
+ return self.replace(year=self.year - value)
85
+
86
+ def subtract_year(self) -> Self:
87
+ return self.subtract_years(1)
88
+
89
+ def add_months(self, value: int) -> Self:
90
+ new_date = self.clone()
91
+ add_years, month_value = divmod(new_date.month + value, months_in_year + 1)
92
+ if add_years:
93
+ month_value += 1
94
+ year = new_date.year + add_years
95
+ max_date = monthrange(year, month_value)[1]
96
+ return new_date.replace(year=new_date.year + add_years, month=month_value, day=min(max_date, new_date.day))
97
+
98
+ def add_month(self) -> Self:
99
+ return self.add_months(1)
100
+
101
+ def subtract_months(self, value: int) -> Self:
102
+ new_date = self.clone()
103
+ subtract_years, remaining_months = divmod(value, months_in_year)
104
+ if subtract_years:
105
+ new_date = new_date.subtract_years(subtract_years)
106
+ if (month_value := new_date.month - remaining_months) < 1:
107
+ new_date = new_date.subtract_year()
108
+ month_value = 12 - abs(month_value)
109
+ max_date = monthrange(new_date.year, month_value)[1]
110
+ return new_date.replace(month=month_value, day=min(new_date.day, max_date))
111
+
112
+ def subtract_month(self) -> Self:
113
+ return self.subtract_months(1)
114
+
115
+ def add_weeks(self, value: int) -> Self:
116
+ return self + timedelta(weeks=value)
117
+
118
+ def add_week(self) -> Self:
119
+ return self.add_weeks(1)
120
+
121
+ def subtract_weeks(self, value: int) -> Self:
122
+ return self - timedelta(weeks=value)
123
+
124
+ def subtract_week(self) -> Self:
125
+ return self.subtract_weeks(1)
126
+
127
+ def add_days(self, value: int) -> Self:
128
+ return self + timedelta(days=value)
129
+
130
+ def add_day(self) -> Self:
131
+ return self.add_days(1)
132
+
133
+ def subtract_days(self, value: int) -> Self:
134
+ return self - timedelta(days=value)
135
+
136
+ def subtract_day(self) -> Self:
137
+ return self.subtract_days(1)
138
+
139
+ def clone(self) -> "PyDate":
140
+ return type(self).from_value(self)
141
+
142
+ def start_of(self, part: DatePart | TimePart) -> Self:
143
+ if isinstance(part, TimePart):
144
+ raise KeyError('Time part cannot be used in PyDate')
145
+ if part in [DatePart.YEAR, DatePart.YEARS]:
146
+ return self.replace(month=1, day=1)
147
+ elif part in [DatePart.MONTH, DatePart.MONTHS]:
148
+ return self.replace(day=1)
149
+ elif part in [DatePart.WEEK, DatePart.WEEKS]:
150
+ current_weekday = self.weekday()
151
+ return self.replace(day=self.day - current_weekday)
152
+ elif part in [DatePart.DAY, DatePart.DAYS]:
153
+ return self
154
+ else:
155
+ raise KeyError(f'Unsupported start_of part {part}')
156
+
157
+ def end_of(self, part: DatePart | TimePart) -> Self:
158
+ if isinstance(part, TimePart):
159
+ raise KeyError('Time part cannot be used in PyDate')
160
+ if part in [DatePart.YEAR, DatePart.YEARS]:
161
+ return self.replace(month=12, day=31)
162
+ elif part in [DatePart.MONTH, DatePart.MONTHS]:
163
+ return self.replace(day=self.max_day)
164
+ elif part in [DatePart.WEEK, DatePart.WEEKS]:
165
+ current_day = self.weekday()
166
+ return self.replace(day=self.day + 6 - current_day)
167
+ elif part in [DatePart.DAY, DatePart.DAYS]:
168
+ return self
169
+ else:
170
+ raise KeyError(f'Unsupported end_of part {part}')
171
+
172
+ def is_before(self, other: date | str, granularity: DatePart | TimePart = DatePart.DAY) -> bool:
173
+ if not isinstance(other, PyDate):
174
+ other = PyDate.from_value(other)
175
+ return self.start_of(granularity) < other.start_of(granularity)
176
+
177
+ def is_same_or_before(self, other: date | str, granularity: DatePart | TimePart = DatePart.DAY) -> bool:
178
+ if not isinstance(other, PyDate):
179
+ other = PyDate.from_value(other)
180
+ return self.start_of(granularity) <= other.start_of(granularity)
181
+
182
+ def is_same(self, other: date | str, granularity: DatePart | TimePart = DatePart.DAY) -> bool:
183
+ if not isinstance(other, PyDate):
184
+ other = PyDate.from_value(other)
185
+ return self.start_of(granularity) == other.start_of(granularity)
186
+
187
+ def is_same_or_after(self, other: date | str, granularity: DatePart | TimePart = DatePart.DAY) -> bool:
188
+ if not isinstance(other, PyDate):
189
+ other = PyDate.from_value(other)
190
+ return self.start_of(granularity) >= other.start_of(granularity)
191
+
192
+ def is_after(self, other: date | str, granularity: DatePart | TimePart = DatePart.DAY) -> bool:
193
+ if not isinstance(other, PyDate):
194
+ other = PyDate.from_value(other)
195
+ return self.start_of(granularity) > other.start_of(granularity)
196
+
197
+ def is_between(self, other1: date | str, other2: date | str, *, granularity: DatePart = DatePart.DAY,
198
+ from_inclusive: bool = True, to_inclusive: bool = True) -> bool:
199
+ if not isinstance(other1, PyDate):
200
+ other1 = PyDate.from_value(other1)
201
+ if not isinstance(other2, PyDate):
202
+ other2 = PyDate.from_value(other2)
203
+ from_date = min(other1, other2)
204
+ to_date = max(other1, other2)
205
+ if from_inclusive:
206
+ if not self.is_same_or_after(from_date, granularity):
207
+ return False
208
+ elif not self.is_after(from_date, granularity):
209
+ return False
210
+ if to_inclusive:
211
+ if not self.is_same_or_before(to_date, granularity):
212
+ return False
213
+ elif not self.is_before(to_date, granularity):
214
+ return False
215
+ return True