Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
0e3e16b
indexing
KommuSoft Oct 5, 2025
3d68d1e
black
KommuSoft Oct 5, 2025
8a6b784
year prev
KommuSoft Oct 5, 2025
6c1fa4c
fix test
KommuSoft Oct 5, 2025
7ccdf38
black reformatting
KommuSoft Oct 5, 2025
07b0a64
indexes
KommuSoft Oct 6, 2025
b128213
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 6, 2025
9ea0b3b
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 6, 2025
26320bb
add readme
KommuSoft Oct 6, 2025
e391018
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 6, 2025
d77e04b
bugfix
KommuSoft Oct 6, 2025
f292d4c
quarter
KommuSoft Oct 7, 2025
cbfe24d
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 7, 2025
e206aaf
quarter
KommuSoft Oct 7, 2025
6dc9467
timeunit and timeunit subscriptable and sliceable
KommuSoft Oct 8, 2025
ab3c583
iterable
KommuSoft Oct 8, 2025
360fd15
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 8, 2025
6407573
📝 Add docstrings to `feature/get-index`
coderabbitai[bot] Oct 8, 2025
5e97d1b
Merge pull request #16 from hapytex/coderabbitai/docstrings/360fd15
KommuSoft Oct 8, 2025
6f8224e
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 8, 2025
0c31704
update readme
KommuSoft Oct 8, 2025
0174990
limit range
KommuSoft Oct 8, 2025
b11c55a
some rewrites
KommuSoft Oct 9, 2025
fed70bc
update readme
KommuSoft Oct 9, 2025
c842d6c
recursive slicing
KommuSoft Oct 10, 2025
2a29deb
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 10, 2025
80d8f9c
test slicing
KommuSoft Oct 11, 2025
744f35f
fix year problem?
KommuSoft Oct 11, 2025
04ba81b
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 11, 2025
e1d30f3
update README
KommuSoft Oct 11, 2025
dc5caa2
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 11, 2025
196cd37
islice
KommuSoft Oct 11, 2025
7b8c486
indexing Timeunit
KommuSoft Oct 11, 2025
3d94a19
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 11, 2025
47a1ea8
matrix
KommuSoft Oct 11, 2025
9aae40a
matrix
KommuSoft Oct 11, 2025
b36e7e0
random order
KommuSoft Oct 11, 2025
9592bb7
truncate
KommuSoft Oct 11, 2025
6df07aa
fix test error
KommuSoft Oct 11, 2025
627e11c
fix remarks
KommuSoft Oct 11, 2025
146359f
merge master into branch
KommuSoft Oct 11, 2025
c24b639
blackcp
KommuSoft Oct 11, 2025
e8c7efe
ℬ𝓁𝒶𝒸𝓀 reformatting
KommuSoft Oct 11, 2025
63d66b4
fix pipeline
KommuSoft Oct 11, 2025
cce7bff
aargh
KommuSoft Oct 11, 2025
e348f65
one day
KommuSoft Oct 11, 2025
d2c10e0
rm __int__
KommuSoft Oct 11, 2025
4e097a0
README formatting
KommuSoft Oct 11, 2025
e969811
README code blocks
KommuSoft Oct 11, 2025
0dc930f
test typeerror
KommuSoft Oct 11, 2025
37dfafb
more Py versions
KommuSoft Oct 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 5 additions & 16 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,12 @@ jobs:
- uses: psf/black@stable
with:
options: "--check"
pylint unit_of_time
test:
name: run tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand All @@ -32,20 +26,15 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Run test
run: |
pip install pytest-cov
pytest --junitxml=pytest.xml --cov-fail-under=100 --cov-report=term-missing:skip-covered --cov=unit_of_time timetest.py
pip install pytest-cov pytest-random-order
pytest --random-order --junitxml=pytest.xml --cov-fail-under=100 --cov-report=term-missing:skip-covered --cov=unit_of_time timetest.py
- name: Coveralls
uses: coverallsapp/github-action@v2
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- name: checkout code
uses: actions/checkout@v4
Expand Down
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,55 @@ for dt in Quarter(date(1958, 3, 25)):

