bsdatetime 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bsdatetime/__init__.py +44 -0
- bsdatetime/bs_lookup.py +10 -0
- bsdatetime/config.py +34 -0
- bsdatetime/conversion.py +114 -0
- bsdatetime/formatting.py +100 -0
- bsdatetime/utils.py +194 -0
- bsdatetime/validation.py +21 -0
- bsdatetime-1.0.0.dist-info/METADATA +79 -0
- bsdatetime-1.0.0.dist-info/RECORD +12 -0
- bsdatetime-1.0.0.dist-info/WHEEL +5 -0
- bsdatetime-1.0.0.dist-info/licenses/LICENSE +21 -0
- bsdatetime-1.0.0.dist-info/top_level.txt +1 -0
bsdatetime/__init__.py
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
"""Bikram Sambat (Nepali) date & datetime utilities.
|
2
|
+
|
3
|
+
Renamed distribution: bsdatetime (formerly bikram-sambat).
|
4
|
+
This module preserves the public API from the former bikram_sambat package.
|
5
|
+
"""
|
6
|
+
|
7
|
+
__version__ = "1.1.0"
|
8
|
+
|
9
|
+
from .config import (
|
10
|
+
BASE_AD,
|
11
|
+
BASE_BS,
|
12
|
+
MIN_BS_YEAR,
|
13
|
+
MAX_BS_YEAR,
|
14
|
+
BS_MONTH_NAMES,
|
15
|
+
BS_WEEKDAY_NAMES,
|
16
|
+
)
|
17
|
+
from . import utils
|
18
|
+
from .conversion import ad_to_bs, bs_to_ad
|
19
|
+
from .validation import is_valid_bs_date, get_bs_month_days
|
20
|
+
from .formatting import (
|
21
|
+
format_bs_date,
|
22
|
+
parse_bs_date,
|
23
|
+
format_bs_datetime,
|
24
|
+
parse_bs_datetime,
|
25
|
+
)
|
26
|
+
|
27
|
+
__all__ = [
|
28
|
+
"__version__",
|
29
|
+
"BASE_AD",
|
30
|
+
"BASE_BS",
|
31
|
+
"MIN_BS_YEAR",
|
32
|
+
"MAX_BS_YEAR",
|
33
|
+
"BS_MONTH_NAMES",
|
34
|
+
"BS_WEEKDAY_NAMES",
|
35
|
+
"utils",
|
36
|
+
"ad_to_bs",
|
37
|
+
"bs_to_ad",
|
38
|
+
"is_valid_bs_date",
|
39
|
+
"get_bs_month_days",
|
40
|
+
"format_bs_date",
|
41
|
+
"parse_bs_date",
|
42
|
+
"format_bs_datetime",
|
43
|
+
"parse_bs_datetime",
|
44
|
+
]
|
bsdatetime/bs_lookup.py
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
import csv, os
|
2
|
+
BS_YEARS = {}
|
3
|
+
csv_path = os.path.join(os.path.dirname(__file__), "data", "calendar_bs.csv")
|
4
|
+
with open(csv_path, newline="", encoding="utf-8") as f:
|
5
|
+
reader = csv.reader(f)
|
6
|
+
next(reader)
|
7
|
+
for row in reader:
|
8
|
+
year = int(row[0])
|
9
|
+
months = list(map(int, row[1:]))
|
10
|
+
BS_YEARS[year] = months
|
bsdatetime/config.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
"""Configuration and constant values for the Bikram Sambat date utilities.
|
2
|
+
"""
|
3
|
+
from __future__ import annotations
|
4
|
+
import datetime as _dt
|
5
|
+
from .bs_lookup import BS_YEARS # local copy of calendar data
|
6
|
+
|
7
|
+
# Core Conversion Anchors
|
8
|
+
BASE_AD: _dt.date = _dt.date(2018, 4, 14) # 2018-04-14 AD
|
9
|
+
BASE_BS: tuple[int, int, int] = (2075, 1, 1) # 2075-01-01 BS
|
10
|
+
|
11
|
+
# Calendar Ranges
|
12
|
+
MIN_BS_YEAR: int = min(BS_YEARS.keys())
|
13
|
+
MAX_BS_YEAR: int = max(BS_YEARS.keys())
|
14
|
+
|
15
|
+
# Localized Names
|
16
|
+
BS_MONTH_NAMES = [
|
17
|
+
"बैशाख", "जेठ", "असार", "साउन", "भदौ", "आश्विन",
|
18
|
+
"कार्तिक", "मंसिर", "पौष", "माघ", "फाल्गुन", "चैत्र"
|
19
|
+
]
|
20
|
+
|
21
|
+
# Weekday mapping: Sunday=0 ... Saturday=6
|
22
|
+
BS_WEEKDAY_NAMES = [
|
23
|
+
"आइतबार", "सोमबार", "मंगलबार", "बुधबार", "बिहिबार", "शुक्रबार", "शनिबार"
|
24
|
+
]
|
25
|
+
|
26
|
+
__all__ = [
|
27
|
+
"BASE_AD",
|
28
|
+
"BASE_BS",
|
29
|
+
"MIN_BS_YEAR",
|
30
|
+
"MAX_BS_YEAR",
|
31
|
+
"BS_MONTH_NAMES",
|
32
|
+
"BS_WEEKDAY_NAMES",
|
33
|
+
"BS_YEARS",
|
34
|
+
]
|
bsdatetime/conversion.py
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
"""Conversion between AD (Gregorian) and BS (Bikram Sambat) calendars."""
|
2
|
+
from __future__ import annotations
|
3
|
+
import datetime as _dt
|
4
|
+
from .config import BASE_AD, BASE_BS, BS_YEARS, MIN_BS_YEAR, MAX_BS_YEAR
|
5
|
+
|
6
|
+
__all__ = ["ad_to_bs", "bs_to_ad"]
|
7
|
+
|
8
|
+
def ad_to_bs(ad_date: _dt.date):
|
9
|
+
"""Convert an AD (Gregorian) date to a BS date tuple (year, month, day).
|
10
|
+
|
11
|
+
Args:
|
12
|
+
ad_date: A datetime.date object in Gregorian calendar
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
tuple: (year, month, day) in Bikram Sambat calendar
|
16
|
+
|
17
|
+
Raises:
|
18
|
+
ValueError: If the date is outside the supported range
|
19
|
+
TypeError: If ad_date is not a datetime.date object
|
20
|
+
"""
|
21
|
+
if not isinstance(ad_date, _dt.date):
|
22
|
+
raise TypeError("ad_date must be a datetime.date object")
|
23
|
+
|
24
|
+
days = (ad_date - BASE_AD).days
|
25
|
+
year, month, day = BASE_BS
|
26
|
+
|
27
|
+
# Handle negative days (dates before BASE_AD)
|
28
|
+
if days < 0:
|
29
|
+
days = abs(days)
|
30
|
+
while days > 0:
|
31
|
+
day -= 1
|
32
|
+
if day < 1:
|
33
|
+
month -= 1
|
34
|
+
if month < 1:
|
35
|
+
month = 12
|
36
|
+
year -= 1
|
37
|
+
if year < MIN_BS_YEAR:
|
38
|
+
raise ValueError(f"Date {ad_date} is before supported BS range")
|
39
|
+
day = BS_YEARS[year][month - 1]
|
40
|
+
days -= 1
|
41
|
+
else:
|
42
|
+
# Handle positive days (dates after BASE_AD)
|
43
|
+
while days > 0:
|
44
|
+
if year > MAX_BS_YEAR:
|
45
|
+
raise ValueError(f"Date {ad_date} is after supported BS range")
|
46
|
+
month_days = BS_YEARS[year][month - 1]
|
47
|
+
if day + days <= month_days:
|
48
|
+
day += days
|
49
|
+
days = 0
|
50
|
+
else:
|
51
|
+
days -= (month_days - day + 1)
|
52
|
+
day = 1
|
53
|
+
month += 1
|
54
|
+
if month > 12:
|
55
|
+
month = 1
|
56
|
+
year += 1
|
57
|
+
|
58
|
+
return (year, month, day)
|
59
|
+
|
60
|
+
def bs_to_ad(bs_year: int, bs_month: int, bs_day: int) -> _dt.date:
|
61
|
+
"""Convert a BS date (y,m,d) to an AD date object.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
bs_year: Bikram Sambat year
|
65
|
+
bs_month: Bikram Sambat month (1-12)
|
66
|
+
bs_day: Bikram Sambat day
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
datetime.date: Corresponding Gregorian date
|
70
|
+
|
71
|
+
Raises:
|
72
|
+
ValueError: If the BS date is invalid or outside supported range
|
73
|
+
TypeError: If inputs are not integers
|
74
|
+
"""
|
75
|
+
# Input validation
|
76
|
+
if not all(isinstance(x, int) for x in [bs_year, bs_month, bs_day]):
|
77
|
+
raise TypeError("BS date components must be integers")
|
78
|
+
|
79
|
+
if bs_year < MIN_BS_YEAR or bs_year > MAX_BS_YEAR:
|
80
|
+
raise ValueError(f"BS year {bs_year} is outside supported range ({MIN_BS_YEAR}-{MAX_BS_YEAR})")
|
81
|
+
|
82
|
+
if bs_month < 1 or bs_month > 12:
|
83
|
+
raise ValueError(f"BS month {bs_month} must be between 1 and 12")
|
84
|
+
|
85
|
+
if bs_year not in BS_YEARS:
|
86
|
+
raise ValueError(f"No calendar data available for BS year {bs_year}")
|
87
|
+
|
88
|
+
if bs_day < 1 or bs_day > BS_YEARS[bs_year][bs_month - 1]:
|
89
|
+
raise ValueError(f"BS day {bs_day} is invalid for month {bs_month} of year {bs_year}")
|
90
|
+
|
91
|
+
ad_date = BASE_AD
|
92
|
+
year, month, day = BASE_BS
|
93
|
+
|
94
|
+
# Prevent infinite loops with a safety counter
|
95
|
+
max_iterations = 50000 # Roughly 137 years worth of days
|
96
|
+
iterations = 0
|
97
|
+
|
98
|
+
while (year, month, day) != (bs_year, bs_month, bs_day):
|
99
|
+
iterations += 1
|
100
|
+
if iterations > max_iterations:
|
101
|
+
raise ValueError("Conversion exceeded maximum iterations - possible infinite loop")
|
102
|
+
|
103
|
+
ad_date += _dt.timedelta(days=1)
|
104
|
+
day += 1
|
105
|
+
if day > BS_YEARS[year][month - 1]:
|
106
|
+
day = 1
|
107
|
+
month += 1
|
108
|
+
if month > 12:
|
109
|
+
month = 1
|
110
|
+
year += 1
|
111
|
+
if year > MAX_BS_YEAR:
|
112
|
+
raise ValueError("BS date is outside supported range")
|
113
|
+
|
114
|
+
return ad_date
|
bsdatetime/formatting.py
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
"""Formatting and parsing for BS dates and datetimes."""
|
2
|
+
from __future__ import annotations
|
3
|
+
import datetime as _dt
|
4
|
+
from .config import BS_MONTH_NAMES, BS_WEEKDAY_NAMES
|
5
|
+
from .conversion import bs_to_ad
|
6
|
+
from .validation import is_valid_bs_date
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"format_bs_date",
|
10
|
+
"parse_bs_date",
|
11
|
+
"format_bs_datetime",
|
12
|
+
"parse_bs_datetime",
|
13
|
+
]
|
14
|
+
|
15
|
+
def _weekday_to_custom(ad_weekday: int) -> int:
|
16
|
+
"""Convert Python weekday to custom Sunday=0 mapping."""
|
17
|
+
return (ad_weekday + 1) % 7
|
18
|
+
|
19
|
+
def format_bs_date(bs_year: int, bs_month: int, bs_day: int, fmt: str = "%Y-%m-%d") -> str:
|
20
|
+
"""Format a BS date according to the given format string."""
|
21
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
22
|
+
weekday = _weekday_to_custom(ad_date.weekday())
|
23
|
+
replacements = {
|
24
|
+
"%Y": f"{bs_year:04d}",
|
25
|
+
"%m": f"{bs_month:02d}",
|
26
|
+
"%d": f"{bs_day:02d}",
|
27
|
+
"%B": BS_MONTH_NAMES[bs_month - 1],
|
28
|
+
"%A": BS_WEEKDAY_NAMES[weekday],
|
29
|
+
}
|
30
|
+
out = fmt
|
31
|
+
for k, v in replacements.items():
|
32
|
+
out = out.replace(k, v)
|
33
|
+
return out
|
34
|
+
|
35
|
+
def parse_bs_date(text: str, fmt: str = "%Y-%m-%d"):
|
36
|
+
"""Parse a BS date string according to the given format."""
|
37
|
+
fmt_parts = fmt.split('-')
|
38
|
+
txt_parts = text.split('-')
|
39
|
+
if len(fmt_parts) != len(txt_parts):
|
40
|
+
raise ValueError("Date string does not match format")
|
41
|
+
y = m = d = None
|
42
|
+
for f, t in zip(fmt_parts, txt_parts):
|
43
|
+
if f == "%Y":
|
44
|
+
y = int(t)
|
45
|
+
elif f == "%m":
|
46
|
+
m = int(t)
|
47
|
+
elif f == "%d":
|
48
|
+
d = int(t)
|
49
|
+
elif f == "%B":
|
50
|
+
if t in BS_MONTH_NAMES:
|
51
|
+
m = BS_MONTH_NAMES.index(t) + 1
|
52
|
+
else:
|
53
|
+
raise ValueError("Invalid month name")
|
54
|
+
else:
|
55
|
+
raise ValueError("Unsupported token")
|
56
|
+
if None in (y, m, d):
|
57
|
+
raise ValueError("Incomplete date")
|
58
|
+
if not is_valid_bs_date(y, m, d):
|
59
|
+
raise ValueError("Invalid BS date")
|
60
|
+
return (y, m, d)
|
61
|
+
|
62
|
+
def format_bs_datetime(bs_year: int, bs_month: int, bs_day: int, hour: int=0, minute: int=0, second: int=0, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
63
|
+
"""Format a BS datetime according to the given format string."""
|
64
|
+
date_fmt, time_fmt = (fmt.split(' ', 1) + [""])[:2] if ' ' in fmt else (fmt, "")
|
65
|
+
date_part = format_bs_date(bs_year, bs_month, bs_day, date_fmt)
|
66
|
+
if not time_fmt:
|
67
|
+
return date_part
|
68
|
+
rep = {
|
69
|
+
"%H": f"{hour:02d}",
|
70
|
+
"%M": f"{minute:02d}",
|
71
|
+
"%S": f"{second:02d}",
|
72
|
+
}
|
73
|
+
time_out = time_fmt
|
74
|
+
for k, v in rep.items():
|
75
|
+
time_out = time_out.replace(k, v)
|
76
|
+
return f"{date_part} {time_out}".strip()
|
77
|
+
|
78
|
+
def parse_bs_datetime(text: str, fmt: str = "%Y-%m-%d %H:%M:%S"):
|
79
|
+
"""Parse a BS datetime string according to the given format."""
|
80
|
+
if ' ' not in fmt:
|
81
|
+
y, m, d = parse_bs_date(text, fmt)
|
82
|
+
return (y, m, d, 0, 0, 0)
|
83
|
+
date_fmt, time_fmt = fmt.split(' ', 1)
|
84
|
+
date_part, time_part = text.split(' ', 1)
|
85
|
+
y, m, d = parse_bs_date(date_part, date_fmt)
|
86
|
+
t_tokens = time_fmt.split(':')
|
87
|
+
v_tokens = time_part.split(':')
|
88
|
+
if len(t_tokens) != len(v_tokens):
|
89
|
+
raise ValueError("Time portion mismatch")
|
90
|
+
hour = minute = second = 0
|
91
|
+
for f, v in zip(t_tokens, v_tokens):
|
92
|
+
if f == "%H":
|
93
|
+
hour = int(v)
|
94
|
+
elif f == "%M":
|
95
|
+
minute = int(v)
|
96
|
+
elif f == "%S":
|
97
|
+
second = int(v)
|
98
|
+
else:
|
99
|
+
raise ValueError("Unsupported time token")
|
100
|
+
return (y, m, d, hour, minute, second)
|
bsdatetime/utils.py
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
"""Utility functions for Bikram Sambat date operations."""
|
2
|
+
|
3
|
+
import datetime
|
4
|
+
from .config import MIN_BS_YEAR, MAX_BS_YEAR, BS_WEEKDAY_NAMES, BS_MONTH_NAMES
|
5
|
+
from . import conversion as _conversion
|
6
|
+
from . import validation as _validation
|
7
|
+
from . import formatting as _formatting
|
8
|
+
|
9
|
+
# Public re-exports
|
10
|
+
ad_to_bs = _conversion.ad_to_bs
|
11
|
+
bs_to_ad = _conversion.bs_to_ad
|
12
|
+
is_valid_bs_date = _validation.is_valid_bs_date
|
13
|
+
get_bs_month_days = _validation.get_bs_month_days
|
14
|
+
format_bs_date = _formatting.format_bs_date
|
15
|
+
parse_bs_date = _formatting.parse_bs_date
|
16
|
+
format_bs_datetime = _formatting.format_bs_datetime
|
17
|
+
parse_bs_datetime = _formatting.parse_bs_datetime
|
18
|
+
|
19
|
+
__all__ = [
|
20
|
+
# conversion
|
21
|
+
"ad_to_bs", "bs_to_ad",
|
22
|
+
# validation
|
23
|
+
"is_valid_bs_date", "get_bs_month_days", "get_bs_year_range",
|
24
|
+
# formatting
|
25
|
+
"format_bs_date", "parse_bs_date", "format_bs_datetime", "parse_bs_datetime",
|
26
|
+
# misc helpers
|
27
|
+
"difference_between_bs_dates", "add_days_to_bs_date", "subtract_days_from_bs_date",
|
28
|
+
"get_current_bs_date", "get_current_bs_datetime", "bs_date_to_ordinal", "ordinal_to_bs_date",
|
29
|
+
"is_leap_year_bs", "get_bs_week_number", "get_bs_quarter", "get_bs_fiscal_year",
|
30
|
+
"get_bs_date_components", "get_bs_date_range", "get_bs_date_from_string",
|
31
|
+
"bs_date_to_timestamp", "timestamp_to_bs_date", "bs_datetime_to_timestamp", "timestamp_to_bs_datetime",
|
32
|
+
"get_bs_month_name", "get_bs_weekday_name",
|
33
|
+
]
|
34
|
+
|
35
|
+
def get_bs_year_range():
|
36
|
+
"""Get the range of supported BS years."""
|
37
|
+
return (MIN_BS_YEAR, MAX_BS_YEAR)
|
38
|
+
|
39
|
+
def get_bs_month_name(bs_month):
|
40
|
+
"""Get the Nepali name of the BS month."""
|
41
|
+
if not isinstance(bs_month, int):
|
42
|
+
raise TypeError("BS month must be an integer")
|
43
|
+
if 1 <= bs_month <= 12:
|
44
|
+
return BS_MONTH_NAMES[bs_month - 1]
|
45
|
+
raise ValueError(f"Invalid BS month: {bs_month}. Must be between 1 and 12")
|
46
|
+
|
47
|
+
def get_bs_weekday_name(weekday):
|
48
|
+
"""Get the Nepali name of the weekday."""
|
49
|
+
if not isinstance(weekday, int):
|
50
|
+
raise TypeError("Weekday must be an integer")
|
51
|
+
if 0 <= weekday <= 6:
|
52
|
+
return BS_WEEKDAY_NAMES[weekday]
|
53
|
+
raise ValueError(f"Invalid weekday: {weekday}. Must be between 0 and 6")
|
54
|
+
|
55
|
+
def add_days_to_bs_date(bs_year, bs_month, bs_day, days_to_add):
|
56
|
+
"""Add days to a BS date."""
|
57
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
58
|
+
new_ad_date = ad_date + datetime.timedelta(days=days_to_add)
|
59
|
+
return ad_to_bs(new_ad_date)
|
60
|
+
|
61
|
+
def subtract_days_from_bs_date(bs_year, bs_month, bs_day, days_to_subtract):
|
62
|
+
"""Subtract days from a BS date."""
|
63
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
64
|
+
new_ad_date = ad_date - datetime.timedelta(days=days_to_subtract)
|
65
|
+
return ad_to_bs(new_ad_date)
|
66
|
+
|
67
|
+
def difference_between_bs_dates(bs_date1, bs_date2):
|
68
|
+
"""Calculate difference in days between two BS dates."""
|
69
|
+
ad_date1 = bs_to_ad(*bs_date1)
|
70
|
+
ad_date2 = bs_to_ad(*bs_date2)
|
71
|
+
return (ad_date2 - ad_date1).days
|
72
|
+
|
73
|
+
def get_current_bs_date():
|
74
|
+
"""Get current BS date."""
|
75
|
+
ad_date = datetime.date.today()
|
76
|
+
return ad_to_bs(ad_date)
|
77
|
+
|
78
|
+
def get_current_bs_datetime():
|
79
|
+
"""Get current BS date and time."""
|
80
|
+
now = datetime.datetime.now()
|
81
|
+
bs_date = ad_to_bs(now.date())
|
82
|
+
return bs_date + (now.hour, now.minute, now.second)
|
83
|
+
|
84
|
+
def bs_date_to_ordinal(bs_year, bs_month, bs_day):
|
85
|
+
"""Convert BS date to ordinal number."""
|
86
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
87
|
+
return ad_date.toordinal()
|
88
|
+
|
89
|
+
def ordinal_to_bs_date(ordinal):
|
90
|
+
"""Convert ordinal number to BS date."""
|
91
|
+
ad_date = datetime.date.fromordinal(ordinal)
|
92
|
+
return ad_to_bs(ad_date)
|
93
|
+
|
94
|
+
def is_leap_year_bs(bs_year):
|
95
|
+
"""Check if BS year is a leap year (simplified)."""
|
96
|
+
if bs_year < MIN_BS_YEAR or bs_year > MAX_BS_YEAR:
|
97
|
+
raise ValueError("BS year out of supported range")
|
98
|
+
ad_date = bs_to_ad(bs_year, 1, 1)
|
99
|
+
ad_year = ad_date.year
|
100
|
+
return (ad_year % 4 == 0 and ad_year % 100 != 0) or (ad_year % 400 == 0)
|
101
|
+
|
102
|
+
def get_bs_week_number(bs_year, bs_month, bs_day):
|
103
|
+
"""Get ISO week number for BS date."""
|
104
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
105
|
+
return ad_date.isocalendar()[1]
|
106
|
+
|
107
|
+
def get_bs_quarter(bs_month):
|
108
|
+
"""Get quarter for BS month."""
|
109
|
+
if 1 <= bs_month <= 3:
|
110
|
+
return 1
|
111
|
+
elif 4 <= bs_month <= 6:
|
112
|
+
return 2
|
113
|
+
elif 7 <= bs_month <= 9:
|
114
|
+
return 3
|
115
|
+
elif 10 <= bs_month <= 12:
|
116
|
+
return 4
|
117
|
+
else:
|
118
|
+
raise ValueError("Invalid BS month")
|
119
|
+
|
120
|
+
def get_bs_fiscal_year(bs_year, bs_month):
|
121
|
+
"""Get fiscal year for BS date (starts in Ashwin)."""
|
122
|
+
if bs_month >= 7:
|
123
|
+
start_year = bs_year
|
124
|
+
end_year = bs_year + 1
|
125
|
+
else:
|
126
|
+
start_year = bs_year - 1
|
127
|
+
end_year = bs_year
|
128
|
+
return f"{start_year}-{end_year}"
|
129
|
+
|
130
|
+
def get_bs_date_components(bs_year, bs_month, bs_day):
|
131
|
+
"""Get BS date components as dictionary."""
|
132
|
+
if not is_valid_bs_date(bs_year, bs_month, bs_day):
|
133
|
+
raise ValueError("Invalid BS date")
|
134
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
135
|
+
weekday = (ad_date.weekday() + 1) % 7
|
136
|
+
return {
|
137
|
+
"year": bs_year,
|
138
|
+
"month": bs_month,
|
139
|
+
"day": bs_day,
|
140
|
+
"month_name": get_bs_month_name(bs_month),
|
141
|
+
"weekday_name": get_bs_weekday_name(weekday)
|
142
|
+
}
|
143
|
+
|
144
|
+
def get_bs_date_range(start_bs_date, end_bs_date):
|
145
|
+
"""Generate list of BS dates between two dates."""
|
146
|
+
start_ad_date = bs_to_ad(*start_bs_date)
|
147
|
+
end_ad_date = bs_to_ad(*end_bs_date)
|
148
|
+
if start_ad_date > end_ad_date:
|
149
|
+
raise ValueError("Start date must be before or equal to end date")
|
150
|
+
delta_days = (end_ad_date - start_ad_date).days
|
151
|
+
bs_dates = []
|
152
|
+
for i in range(delta_days + 1):
|
153
|
+
current_ad_date = start_ad_date + datetime.timedelta(days=i)
|
154
|
+
bs_dates.append(ad_to_bs(current_ad_date))
|
155
|
+
return bs_dates
|
156
|
+
|
157
|
+
def get_bs_date_from_string(date_string):
|
158
|
+
"""Parse BS date from string in various formats."""
|
159
|
+
common_formats = [
|
160
|
+
"%Y-%m-%d",
|
161
|
+
"%Y/%m/%d",
|
162
|
+
"%d-%m-%Y",
|
163
|
+
"%d/%m/%Y",
|
164
|
+
]
|
165
|
+
for fmt in common_formats:
|
166
|
+
try:
|
167
|
+
return parse_bs_date(date_string, fmt)
|
168
|
+
except ValueError:
|
169
|
+
continue
|
170
|
+
raise ValueError("Date string does not match any known format")
|
171
|
+
|
172
|
+
# Timestamp helpers
|
173
|
+
def bs_date_to_timestamp(bs_year, bs_month, bs_day):
|
174
|
+
"""Convert BS date to Unix timestamp."""
|
175
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
176
|
+
return int(datetime.datetime(ad_date.year, ad_date.month, ad_date.day).timestamp())
|
177
|
+
|
178
|
+
def timestamp_to_bs_date(ts):
|
179
|
+
"""Convert Unix timestamp to BS date."""
|
180
|
+
ad_dt = datetime.datetime.fromtimestamp(ts)
|
181
|
+
return ad_to_bs(ad_dt.date())
|
182
|
+
|
183
|
+
def bs_datetime_to_timestamp(bs_year, bs_month, bs_day, hour=0, minute=0, second=0):
|
184
|
+
"""Convert BS datetime to Unix timestamp."""
|
185
|
+
ad_date = bs_to_ad(bs_year, bs_month, bs_day)
|
186
|
+
ad_dt = datetime.datetime(ad_date.year, ad_date.month, ad_date.day, hour, minute, second)
|
187
|
+
return int(ad_dt.timestamp())
|
188
|
+
|
189
|
+
def timestamp_to_bs_datetime(ts):
|
190
|
+
"""Convert Unix timestamp to BS datetime."""
|
191
|
+
ad_dt = datetime.datetime.fromtimestamp(ts)
|
192
|
+
y, m, d = ad_to_bs(ad_dt.date())
|
193
|
+
return (y, m, d, ad_dt.hour, ad_dt.minute, ad_dt.second)
|
194
|
+
|
bsdatetime/validation.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
"""Validation helpers for BS dates."""
|
2
|
+
from __future__ import annotations
|
3
|
+
from .config import BS_YEARS
|
4
|
+
|
5
|
+
__all__ = ["is_valid_bs_date", "get_bs_month_days"]
|
6
|
+
|
7
|
+
def is_valid_bs_date(year: int, month: int, day: int) -> bool:
|
8
|
+
"""Check if a BS date is valid."""
|
9
|
+
if year not in BS_YEARS:
|
10
|
+
return False
|
11
|
+
if not (1 <= month <= 12):
|
12
|
+
return False
|
13
|
+
if not (1 <= day <= BS_YEARS[year][month - 1]):
|
14
|
+
return False
|
15
|
+
return True
|
16
|
+
|
17
|
+
def get_bs_month_days(year: int, month: int) -> int:
|
18
|
+
"""Get the number of days in a BS month."""
|
19
|
+
if year in BS_YEARS and 1 <= month <= 12:
|
20
|
+
return BS_YEARS[year][month - 1]
|
21
|
+
raise ValueError("Invalid BS year or month")
|
@@ -0,0 +1,79 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: bsdatetime
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Bikram Sambat (Nepali) date/datetime conversion and utilities
|
5
|
+
Home-page: https://github.com/Rajendra-Katuwal/bsdatetime
|
6
|
+
Author: Rajendra Katuwal
|
7
|
+
Author-email: Rajendra Katuwal <raj.katuwal2061@gmail.com>
|
8
|
+
License: MIT
|
9
|
+
Project-URL: Homepage, https://github.com/Rajendra-Katuwal/bsdatetime
|
10
|
+
Project-URL: Documentation, https://rajendra-katuwal.github.io/bsdatetime.docs/
|
11
|
+
Project-URL: Issues, https://github.com/Rajendra-Katuwal/bsdatetime/issues
|
12
|
+
Project-URL: Source, https://github.com/Rajendra-Katuwal/bsdatetime
|
13
|
+
Keywords: bikram sambat,nepali,calendar,date,datetime,conversion
|
14
|
+
Classifier: Intended Audience :: Developers
|
15
|
+
Classifier: Topic :: Software Development :: Internationalization
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: License :: OSI Approved :: MIT License
|
23
|
+
Classifier: Operating System :: OS Independent
|
24
|
+
Classifier: Development Status :: 5 - Production/Stable
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
26
|
+
Requires-Python: >=3.9
|
27
|
+
Description-Content-Type: text/markdown
|
28
|
+
License-File: LICENSE
|
29
|
+
Dynamic: author
|
30
|
+
Dynamic: home-page
|
31
|
+
Dynamic: license-file
|
32
|
+
Dynamic: requires-python
|
33
|
+
|
34
|
+
# bsdatetime
|
35
|
+
|
36
|
+
Lightweight, dependency‑free Bikram Sambat (Nepali) calendar utilities for Python.
|
37
|
+
|
38
|
+
Documentation: https://rajendra-katuwal.github.io/bsdatetime.docs/
|
39
|
+
|
40
|
+
## What it does
|
41
|
+
* Convert between Gregorian (AD) and Bikram Sambat (BS)
|
42
|
+
* Format / parse BS dates (localized month + weekday names)
|
43
|
+
* Validate dates, get fiscal year, week number, ranges
|
44
|
+
* Provide current BS date/time helpers
|
45
|
+
|
46
|
+
## Install
|
47
|
+
```bash
|
48
|
+
pip install bsdatetime
|
49
|
+
```
|
50
|
+
|
51
|
+
## Quick start
|
52
|
+
```python
|
53
|
+
import datetime, bsdatetime as bs
|
54
|
+
|
55
|
+
ad = datetime.date(2024, 12, 25)
|
56
|
+
bs_tuple = bs.ad_to_bs(ad) # (2081, 9, 9)
|
57
|
+
ad_back = bs.bs_to_ad(*bs_tuple) # 2024-12-25
|
58
|
+
text = bs.format_bs_date(*bs_tuple, "%B %d, %Y") # भदौ 09, 2081
|
59
|
+
current_bs = bs.utils.get_current_bs_date()
|
60
|
+
```
|
61
|
+
|
62
|
+
Core API (most used)
|
63
|
+
* ad_to_bs(date)
|
64
|
+
* bs_to_ad(y, m, d)
|
65
|
+
* format_bs_date(y, m, d, fmt)
|
66
|
+
* parse_bs_date(text, fmt)
|
67
|
+
* is_valid_bs_date(y, m, d)
|
68
|
+
* utils.get_current_bs_date()
|
69
|
+
|
70
|
+
Supported range: BS 1975–2100 (≈ AD 1918–2043)
|
71
|
+
|
72
|
+
## Django?
|
73
|
+
Use the companion package for model fields:
|
74
|
+
```bash
|
75
|
+
pip install django-bsdatetime
|
76
|
+
```
|
77
|
+
|
78
|
+
## License
|
79
|
+
MIT
|
@@ -0,0 +1,12 @@
|
|
1
|
+
bsdatetime/__init__.py,sha256=sWuzyWMSrr2L9YmIU5SxgMWhsrwq-8mVGBJfeEBaUdQ,960
|
2
|
+
bsdatetime/bs_lookup.py,sha256=mLqhdOIwLc_28xqIwd7yGb3oTTyCgS9YKJTZlHiiJ_w,340
|
3
|
+
bsdatetime/config.py,sha256=Vh3z8JS8eW_2Effe4T5Apjo-WpWG6pbFd2Vu0H8LPwM,1092
|
4
|
+
bsdatetime/conversion.py,sha256=j6nXTqgbFCxa6oReywzkyiXg92aTFJf_Oz5AWvYmaq8,4042
|
5
|
+
bsdatetime/formatting.py,sha256=-4KlTRL6t8ZLaih4e0rMkxcFT1HiM81LYrkpIQFaO_0,3601
|
6
|
+
bsdatetime/utils.py,sha256=BzQNiFoWk8CnrRRFDxShXUaqtSXz41s76d4jFUpHMRE,7300
|
7
|
+
bsdatetime/validation.py,sha256=nm-2wQ3huIPeUFVkVJtn2xENc8bavJJZql0XGzbyElg,709
|
8
|
+
bsdatetime-1.0.0.dist-info/licenses/LICENSE,sha256=ZyF1-rB6W4yPWcNMiDCd_vg6t1yXGtmVx5Ewd2f7jMo,1094
|
9
|
+
bsdatetime-1.0.0.dist-info/METADATA,sha256=lj8_t_BqJYo2q7l3wwucMcIOfOOaNrSMPC7JcwC6AGw,2638
|
10
|
+
bsdatetime-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
bsdatetime-1.0.0.dist-info/top_level.txt,sha256=N0jO3BS_m6QK9Cy815lMRFsWspi-IducsC59bE88YaE,11
|
12
|
+
bsdatetime-1.0.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Rajendra Katuwal
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
bsdatetime
|