Skip to content
32 changes: 30 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,38 @@ jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
options: "--check"
pyright:
name: pyright
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
cache: 'pip'
- name: Run pyright
run: |
pip install "pyright>=1.1,<2" "setuptools>=38.6.0,<69.0"
pyright
isort:
name: isort
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
cache: 'pip'
- name: Run isort
run: |
pip install "isort>=6.0,<6.1"
isort -c unit_of_time
test:
name: run tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -46,7 +74,7 @@ jobs:
publish:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
needs: [black, build, test]
needs: [black, build, test, pyright]
steps:
- name: checkout code
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion timetest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
from datetime import date, datetime, time, timedelta

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


class Decade(TimeunitKind):
Expand Down
92 changes: 48 additions & 44 deletions unit_of_time/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import math
from datetime import date, datetime, timedelta
from typing import Dict, Union


def date_from_int(val, div=1):
def date_from_int(val: int, div: int=1) -> date:
val //= div
d = val % 100
val //= 100
Expand All @@ -11,26 +11,26 @@ def date_from_int(val, div=1):
return date(val, m, d)


def date_to_int(val, mul=1):
def date_to_int(val: date, mul: int = 1) -> int:
return mul * (val.year * 10000 + val.month * 100 + val.day)


class TimeunitKindMeta(type):
kind_int = None
formatter = None
_pre_registered = []
_registered = None
_multiplier = None
kind_int: int = -1
formatter: str = ""
_pre_registered: list["TimeunitKindMeta"] = []
_registered: None | Dict[int, "TimeunitKindMeta"] = None
_multiplier: int = -1

def __init__(cls, name, bases, attrs):
def __init__(cls, name, bases, attrs) -> None:
super().__init__(name, bases, attrs)
if cls.kind_int is not None:
TimeunitKindMeta._pre_registered.append(cls)
TimeunitKindMeta._registered = None
TimeunitKindMeta._multiplier = None
TimeunitKindMeta._multiplier = -1

@property
def unit_register(self):
def unit_register(self) -> Dict[int, "TimeunitKindMeta"]:
result = TimeunitKindMeta._registered
if result is None:
result = {
Expand All @@ -42,27 +42,29 @@ def unit_register(self):
return result

@property
def multiplier(cls):
def multiplier(cls) -> int:
result = TimeunitKindMeta._multiplier
if result is None:
result = max(1, *[k.kind_int for k in TimeunitKindMeta._pre_registered])
result = 10 ** math.ceil(math.log10(result))
if result == -1:
result = 1
for k in TimeunitKindMeta._pre_registered:
while k.kind_int >= result:
result *= 10
TimeunitKindMeta._multiplier = result
return result

def __int__(self):
def __int__(self) -> int:
return self.kind_int

def __index__(self):
def __index__(self) -> int:
return int(self)

def __hash__(self):
def __hash__(self) -> int:
"""
Return the hash value of the time unit, based on its integer encoding.
"""
return hash(int(self))

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
"""
Return True if this time unit kind is the same as another kind or matches the kind registered for the given integer.

Expand All @@ -73,10 +75,11 @@ def __eq__(self, other):
bool: True if both refer to the same time unit kind, otherwise False.
"""
if isinstance(other, int):
other = TimeunitKind.unit_register[other]
# if out of range, None will fail
other = TimeunitKind.unit_register.get(other)
return self is other

def __call__(cls, dt):
def __call__(cls, dt: Union["Timeunit", date]) -> "Timeunit":
"""
Creates a `Timeunit` instance of this kind from a given date or `Timeunit`.

Expand All @@ -89,17 +92,17 @@ def __call__(cls, dt):
def __lt__(self, other):
return self.kind_int < other.kind_int

def from_int(cls, val):
def from_int(cls, val: int) -> "Timeunit":
mul = cls.multiplier
return TimeunitKind.unit_register[val % mul](date_from_int(val, mul))

def get_previous(cls, dt):
def get_previous(cls, dt: Union[date, "Timeunit"]) -> "Timeunit":
if isinstance(dt, Timeunit):
dt = dt.dt
dt -= timedelta(days=1)
return cls(dt)

def last_day(cls, dt):
def last_day(cls, dt: date) -> date:
"""
Return the last date of the time unit containing the given date.

Expand All @@ -111,7 +114,7 @@ def last_day(cls, dt):
"""
return cls._next(dt) - timedelta(days=1)

def _next(cls, dt):
def _next(cls, dt: date) -> date:
"""
Return the first day of the next time unit following the given date.

Expand All @@ -123,7 +126,7 @@ def _next(cls, dt):
"""
return cls.last_day(dt) + timedelta(days=1)

def get_next(cls, dt):
def get_next(cls, dt: Union["Timeunit", date]) -> "Timeunit":
"""
Return the next time unit instance of this kind after the given date.

Expand All @@ -133,16 +136,16 @@ def get_next(cls, dt):
dt = dt.dt
return cls(cls._next(cls.truncate(dt)))

def to_str(cls, dt):
def to_str(cls, dt) -> str:
return dt.strftime(cls.formatter)

def truncate(cls, dt):
def truncate(cls, dt: date) -> date:
return datetime.strptime(cls.to_str(dt), cls.formatter).date()

def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur, dt, amount) -> date | None:
return None