we can also convert such collection to a list.

### Subscripting

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:

```python3
Week.get_index_for_date(date(1958, 3, 25)) # 102123
Week.get_date_from_index(102123) # date(1958, 3, 24)
```

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.

We can also use the index to get a `TimUnit` with:

Comment on lines +119 to +120
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Correct the “TimUnit” typo.
Line 119 refers to “TimUnit”; please fix to “Timeunit” to match the class name.

🤖 Prompt for AI Agents
In README.md around lines 119 to 120, fix the typo "TimUnit" to the correct
class name "Timeunit" so the documentation matches the actual class name; update
the inline text and any code snippets on these lines to use "Timeunit"
consistently.

```python3
Week[102123] # Week(date(1958, 3, 24))
```

moreover a week itself can be subscripted, for example:

```python3
Week(date(1958, 3, 24))[2] # date(1958, 3, 26)
```

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:

```python3
Week[102123:105341:2]
```

is a collection of `Week` objects between `1958-03-24` and `2019-11-25` each time with one week in between.


The `Week` class itself is also iterable, for example:

```python3
for week in Week:
print(week)
```

will start enumerating over all weeks since 0001-01-01.

A time unit also has a length: the number of time units that can be represented, so:

```python3
len(Week) # 521722
```

means the software can represent 521'722 weeks from 0001-01-01 to 9999-12-26.

### Shifting units of time

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

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

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.
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.

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

Expand Down
68 changes: 60 additions & 8 deletions timetest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import date, datetime, time, timedelta

from unit_of_time import Year, Quarter, Month, Week, Day, TimeunitKind, Timeunit
from itertools import islice


class Decade(TimeunitKind):
Expand All @@ -19,26 +20,52 @@ def truncate(cls, dt):
Returns:
date: The first day (January 1) of the decade in which `dt` falls.
"""
return date(10 * (dt.year // 10), 1, 1)
return date(max(10 * (dt.year // 10), 1), 1, 1)

@classmethod
def get_index_for_date(cls, dt):
"""
Return the zero-based decade index for the given date.

Parameters:
dt (date or datetime): The date for which to compute the decade index.

