Source code for calendar_smith.core

# src/calendar_smith/core.py
from datetime import date, timedelta
from typing import NamedTuple, List


[docs] class WeekSpan(NamedTuple): """ISO week span with week number, start date, and end date.""" number: int start: date end: date
[docs] class DateRange(NamedTuple): """Simple date range with start and end dates.""" start: date end: date
[docs] def get_fiscal_year(date_obj: date, system: str = "us") -> int: """Return the fiscal year for a date. Supported systems: - ``us``: fiscal year starts Oct 1 - ``jp``: fiscal year starts Apr 1 """ if system == "us": return date_obj.year + 1 if date_obj.month >= 10 else date_obj.year elif system == "jp": return date_obj.year if date_obj.month >= 4 else date_obj.year - 1 raise ValueError(f"Unsupported fiscal system: {system}")
[docs] def get_iso_week_span(iso_year: int, iso_week: int) -> WeekSpan: """Return the Monday-to-Sunday span for an ISO week.""" iso_week_monday = 1 iso_week_sunday = 7 week_start = date.fromisocalendar(iso_year, iso_week, iso_week_monday) week_end = date.fromisocalendar(iso_year, iso_week, iso_week_sunday) return WeekSpan(number=iso_week, start=week_start, end=week_end)
[docs] def get_iso_weeks_for_year(year: int) -> List[WeekSpan]: """Return all ISO week spans for a given year.""" max_week = date(year, 12, 28).isocalendar().week return [get_iso_week_span(year, w) for w in range(1, max_week + 1)]
[docs] def get_nth_week_of_month(date_obj: date) -> int: """Return the 1-based week number within the month.""" first_day = date_obj.replace(day=1) dom = date_obj.day adjusted_dom = dom + first_day.weekday() return (adjusted_dom - 1) // 7 + 1
[docs] def get_dates_windows( start_date: date, window_size: int, repeats: int, sampling_rate: int | None = None, ) -> list[DateRange]: """ Return date windows based on a sampling rate. Args: start_date: The starting date of the first window. window_size: The duration of each window in days. repeats: Number of windows to generate. sampling_rate: The number of days between the start of each window. Defaults to window_size, which produces consecutive non-overlapping windows. """ if window_size < 1: raise ValueError("window_size must be at least 1 day.") if repeats < 0: raise ValueError("repeats cannot be negative.") sampling_rate = window_size if sampling_rate is None else sampling_rate if sampling_rate < 1: raise ValueError("sampling_rate must be at least 1 day to progress forward.") window_end_offset = timedelta(days=window_size - 1) return [ DateRange( start_date + timedelta(days=index * sampling_rate), start_date + timedelta(days=index * sampling_rate) + window_end_offset, ) for index in range(repeats) ]