def _shift(cls, cur, dt, amount):
def _shift(cls, cur: "Timeunit", dt: date, amount: int) -> "Timeunit":
new_dt = cls._inner_shift(cur, dt, amount)
if new_dt is not None:
return cls(new_dt)
Expand All @@ -159,20 +162,20 @@ def _shift(cls, cur, dt, amount):


class TimeunitKind(metaclass=TimeunitKindMeta):
kind_int = None
formatter = None
kind_int = -1
formatter = ""


class Year(TimeunitKind):
kind_int = 1
formatter = "%Y"

@classmethod
def _next(cls, dt):
def _next(cls, dt: date) -> date:
return date(dt.year + 1, 1, 1)

@classmethod
def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int):
return date(dt.year + amount, 1, 1)


Expand All @@ -184,15 +187,15 @@ def to_str(cls, dt):
return f"{dt.year}Q{dt.month//3}"

@classmethod
def truncate(cls, dt):
def truncate(cls, dt: date) -> date:
return date(dt.year, 3 * ((dt.month - 1) // 3) + 1, 1)

@classmethod
def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date:
q_new = dt.year * 4 + amount + (dt.month - 1) // 3
y = q_new // 4
q = q_new % 4
return date(q_new // 4, 3 * q + 1, 1)
return date(y, 3 * q + 1, 1)

@classmethod
def _next(cls, dt):
Expand All @@ -207,7 +210,7 @@ class Month(TimeunitKind):
formatter = "%YM%m"

@classmethod
def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date:
m_new = dt.year * 12 + amount + dt.month - 1
return date(m_new // 12, m_new % 12 + 1, 1)

Expand All @@ -225,11 +228,11 @@ class Week(TimeunitKind):
formatter = "%YW%W"

@classmethod
def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date:
return dt + timedelta(days=7 * amount)

@classmethod
def truncate(cls, dt):
def truncate(cls, dt: date) -> date:
if isinstance(dt, datetime):
dt = dt.date()
return dt - timedelta(days=dt.weekday())
Expand All @@ -244,11 +247,11 @@ class Day(TimeunitKind):
formatter = "%Y-%m-%d"

@classmethod
def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date:
return dt + timedelta(days=amount)

@classmethod
def _next(self, dt):
def _next(cls, dt: date) -> date:
return dt + timedelta(days=1)


Expand Down Expand Up @@ -370,7 +373,7 @@ def __repr__(self):
return f"{self.__class__.__name__}({self.kind.__qualname__}, {self.dt!r})"

@classmethod
def _get_range(cls, item):
def _get_range(cls, item) -> tuple[date, date]:
"""
Extracts a date range tuple from the given item.

Expand All @@ -396,7 +399,8 @@ def _get_range(cls, item):
try:
dt0, dt1 = item
if isinstance(dt0, date) and isinstance(dt1, date):
return item
return dt0, dt1
raise TypeError(f"Item {item!r} is not a date range.") from None
except TypeError:
raise TypeError(f"Item {item!r} has no date range.") from None

Expand Down
Loading