Returns:
int: The decade index equal to the calendar year divided by 10 using integer division (year // 10).
"""
return dt.year // 10
Comment on lines +25 to +36
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent indexing pattern with other time units.

Decade returns the absolute decade number (dt.year // 10), while all other units return offsets from date.min. For example:

  • Year: dt.year - date.min.year
  • Month: 12 * (dt.year - date.min.year) + dt.month - 1
  • Day: (dt - date.min).days

This works for the current date.min.year=1 but breaks the design pattern. If date.min.year changes, Decade would be incorrect.

Apply this diff to align with the offset-based pattern:

     @classmethod
     def get_index_for_date(cls, dt):
-        return dt.year // 10
+        return (dt.year - date.min.year) // 10

Note: You'll also need to implement get_date_from_index for Decade to support the full indexing API:

@classmethod
def get_date_from_index(cls, idx):
    return date((idx * 10) + date.min.year, 1, 1)
🤖 Prompt for AI Agents
In timetest.py around lines 24-26, get_index_for_date currently returns an
absolute decade (dt.year // 10) which is inconsistent with other units; change
it to return the offset-based decade index: (dt.year - date.min.year) // 10.
Also add a companion classmethod get_date_from_index that returns date((idx *
10) + date.min.year, 1, 1) so the decade indexing API is complete and consistent
with date.min offsets.


@classmethod
def get_date_from_index(cls, idx):
"""
Return the start date (January 1) of the decade represented by the given index.

Parameters:
idx (int): Decade index; the corresponding year is 10 * idx.

Returns:
datetime.date: January 1 of the year 10 * idx.
"""
return date(max(10 * idx, 1), 1, 1)

@classmethod
def last_day(cls, dt):
"""
Return the last day of the decade containing the given date.
Return the last date of the decade that contains the given date.

Parameters:
dt (date or datetime): The date for which to determine the last day of its decade.
dt (date | datetime): Date or datetime within the target decade.

Returns:
date: The last day of the decade as a date object.
date: The last day of that decade.
"""
dt = cls.truncate(dt)
return date(dt.year + 10, 1, 1) - timedelta(days=1)


TIME_UNITS = [Decade, Year, Quarter, Month, Week, Day]
START_DATE = date(1302, 7, 11)
END_DATE = date(2019, 11, 25)
START_DATE = date(902, 7, 11)
END_DATE = date(1019, 11, 25)


class TimeUnitTest(unittest.TestCase):
Expand Down Expand Up @@ -99,6 +126,17 @@ def test_to_int(self):
self.assertLess(tu, tu.next)
self.assertLessEqual(tu.previous, tu)
self.assertLessEqual(tu, tu.next)
idx = kind.get_index_for_date(tu.dt)
self.assertEqual(
idx,
kind.get_index_for_date(tu.next.dt) - 1,
)
self.assertEqual(tu.dt, kind.get_date_from_index(idx))
self.assertEqual(tu, kind[idx])
if dt == tu.first_date:
for idx2, dt2 in enumerate(tu):
self.assertEqual(idx, kind.get_index_for_date(dt2))
self.assertEqual(dt2, tu[idx2])
self.assertGreater(tu, tu.previous)
self.assertGreater(tu.next, tu)
self.assertGreaterEqual(tu, tu.previous)
Expand All @@ -109,6 +147,8 @@ def test_to_int(self):
self.assertEqual(TimeunitKind.from_int(int(tu)), tu)
self.assertIn(dt, tu)
self.assertIn((dt, dt), tu)
with self.assertRaises(TypeError):
(dt, None) in tu
self.assertIn((tu.first_date, tu.last_date), tu)
with self.assertRaises(TypeError):
self.assertIn(1425, tu)
Expand Down Expand Up @@ -151,8 +191,16 @@ def test_to_int(self):
self.assertEqual(tu.previous.previous.previous, 3 << tu)
self.assertLess(tu.last_date, tu.next.first_date)
self.assertLess(tu.previous.last_date, tu.first_date)
self.assertEqual((tu.next.first_date - tu.last_date), timedelta(days=1))
self.assertEqual((tu.first_date - tu.previous.last_date), timedelta(days=1))
self.assertEqual(
(tu.next.first_date - tu.last_date), timedelta(days=1)
)
self.assertEqual(
(tu.first_date - tu.previous.last_date), timedelta(days=1)
)

def test_repr(self):
self.assertEqual("Week", repr(Week))
self.assertEqual("Week[102123:105341:]", repr(Week[102123:105341:]))

def test_hierarchy(self):
"""
Expand All @@ -166,6 +214,7 @@ def test_hierarchy(self):
"""
for i, superkind in enumerate(TIME_UNITS, 1):
for kind in TIME_UNITS[i:]:
self.assertLess(superkind, kind)
for dt in self.date_range_yield():
with self.subTest(superkind=superkind, kind=kind, dt=dt):
stu = superkind(dt)
Expand Down Expand Up @@ -193,7 +242,10 @@ def test_kinds(self):
self.assertEqual(kind, kind.kind_int)
self.assertEqual(kind.kind_int, kind)
self.assertEqual(d[kind], kind in seen)
self.assertEqual(kind.get_index_for_date(date.min), 0)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
d[kind] = True
self.assertEqual(list(kind[10:110:10][5:9:2]), list(kind[60:100:20]))
self.assertEqual(list(islice(kind, 3)), list(kind[:3]))
self.assertNotIn(kind, seen)
seen.add(kind)
for kind2 in TIME_UNITS[i:]:
Expand Down
Loading