Skip to content

Commit c4e8dc2

Browse files
authored
Merge pull request #15 from hapytex/feature/get-index
indexing and slicing
2 parents cda9b9f + 37dfafb commit c4e8dc2

File tree

4 files changed

+570
-36
lines changed

4 files changed

+570
-36
lines changed

.github/workflows/build.yml

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,12 @@ jobs:
1212
- uses: psf/black@stable
1313
with:
1414
options: "--check"
15-
pylint unit_of_time
1615
test:
1716
name: run tests
18-
runs-on: ubuntu-latest
1917
strategy:
2018
matrix:
21-
python-version:
22-
- '3.9'
23-
- '3.10'
24-
- '3.11'
25-
- '3.12'
26-
- '3.13'
19+
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
20+
runs-on: ubuntu-latest
2721
steps:
2822
- uses: actions/checkout@v4
2923
- name: Set up Python
@@ -32,20 +26,15 @@ jobs:
3226
python-version: ${{ matrix.python-version }}
3327
- name: Run test
3428
run: |
35-
pip install pytest-cov
36-
pytest --junitxml=pytest.xml --cov-fail-under=100 --cov-report=term-missing:skip-covered --cov=unit_of_time timetest.py
29+
pip install pytest-cov pytest-random-order
30+
pytest --random-order --junitxml=pytest.xml --cov-fail-under=100 --cov-report=term-missing:skip-covered --cov=unit_of_time timetest.py
3731
- name: Coveralls
3832
uses: coverallsapp/github-action@v2
3933
build:
4034
runs-on: ubuntu-latest
4135
strategy:
4236
matrix:
43-
python-version:
44-
- '3.9'
45-
- '3.10'
46-
- '3.11'
47-
- '3.12'
48-
- '3.13'
37+
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
4938
steps:
5039
- name: checkout code
5140
uses: actions/checkout@v4

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,55 @@ for dt in Quarter(date(1958, 3, 25)):
105105

106106
we can also convert such collection to a list.
107107

