Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 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
49 changes: 49 additions & 0 deletions 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:

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

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix inline code formatting and class name typo.

The inline code span currently reads Month, etc., which swallows the trailing text and leaves TimUnit misspelled. Please close the Month code span before the comma and spell the class name as Timeunit to match the API.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

110-110: Spaces inside code span elements

(MD038, no-space-in-code)


112-112: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In README.md around lines 110 to 118 the inline code span for "Month" is not
closed before the comma and the class name "TimUnit" is misspelled; fix the
markdown by closing the inline code span for Month (e.g. `Month`) before the
comma, and correct the class name to `Timeunit` (or use `Timeunit` in backticks
wherever referenced) so the inline code formatting and API name are consistent.

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

Comment on lines +119 to +120
Copy link
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.

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

moreover a week itself can be subscripted, for example:

```
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:

```
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:

```
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:

```
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
47 changes: 42 additions & 5 deletions timetest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,50 @@ def truncate(cls, dt):
"""
return date(10 * (dt.year // 10), 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

@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(10 * idx, 1, 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix the index calculation to be consistent with other time units.

The current implementation has two issues:

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

    • Year: dt.year - date.min.year
    • Month: 12 * (dt.year - date.min.year) + dt.month - 1
  2. Broken for index 0: get_date_from_index(0) returns date(0, 1, 1), which raises ValueError because year 0 doesn't exist in the Gregorian calendar. While the current tests don't hit this case (START_DATE is year 902), any code calling Decade.get_date_from_index(0) or Decade[0] will fail.

Apply this diff to fix both issues:

     @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).
+            int: The decade index relative to date.min, computed as (year - date.min.year) // 10.
         """
-        return dt.year // 10
+        return (dt.year - date.min.year) // 10
 
     @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.
+            idx (int): Decade index relative to date.min; the corresponding year is (10 * idx) + date.min.year.
 
         Returns:
-            datetime.date: January 1 of the year 10 * idx.
+            datetime.date: January 1 of the year (10 * idx) + date.min.year.
         """
-        return date(10 * idx, 1, 1)
+        return date((10 * idx) + date.min.year, 1, 1)

This ensures:

  • Decade.get_index_for_date(date.min) returns 0 (for year 1: (1 - 1) // 10 = 0)
  • Decade.get_date_from_index(0) returns date(1, 1, 1) (valid)
  • The pattern is consistent with Year, Quarter, Month, Week, and Day
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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
@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(10 * idx, 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 relative to date.min, computed as (year - date.min.year) // 10.
"""
return (dt.year - date.min.year) // 10
@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 relative to date.min; the corresponding year is (10 * idx) + date.min.year.
Returns:
datetime.date: January 1 of the year (10 * idx) + date.min.year.
"""
return date((10 * idx) + date.min.year, 1, 1)
🤖 Prompt for AI Agents
In timetest.py around lines 24 to 48, Decade.get_index_for_date and
get_date_from_index are inconsistent with other units and produce invalid year
0; change get_index_for_date to compute the offset from date.min (i.e. (dt.year
- date.min.year) // 10) so date.min maps to index 0, and change
get_date_from_index to construct the decade start using that offset added back
to date.min.year (i.e. year = 10 * idx + date.min.year) so
get_date_from_index(0) returns a valid date(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 +125,15 @@ 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])
for dt2 in tu:
self.assertEqual(idx, kind.get_index_for_date(dt2))
self.assertGreater(tu, tu.previous)
self.assertGreater(tu.next, tu)
self.assertGreaterEqual(tu, tu.previous)
Expand Down Expand Up @@ -197,7 +232,9 @@ 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)
d[kind] = True
self.assertEqual(list(kind[10:110:10][5:9:2]), list(kind[60:100:20]))
self.assertNotIn(kind, seen)
seen.add(kind)
for kind2 in TIME_UNITS[i:]:
Expand Down
Loading