"""Provides a calendar system for Bikram Sambat (BS) dates.
This module offers functions to generate calendar data structures, providing
day-by-day mapping between BS and AD dates. It includes methods to fetch
data for specific months or entire years, and a `monthcalendar` function
similar to the standard Python `calendar` module.
"""
from dataclasses import dataclass, asdict
from typing import List, Optional, Union
import functools
import calendar as pycalendar
import datetime
from .bs_date import BSDate
from .conversion import ad_to_bs, bs_to_ad
from .data.calendar_data import YEAR_MONTH_DAYS_BS
from . import config
[docs]
@dataclass
class CalendarDayData:
"""Represents a single day in the calendar with both BS and AD dates."""
bs_year: int
bs_month: int
bs_day: int
ad_year: int
ad_month: int
ad_day: int
ad_full_date: str
week_day: int # 0=Sunday, 6=Saturday
[docs]
def to_dict(self) -> dict:
"""Returns the day data as a dictionary."""
return asdict(self)
[docs]
@dataclass
class CalendarMonthData:
"""Represents a month in the calendar."""
bs_year: int
bs_month: int
ad_year: int
ad_month: int
days: List[CalendarDayData]
[docs]
def to_dict(self) -> dict:
"""Returns the month data as a dictionary.
Nested `CalendarDayData` objects are also converted to dictionaries.
"""
return asdict(self)
[docs]
@functools.lru_cache(maxsize=128)
def bs_calendar(year: int, month: Optional[int] = None) -> Union[CalendarMonthData, List[CalendarMonthData]]:
"""Generates calendar data for a given BS year and optional month.
If `month` is provided, returns data for that specific BS month.
If `month` is None, returns a list of data for all 12 BS months.
"""
if month is None:
return [bs_calendar(year, m) for m in range(1, 13)] # type: ignore
if not (config.BS_MIN_SUPPORTED_YEAR <= year <= config.BS_MAX_SUPPORTED_YEAR):
raise ValueError(f"Year {year} is out of the supported range.")
if not (1 <= month <= 12):
raise ValueError(f"Month {month} must be between 1 and 12.")
days_in_month = YEAR_MONTH_DAYS_BS[year][month - 1]
days_data = []
first_day_ad = bs_to_ad(year, month, 1)
for day in range(1, days_in_month + 1):
ad_date = bs_to_ad(year, month, day)
# Using BSDate to calculate the weekday (0=Sun, 6=Sat)
bs_date_obj = BSDate(year, month, day)
days_data.append(CalendarDayData(
bs_year=year,
bs_month=month,
bs_day=day,
ad_year=ad_date.year,
ad_month=ad_date.month,
ad_day=ad_date.day,
ad_full_date=ad_date.isoformat(),
week_day=bs_date_obj.weekday()
))
return CalendarMonthData(
bs_year=year,
bs_month=month,
ad_year=first_day_ad.year,
ad_month=first_day_ad.month,
days=days_data
)
[docs]
@functools.lru_cache(maxsize=128)
def ad_calendar(year: int, month: Optional[int] = None) -> Union[CalendarMonthData, List[CalendarMonthData]]:
"""Generates calendar data for a given AD year and optional month.
If `month` is provided, returns data for that specific AD month.
If `month` is None, returns a list of data for all 12 AD months.
"""
if month is None:
return [ad_calendar(year, m) for m in range(1, 13)] # type: ignore
if not (1 <= month <= 12):
raise ValueError(f"Month {month} must be between 1 and 12.")
_, days_in_month = pycalendar.monthrange(year, month)
days_data = []
# Calculate the BS date for the first day of the AD month
first_day_ad = datetime.date(year, month, 1)
first_bs_year, first_bs_month, _ = ad_to_bs(first_day_ad)
for day in range(1, days_in_month + 1):
ad_date = datetime.date(year, month, day)
bs_y, bs_m, bs_d = ad_to_bs(ad_date)
bs_date_obj = BSDate(bs_y, bs_m, bs_d)
days_data.append(CalendarDayData(
bs_year=bs_y,
bs_month=bs_m,
bs_day=bs_d,
ad_year=year,
ad_month=month,
ad_day=day,
ad_full_date=ad_date.isoformat(),
week_day=bs_date_obj.weekday()
))
return CalendarMonthData(
bs_year=first_bs_year,
bs_month=first_bs_month,
ad_year=year,
ad_month=month,
days=days_data
)
[docs]
@functools.lru_cache(maxsize=128)
def monthcalendar(year: int, month: int) -> List[List[int]]:
"""Returns a matrix representing a BS month's calendar.
Each row represents a week; days outside the month are represented by zeros.
The week starts on Sunday.
"""
if not (config.BS_MIN_SUPPORTED_YEAR <= year <= config.BS_MAX_SUPPORTED_YEAR):
raise ValueError(f"Year {year} is out of the supported range.")
if not (1 <= month <= 12):
raise ValueError(f"Month {month} must be between 1 and 12.")
days_in_month = YEAR_MONTH_DAYS_BS[year][month - 1]
first_day = BSDate(year, month, 1)
start_weekday = first_day.weekday() # 0 = Sunday
matrix = []
current_week = [0] * start_weekday
for day in range(1, days_in_month + 1):
current_week.append(day)
if len(current_week) == 7:
matrix.append(current_week)
current_week = []
if current_week:
# Pad the last week with zeros
current_week.extend([0] * (7 - len(current_week)))
matrix.append(current_week)
return matrix