108+
### Subscripting
109+
110+
The `Day`, `Week`, `Month, etc. classes have `.get_index_for_date(…)` and `.get_date_from_index(…)` methods, which allow to determine how many days, weeks, months, quarters and years are between `date.min` and the date given, and convert this back to a date. For example:
111+
112+
```python3
113+
Week.get_index_for_date(date(1958, 3, 25)) # 102123
114+
Week.get_date_from_index(102123) # date(1958, 3, 24)
115+
```
116+
117+
so 1958-03-25 is the 102'123 week since 0001-01-01, and that week starts the 24<sup>th</sup> of March, 1958.
118+
119+
We can also use the index to get a `TimUnit` with:
120+
121+
```python3
122+
Week[102123] # Week(date(1958, 3, 24))
123+
```
124+
125+
moreover a week itself can be subscripted, for example:
126+
127+
```python3
128+
Week(date(1958, 3, 24))[2] # date(1958, 3, 26)
129+
```
130+
131+
one can also slice to created an object that is a sliced "view" that generates `Week`s or `date`s in the week respectively. This view can then be sliced or indexed further. For example:
132+
133+
```python3
134+
Week[102123:105341:2]
135+
```
136+
137+
is a collection of `Week` objects between `1958-03-24` and `2019-11-25` each time with one week in between.
138+
139+
140+
The `Week` class itself is also iterable, for example:
141+
142+
```python3
143+
for week in Week:
144+
print(week)
145+
```
146+
147+
will start enumerating over all weeks since 0001-01-01.
148+
149+
A time unit also has a length: the number of time units that can be represented, so:
150+
151+
```python3
152+
len(Week) # 521722
153+
```
154+
155+
means the software can represent 521'722 weeks from 0001-01-01 to 9999-12-26.
156+
108157
### Shifting units of time
109158

110159
The units of time can also be shifted, for example:
@@ -181,7 +230,7 @@ class Decade(TimeunitKind):
181230

182231
this might be useful if the formatting is more advanced than what Python's date formatter can handle.
183232

184-
Furthermore, one implements the `.truncate(..)` class method to convert a date to the start of the date range, and the `_next(..)` which returns the first date for the next decade.
233+
Furthermore, one implements the `.truncate()` class method to convert a date to the start of the date range, and the `_next()` which returns the first date for the next decade.
185234

186235
With these functions, we have registered a new time unit.
187236

timetest.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import date, datetime, time, timedelta
33

44
from unit_of_time import Year, Quarter, Month, Week, Day, TimeunitKind, Timeunit
5+
from itertools import islice
56

67

78
class Decade(TimeunitKind):
@@ -19,26 +20,52 @@ def truncate(cls, dt):
1920
Returns:
2021
date: The first day (January 1) of the decade in which `dt` falls.
2122
"""
22-
return date(10 * (dt.year // 10), 1, 1)
23+
return date(max(10 * (dt.year // 10), 1), 1, 1)
24+
25+
@classmethod
26+
def get_index_for_date(cls, dt):
27+
"""
28+
Return the zero-based decade index for the given date.
29+
30+
Parameters:
31+
dt (date or datetime): The date for which to compute the decade index.
32+
33+
Returns:
34+
int: The decade index equal to the calendar year divided by 10 using integer division (year // 10).
35+
"""
36+
return dt.year // 10
37+
38+
@classmethod
39+
def get_date_from_index(cls, idx):
40+
"""
41+
Return the start date (January 1) of the decade represented by the given index.
42+
43+
Parameters:
44+
idx (int): Decade index; the corresponding year is 10 * idx.
45+
46+
Returns:
47+
datetime.date: January 1 of the year 10 * idx.
48+
"""
49+
return date(max(10 * idx, 1), 1, 1)
2350

2451
@classmethod
2552
def last_day(cls, dt):
2653
"""
27-
Return the last day of the decade containing the given date.
54+
Return the last date of the decade that contains the given date.
2855
2956
Parameters:
30-
dt (date or datetime): The date for which to determine the last day of its decade.
57+
dt (date | datetime): Date or datetime within the target decade.
3158
3259
Returns:
33-
date: The last day of the decade as a date object.
60+
date: The last day of that decade.
3461
"""
3562
dt = cls.truncate(dt)
3663
return date(dt.year + 10, 1, 1) - timedelta(days=1)
3764

3865

3966
TIME_UNITS = [Decade, Year, Quarter, Month, Week, Day]
40-
START_DATE = date(1302, 7, 11)
41-
END_DATE = date(2019, 11, 25)
67+
START_DATE = date(902, 7, 11)
68+
END_DATE = date(1019, 11, 25)
4269

4370

4471
class TimeUnitTest(unittest.TestCase):
@@ -99,6 +126,17 @@ def test_to_int(self):
99126
self.assertLess(tu, tu.next)
100127
self.assertLessEqual(tu.previous, tu)
101128
self.assertLessEqual(tu, tu.next)
129+
idx = kind.get_index_for_date(tu.dt)
130+
self.assertEqual(
131+
idx,
132+
kind.get_index_for_date(tu.next.dt) - 1,
133+
)
134+
self.assertEqual(tu.dt, kind.get_date_from_index(idx))
135+
self.assertEqual(tu, kind[idx])
136+
if dt == tu.first_date:
137+
for idx2, dt2 in enumerate(tu):
138+
self.assertEqual(idx, kind.get_index_for_date(dt2))
139+
self.assertEqual(dt2, tu[idx2])
102140
self.assertGreater(tu, tu.previous)
103141
self.assertGreater(tu.next, tu)
104142
self.assertGreaterEqual(tu, tu.previous)
@@ -109,6 +147,8 @@ def test_to_int(self):
109147
self.assertEqual(TimeunitKind.from_int(int(tu)), tu)
110148
self.assertIn(dt, tu)
111149
self.assertIn((dt, dt), tu)
150+
with self.assertRaises(TypeError):
151+
(dt, None) in tu
112152
self.assertIn((tu.first_date, tu.last_date), tu)
113153
with self.assertRaises(TypeError):
114154
self.assertIn(1425, tu)
@@ -151,8 +191,16 @@ def test_to_int(self):
151191
self.assertEqual(tu.previous.previous.previous, 3 << tu)
152192
self.assertLess(tu.last_date, tu.next.first_date)
153193
self.assertLess(tu.previous.last_date, tu.first_date)
154-
self.assertEqual((tu.next.first_date - tu.last_date), timedelta(days=1))
155-
self.assertEqual((tu.first_date - tu.previous.last_date), timedelta(days=1))
194+
self.assertEqual(
195+
(tu.next.first_date - tu.last_date), timedelta(days=1)
196+
)
197+
self.assertEqual(
198+
(tu.first_date - tu.previous.last_date), timedelta(days=1)
199+
)
200+
201+
def test_repr(self):
202+
self.assertEqual("Week", repr(Week))
203+
self.assertEqual("Week[102123:105341:]", repr(Week[102123:105341:]))
156204

157205
def test_hierarchy(self):
158206
"""
@@ -166,6 +214,7 @@ def test_hierarchy(self):
166214
"""
167215
for i, superkind in enumerate(TIME_UNITS, 1):
168216
for kind in TIME_UNITS[i:]:
217+
self.assertLess(superkind, kind)
169218
for dt in self.date_range_yield():
170219
with self.subTest(superkind=superkind, kind=kind, dt=dt):
171220
stu = superkind(dt)
@@ -193,7 +242,10 @@ def test_kinds(self):
193242
self.assertEqual(kind, kind.kind_int)
194243
self.assertEqual(kind.kind_int, kind)
195244
self.assertEqual(d[kind], kind in seen)
245+
self.assertEqual(kind.get_index_for_date(date.min), 0)
196246
d[kind] = True
247+
self.assertEqual(list(kind[10:110:10][5:9:2]), list(kind[60:100:20]))
248+
self.assertEqual(list(islice(kind, 3)), list(kind[:3]))
197249
self.assertNotIn(kind, seen)
198250
seen.add(kind)
199251
for kind2 in TIME_UNITS[i:]:

0 commit comments

Comments
 (0)