bsad-converter 1.0.0__tar.gz
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.
- bsad_converter-1.0.0/.gitignore +6 -0
- bsad_converter-1.0.0/LICENSE +21 -0
- bsad_converter-1.0.0/PKG-INFO +20 -0
- bsad_converter-1.0.0/README.md +8 -0
- bsad_converter-1.0.0/pyproject.toml +22 -0
- bsad_converter-1.0.0/src/bsad_converter/__init__.py +9 -0
- bsad_converter-1.0.0/src/bsad_converter/_core.py +309 -0
- bsad_converter-1.0.0/src/bsad_converter/cli.py +28 -0
- bsad_converter-1.0.0/tests/test_patro.py +27 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Government of Nepal — Office of the Prime Minister and Council of Ministers (OPMCM)
|
|
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,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bsad-converter
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Bikram Sambat (BS) <-> Gregorian (AD) date conversion and the Nepali calendar. Offline, zero-dependency, bilingual.
|
|
5
|
+
Project-URL: Homepage, https://nepal.gov.np/calendar
|
|
6
|
+
Author: Government of Nepal — OPMCM
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: bikram-sambat,calendar,converter,date,nepal,nepali,patro
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# bsad-converter (Python)
|
|
14
|
+
|
|
15
|
+
Bikram Sambat <-> Gregorian conversion. `pip install bsad-converter`.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import bsad_converter as p
|
|
19
|
+
p.ad_to_bs(__import__("datetime").date(2026,6,25))["formatted_ne"] # असार ११, २०८३ बि.सं.
|
|
20
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bsad-converter"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Bikram Sambat (BS) <-> Gregorian (AD) date conversion and the Nepali calendar. Offline, zero-dependency, bilingual."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Government of Nepal — OPMCM" }]
|
|
13
|
+
keywords = ["nepali", "bikram-sambat", "date", "patro", "nepal", "calendar", "converter"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
bsad = "bsad_converter.cli:main"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://nepal.gov.np/calendar"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/bsad_converter"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""bsad-converter — Bikram Sambat (BS) <-> Gregorian (AD) conversion + Nepali calendar.
|
|
2
|
+
Offline, zero-dependency, bilingual. Verified medic/bikram-sambat data; 1970-2090 BS."""
|
|
3
|
+
from ._core import (
|
|
4
|
+
ad_to_bs, bs_to_ad, today_bs, month_calendar, year_calendar,
|
|
5
|
+
DateConversionError, MIN_BS_YEAR, MAX_BS_YEAR, AD_MIN, AD_MAX, BS_MONTHS,
|
|
6
|
+
)
|
|
7
|
+
__all__ = ["ad_to_bs", "bs_to_ad", "today_bs", "month_calendar", "year_calendar",
|
|
8
|
+
"DateConversionError", "MIN_BS_YEAR", "MAX_BS_YEAR", "AD_MIN", "AD_MAX", "BS_MONTHS"]
|
|
9
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Bikram Sambat (BS) ↔ Gregorian (AD) date conversion — sovereign, offline, no deps.
|
|
2
|
+
|
|
3
|
+
Why this exists: NIHIS stores DOB in both AD and BS (`dob`, `dob_bs`) and the portal
|
|
4
|
+
shows BS dates, but conversion was manual / display-only. This is the one shared,
|
|
5
|
+
verified converter for both directions.
|
|
6
|
+
|
|
7
|
+
Calendar data: per-BS-year month lengths, decoded from the **medic/bikram-sambat**
|
|
8
|
+
dataset — the same verified source nepal.gov.np already uses (it agrees with the
|
|
9
|
+
portal table; a third library, sharingapples/nepali-date, disagreed on 2081–2083 and
|
|
10
|
+
was rejected). Epoch: 1 Baishakh 1970 BS = 13 April 1913 AD. Range: 1970–2090 BS.
|
|
11
|
+
|
|
12
|
+
Verified 2026-06-24: every year's month-sum is plausible; Baishakh-1 anchors for
|
|
13
|
+
2000/2080/2081/2082/2083 BS are exact against their known AD dates; a full round-trip
|
|
14
|
+
over all 44,196 days in range is identity; today 24 Jun 2026 AD = 10 Asar 2083 BS.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from datetime import date, datetime, timedelta, timezone
|
|
19
|
+
|
|
20
|
+
EPOCH_AD = date(1913, 4, 13) # 1 Baishakh 1970 BS
|
|
21
|
+
BS_YEAR_ZERO = 1970
|
|
22
|
+
NPT = timezone(timedelta(hours=5, minutes=45)) # Asia/Kathmandu (no DST)
|
|
23
|
+
|
|
24
|
+
MONTHS_EN = ["Baishakh", "Jestha", "Ashadh", "Shrawan", "Bhadra", "Ashwin",
|
|
25
|
+
"Kartik", "Mangsir", "Poush", "Magh", "Falgun", "Chaitra"]
|
|
26
|
+
MONTHS_NE = ["बैशाख", "जेठ", "असार", "साउन", "भदौ", "असोज",
|
|
27
|
+
"कात्तिक", "मंसिर", "पुस", "माघ", "फागुन", "चैत"]
|
|
28
|
+
WEEKDAYS_NE = ["सोमबार", "मंगलबार", "बुधबार", "बिहीबार", "शुक्रबार", "शनिबार", "आइतबार"]
|
|
29
|
+
_NE_DIGITS = str.maketrans("0123456789", "०१२३४५६७८९")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DateConversionError(ValueError):
|
|
33
|
+
"""Input date is malformed or outside the supported BS/AD range."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Month lengths (days) per BS year. Source: medic/bikram-sambat (verified).
|
|
37
|
+
BS_MONTHS: dict[int, list[int]] = {
|
|
38
|
+
1970: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
39
|
+
1971: [31, 31, 32, 31, 32, 30, 30, 29, 30, 29, 30, 30],
|
|
40
|
+
1972: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
41
|
+
1973: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
42
|
+
1974: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
43
|
+
1975: [31, 31, 32, 32, 30, 31, 30, 29, 30, 29, 30, 30],
|
|
44
|
+
1976: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
45
|
+
1977: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
46
|
+
1978: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
47
|
+
1979: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
48
|
+
1980: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
49
|
+
1981: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
|
|
50
|
+
1982: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
51
|
+
1983: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
52
|
+
1984: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
53
|
+
1985: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
|
|
54
|
+
1986: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
55
|
+
1987: [31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
56
|
+
1988: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
57
|
+
1989: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
58
|
+
1990: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
59
|
+
1991: [31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
60
|
+
1992: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
61
|
+
1993: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
62
|
+
1994: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
63
|
+
1995: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
|
|
64
|
+
1996: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
65
|
+
1997: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
66
|
+
1998: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
67
|
+
1999: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
68
|
+
2000: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
69
|
+
2001: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
70
|
+
2002: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
71
|
+
2003: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
72
|
+
2004: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
73
|
+
2005: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
74
|
+
2006: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
75
|
+
2007: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
76
|
+
2008: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 29, 31],
|
|
77
|
+
2009: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
78
|
+
2010: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
79
|
+
2011: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
80
|
+
2012: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
|
|
81
|
+
2013: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
82
|
+
2014: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
83
|
+
2015: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
84
|
+
2016: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
|
|
85
|
+
2017: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
86
|
+
2018: [31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
87
|
+
2019: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
88
|
+
2020: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
89
|
+
2021: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
90
|
+
2022: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
|
|
91
|
+
2023: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
92
|
+
2024: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
93
|
+
2025: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
94
|
+
2026: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
95
|
+
2027: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
96
|
+
2028: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
97
|
+
2029: [31, 31, 32, 31, 32, 30, 30, 29, 30, 29, 30, 30],
|
|
98
|
+
2030: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
99
|
+
2031: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
100
|
+
2032: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
101
|
+
2033: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
102
|
+
2034: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
103
|
+
2035: [30, 32, 31, 32, 31, 31, 29, 30, 30, 29, 29, 31],
|
|
104
|
+
2036: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
105
|
+
2037: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
106
|
+
2038: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
107
|
+
2039: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
|
|
108
|
+
2040: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
109
|
+
2041: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
110
|
+
2042: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
111
|
+
2043: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
|
|
112
|
+
2044: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
113
|
+
2045: [31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
114
|
+
2046: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
115
|
+
2047: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
116
|
+
2048: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
117
|
+
2049: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
|
|
118
|
+
2050: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
119
|
+
2051: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
120
|
+
2052: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
121
|
+
2053: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
|
|
122
|
+
2054: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
123
|
+
2055: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
124
|
+
2056: [31, 31, 32, 31, 32, 30, 30, 29, 30, 29, 30, 30],
|
|
125
|
+
2057: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
126
|
+
2058: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
127
|
+
2059: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
128
|
+
2060: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
129
|
+
2061: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
130
|
+
2062: [30, 32, 31, 32, 31, 31, 29, 30, 29, 30, 29, 31],
|
|
131
|
+
2063: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
132
|
+
2064: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
133
|
+
2065: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
134
|
+
2066: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 29, 31],
|
|
135
|
+
2067: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
136
|
+
2068: [31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
137
|
+
2069: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
138
|
+
2070: [31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
|
|
139
|
+
2071: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
140
|
+
2072: [31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
|
|
141
|
+
2073: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
|
|
142
|
+
2074: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
143
|
+
2075: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
144
|
+
2076: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
|
|
145
|
+
2077: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
146
|
+
2078: [31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
147
|
+
2079: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
148
|
+
2080: [31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
|
|
149
|
+
2081: [31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
|
|
150
|
+
2082: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
151
|
+
2083: [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
|
|
152
|
+
2084: [31, 31, 32, 31, 31, 30, 30, 30, 29, 30, 30, 30],
|
|
153
|
+
2085: [31, 32, 31, 32, 30, 31, 30, 30, 29, 30, 30, 30],
|
|
154
|
+
2086: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
|
|
155
|
+
2087: [31, 31, 32, 31, 31, 31, 30, 30, 29, 30, 30, 30],
|
|
156
|
+
2088: [30, 31, 32, 32, 30, 31, 30, 30, 29, 30, 30, 30],
|
|
157
|
+
2089: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
|
|
158
|
+
2090: [30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
MIN_BS_YEAR = min(BS_MONTHS)
|
|
162
|
+
MAX_BS_YEAR = max(BS_MONTHS)
|
|
163
|
+
|
|
164
|
+
# Precompute days from EPOCH to 1 Baishakh of each year (offsets), and the AD bounds.
|
|
165
|
+
_CUM: dict[int, int] = {}
|
|
166
|
+
_acc = 0
|
|
167
|
+
for _y in range(BS_YEAR_ZERO, MAX_BS_YEAR + 1):
|
|
168
|
+
_CUM[_y] = _acc
|
|
169
|
+
_acc += sum(BS_MONTHS[_y])
|
|
170
|
+
AD_MIN = EPOCH_AD
|
|
171
|
+
AD_MAX = EPOCH_AD + timedelta(days=_acc - 1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _ne_num(n: int | str) -> str:
|
|
175
|
+
return str(n).translate(_NE_DIGITS)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _bs_payload(y: int, m: int, d: int) -> dict:
|
|
179
|
+
return {
|
|
180
|
+
"year": y, "month": m, "day": d,
|
|
181
|
+
"month_en": MONTHS_EN[m - 1], "month_ne": MONTHS_NE[m - 1],
|
|
182
|
+
"iso": f"{y:04d}-{m:02d}-{d:02d}",
|
|
183
|
+
"formatted_en": f"{MONTHS_EN[m - 1]} {d}, {y} BS",
|
|
184
|
+
"formatted_ne": f"{MONTHS_NE[m - 1]} {_ne_num(d)}, {_ne_num(y)} बि.सं.",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def ad_to_bs(d: date) -> dict:
|
|
189
|
+
"""Gregorian date → Bikram Sambat (payload with both EN and NE formatting)."""
|
|
190
|
+
if not (AD_MIN <= d <= AD_MAX):
|
|
191
|
+
raise DateConversionError(f"AD {d.isoformat()} is outside supported range "
|
|
192
|
+
f"{AD_MIN.isoformat()}..{AD_MAX.isoformat()}")
|
|
193
|
+
days = (d - EPOCH_AD).days
|
|
194
|
+
y = BS_YEAR_ZERO
|
|
195
|
+
while y < MAX_BS_YEAR and _CUM[y + 1] <= days:
|
|
196
|
+
y += 1
|
|
197
|
+
days -= _CUM[y]
|
|
198
|
+
for m in range(12):
|
|
199
|
+
dim = BS_MONTHS[y][m]
|
|
200
|
+
if days < dim:
|
|
201
|
+
res = _bs_payload(y, m + 1, days + 1)
|
|
202
|
+
_wd = _weekday_sun0(d)
|
|
203
|
+
res["weekday_en"] = WEEKDAYS_EN_SUN[_wd]
|
|
204
|
+
res["weekday_ne"] = WEEKDAYS_NE_SUN[_wd]
|
|
205
|
+
res["events"] = _events_for(m + 1, days + 1, d)
|
|
206
|
+
return res
|
|
207
|
+
days -= dim
|
|
208
|
+
raise DateConversionError("internal conversion overflow") # unreachable if table is sound
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def bs_to_ad(y: int, m: int, d: int) -> date:
|
|
212
|
+
"""Bikram Sambat (year, month 1-12, day) → Gregorian date."""
|
|
213
|
+
if y not in BS_MONTHS:
|
|
214
|
+
raise DateConversionError(f"BS year {y} is outside supported range "
|
|
215
|
+
f"{MIN_BS_YEAR}..{MAX_BS_YEAR}")
|
|
216
|
+
if not 1 <= m <= 12:
|
|
217
|
+
raise DateConversionError(f"BS month {m} invalid (must be 1..12)")
|
|
218
|
+
dim = BS_MONTHS[y][m - 1]
|
|
219
|
+
if not 1 <= d <= dim:
|
|
220
|
+
raise DateConversionError(f"BS day {d} invalid for {y}-{m:02d} (this month has {dim} days)")
|
|
221
|
+
return EPOCH_AD + timedelta(days=_CUM[y] + sum(BS_MONTHS[y][:m - 1]) + (d - 1))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def today_bs() -> dict:
|
|
225
|
+
"""Today's date in Nepal (Asia/Kathmandu) as BS, with the AD date alongside."""
|
|
226
|
+
now = datetime.now(NPT).date()
|
|
227
|
+
bs = ad_to_bs(now)
|
|
228
|
+
bs["weekday_en"] = now.strftime("%A")
|
|
229
|
+
bs["weekday_ne"] = WEEKDAYS_NE[now.weekday()]
|
|
230
|
+
return {"ad": now.isoformat(), "bs": bs}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── Calendar (month grid / full year) + known fixed national days ─────────────
|
|
234
|
+
# Nepali calendars run Sunday-first. WEEKDAYS_NE above is Monday-first (matches
|
|
235
|
+
# Python's date.weekday()); these are Sunday-indexed for grid layout.
|
|
236
|
+
WEEKDAYS_EN_SUN = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
|
237
|
+
WEEKDAYS_NE_SUN = ["आइतबार", "सोमबार", "मंगलबार", "बुधबार", "बिहीबार", "शुक्रबार", "शनिबार"]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _weekday_sun0(d: date) -> int:
|
|
241
|
+
return (d.weekday() + 1) % 7 # Python Mon=0..Sun=6 -> Sun=0..Sat=6
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# FIXED national days only — these recur on the SAME Bikram Sambat month/day (solar)
|
|
245
|
+
# or the same Gregorian date every year, so they are safe to compute. Movable / lunar
|
|
246
|
+
# festivals (Dashain, Tihar, Holi, Buddha Jayanti, Shivaratri, Eid, Lhosar, …) shift
|
|
247
|
+
# each year and require the official Nepal Panchanga — they are deliberately NOT
|
|
248
|
+
# guessed here. See EVENTS_NOTE.
|
|
249
|
+
EVENTS_NOTE = ("Fixed national days only (recur on the same BS or AD date). Movable "
|
|
250
|
+
"lunar festivals (Dashain, Tihar, Holi, Buddha Jayanti, etc.) are not "
|
|
251
|
+
"included — they require the official Nepal Panchanga.")
|
|
252
|
+
|
|
253
|
+
FIXED_EVENTS_BS: dict[tuple[int, int], list[dict]] = {
|
|
254
|
+
(1, 1): [{"en": "Nepali New Year", "ne": "नयाँ वर्ष"}],
|
|
255
|
+
(2, 15): [{"en": "Republic Day", "ne": "गणतन्त्र दिवस"}],
|
|
256
|
+
(6, 3): [{"en": "Constitution Day", "ne": "संविधान दिवस"}],
|
|
257
|
+
(9, 27): [{"en": "Prithvi Jayanti / National Unity Day", "ne": "पृथ्वी जयन्ती / राष्ट्रिय एकता दिवस"}],
|
|
258
|
+
(10, 1): [{"en": "Maghe Sankranti", "ne": "माघे संक्रान्ति"}],
|
|
259
|
+
(10, 16): [{"en": "Martyrs' Day", "ne": "शहीद दिवस"}],
|
|
260
|
+
(11, 7): [{"en": "Democracy Day", "ne": "प्रजातन्त्र दिवस"}],
|
|
261
|
+
}
|
|
262
|
+
FIXED_EVENTS_AD: dict[tuple[int, int], list[dict]] = {
|
|
263
|
+
(1, 1): [{"en": "English New Year", "ne": "अंग्रेजी नयाँ वर्ष"}],
|
|
264
|
+
(3, 8): [{"en": "International Women's Day", "ne": "अन्तर्राष्ट्रिय नारी दिवस"}],
|
|
265
|
+
(5, 1): [{"en": "International Labour Day", "ne": "अन्तर्राष्ट्रिय श्रमिक दिवस"}],
|
|
266
|
+
(12, 25): [{"en": "Christmas", "ne": "क्रिसमस"}],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _events_for(m: int, d: int, ad: date) -> list[dict]:
|
|
271
|
+
return FIXED_EVENTS_BS.get((m, d), []) + FIXED_EVENTS_AD.get((ad.month, ad.day), [])
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def month_calendar(y: int, m: int) -> dict:
|
|
275
|
+
"""One BS month as a calendar: every day with its AD date, weekday (EN+NE) and
|
|
276
|
+
any fixed national-day events. `first_weekday` lets a UI offset the day-1 cell."""
|
|
277
|
+
if y not in BS_MONTHS:
|
|
278
|
+
raise DateConversionError(f"BS year {y} is outside supported range {MIN_BS_YEAR}..{MAX_BS_YEAR}")
|
|
279
|
+
if not 1 <= m <= 12:
|
|
280
|
+
raise DateConversionError(f"BS month {m} invalid (must be 1..12)")
|
|
281
|
+
n = BS_MONTHS[y][m - 1]
|
|
282
|
+
day_list: list[dict] = []
|
|
283
|
+
for d in range(1, n + 1):
|
|
284
|
+
ad = bs_to_ad(y, m, d)
|
|
285
|
+
w = _weekday_sun0(ad)
|
|
286
|
+
day_list.append({
|
|
287
|
+
"day": d, "day_ne": _ne_num(d), "ad": ad.isoformat(),
|
|
288
|
+
"weekday": w, "weekday_en": WEEKDAYS_EN_SUN[w], "weekday_ne": WEEKDAYS_NE_SUN[w],
|
|
289
|
+
"events": _events_for(m, d, ad),
|
|
290
|
+
})
|
|
291
|
+
first = day_list[0]
|
|
292
|
+
return {
|
|
293
|
+
"month": m, "month_en": MONTHS_EN[m - 1], "month_ne": MONTHS_NE[m - 1], "days": n,
|
|
294
|
+
"first_weekday": {"index": first["weekday"], "en": first["weekday_en"], "ne": first["weekday_ne"]},
|
|
295
|
+
"day_list": day_list,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def year_calendar(y: int) -> dict:
|
|
300
|
+
"""A full BS year — all 12 months, each a `month_calendar`."""
|
|
301
|
+
if y not in BS_MONTHS:
|
|
302
|
+
raise DateConversionError(f"BS year {y} is outside supported range {MIN_BS_YEAR}..{MAX_BS_YEAR}")
|
|
303
|
+
return {
|
|
304
|
+
"bs_year": y, "bs_year_ne": _ne_num(y),
|
|
305
|
+
"total_days": sum(BS_MONTHS[y]),
|
|
306
|
+
"weekday_header_en": WEEKDAYS_EN_SUN, "weekday_header_ne": WEEKDAYS_NE_SUN,
|
|
307
|
+
"events_note": EVENTS_NOTE,
|
|
308
|
+
"months": [month_calendar(y, m) for m in range(1, 13)],
|
|
309
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""CLI: bsad today | ad2bs YYYY-MM-DD | bs2ad YYYY-MM-DD [--en]"""
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import date
|
|
4
|
+
from . import ad_to_bs, bs_to_ad, today_bs, DateConversionError
|
|
5
|
+
|
|
6
|
+
def main(argv=None):
|
|
7
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
8
|
+
en = "--en" in argv
|
|
9
|
+
pos = [a for a in argv if not a.startswith("--")]
|
|
10
|
+
cmd = (pos[0] if pos else "today").lower()
|
|
11
|
+
try:
|
|
12
|
+
if cmd == "today":
|
|
13
|
+
t = today_bs()["bs"]; print(t["formatted_en"] if en else t["formatted_ne"])
|
|
14
|
+
elif cmd == "ad2bs":
|
|
15
|
+
d = date.fromisoformat(pos[1]); b = ad_to_bs(d)
|
|
16
|
+
print(b["formatted_en"] if en else b["formatted_ne"])
|
|
17
|
+
elif cmd == "bs2ad":
|
|
18
|
+
y, m, dd = (int(x) for x in pos[1].split("-")); print(bs_to_ad(y, m, dd).isoformat())
|
|
19
|
+
elif cmd in ("help", "--help"):
|
|
20
|
+
print("bsad today | ad2bs YYYY-MM-DD | bs2ad YYYY-MM-DD [--en]")
|
|
21
|
+
else:
|
|
22
|
+
print(f"unknown command '{cmd}'", file=sys.stderr); return 1
|
|
23
|
+
except (IndexError, ValueError, DateConversionError) as e:
|
|
24
|
+
print(f"error: {e}", file=sys.stderr); return 1
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
import bsad_converter as p
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
def test_anchors():
|
|
6
|
+
assert p.bs_to_ad(2000, 1, 1).isoformat() == "1943-04-14"
|
|
7
|
+
assert p.bs_to_ad(2080, 1, 1).isoformat() == "2023-04-14"
|
|
8
|
+
assert p.bs_to_ad(2083, 1, 1).isoformat() == "2026-04-14"
|
|
9
|
+
assert p.ad_to_bs(date(2026, 4, 14))["iso"] == "2083-01-01"
|
|
10
|
+
|
|
11
|
+
def test_format_events():
|
|
12
|
+
bs = p.ad_to_bs(date(2026, 6, 25))
|
|
13
|
+
assert bs["formatted_en"] == "Ashadh 11, 2083 BS"
|
|
14
|
+
assert bs["formatted_ne"] == "असार ११, २०८३ बि.सं."
|
|
15
|
+
assert p.ad_to_bs(date(2026, 4, 14))["events"] == [{"en": "Nepali New Year", "ne": "नयाँ वर्ष"}]
|
|
16
|
+
|
|
17
|
+
def test_round_trip():
|
|
18
|
+
d, end, n = date(1913, 4, 13), date(2034, 4, 13), 0
|
|
19
|
+
while d <= end:
|
|
20
|
+
bs = p.ad_to_bs(d)
|
|
21
|
+
assert p.bs_to_ad(bs["year"], bs["month"], bs["day"]) == d
|
|
22
|
+
d += timedelta(days=1); n += 1
|
|
23
|
+
assert n > 40000
|
|
24
|
+
|
|
25
|
+
def test_validation():
|
|
26
|
+
with pytest.raises(p.DateConversionError): p.ad_to_bs(date(2099, 1, 1))
|
|
27
|
+
with pytest.raises(p.DateConversionError): p.bs_to_ad(2083, 13, 1)
|