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.
- dvrd_pydate-1.0.0/PKG-INFO +182 -0
- dvrd_pydate-1.0.0/README.md +171 -0
- dvrd_pydate-1.0.0/pyproject.toml +24 -0
- dvrd_pydate-1.0.0/setup.cfg +4 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate/__init__.py +3 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate/enums.py +23 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate/pydate.py +215 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate/pydatetime.py +147 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate/tests/__init__.py +0 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate/tests/test_pydate.py +422 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate/tests/test_pydatetime.py +199 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate.egg-info/PKG-INFO +182 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate.egg-info/SOURCES.txt +14 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate.egg-info/dependency_links.txt +1 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate.egg-info/requires.txt +3 -0
- dvrd_pydate-1.0.0/src/dvrd_pydate.egg-info/top_level.txt +1 -0
|
@@ -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,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
|