use std::fmt;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
const BENGALI_YEAR_OFFSET: i32 = 593;
const SECONDS_PER_DAY: i64 = 86_400;
const UNIX_EPOCH_ABSOLUTE: i64 = 719_163;
const DHAKA_UTC_OFFSET_SECONDS: i64 = 6 * 60 * 60;
const KOLKATA_UTC_OFFSET_SECONDS: i64 = 5 * 60 * 60 + 30 * 60;
const JULIAN_DAY_OFFSET: f64 = 1_721_424.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CalendarStandard {
Bangladesh,
BangladeshLegacy,
WestBengalTraditional,
}
impl CalendarStandard {
fn default_utc_offset_seconds(self) -> i64 {
match self {
Self::Bangladesh | Self::BangladeshLegacy => DHAKA_UTC_OFFSET_SECONDS,
Self::WestBengalTraditional => KOLKATA_UTC_OFFSET_SECONDS,
}
}
}
impl fmt::Display for CalendarStandard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Bangladesh => "Bangladesh",
Self::BangladeshLegacy => "BangladeshLegacy",
Self::WestBengalTraditional => "WestBengalTraditional",
};
f.write_str(label)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseCalendarStandardError;
impl fmt::Display for ParseCalendarStandardError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unsupported calendar standard")
}
}
impl std::error::Error for ParseCalendarStandardError {}
impl FromStr for CalendarStandard {
type Err = ParseCalendarStandardError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let normalized = input.trim().to_ascii_uppercase();
match normalized.as_str() {
"BD" | "BANGLADESH" | "BANGLADESH_MODERN" | "BD_MODERN" => Ok(Self::Bangladesh),
"BD_LEGACY" | "BANGLADESH_LEGACY" | "BANGLADESH_1987" => Ok(Self::BangladeshLegacy),
"WB"
| "IN"
| "INDIA"
| "INDIAN_BENGALI"
| "INDIA_BENGALI"
| "WEST_BENGAL"
| "WESTBENGAL"
| "WEST_BENGAL_TRADITIONAL" => Ok(Self::WestBengalTraditional),
_ => Err(ParseCalendarStandardError),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BongabdoError {
InvalidGregorianDate { year: i32, month: u32, day: u32 },
SystemTimeBeforeUnixEpoch,
}
impl fmt::Display for BongabdoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidGregorianDate { year, month, day } => {
write!(f, "invalid Gregorian date: {year:04}-{month:02}-{day:02}")
}
Self::SystemTimeBeforeUnixEpoch => f.write_str("system time is before the Unix epoch"),
}
}
}
impl std::error::Error for BongabdoError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bongabdo {
pub year: i32,
pub month: u32,
pub day: u32,
pub weekday: u32,
pub standard: CalendarStandard,
}
impl Bongabdo {
pub fn today(standard: CalendarStandard) -> Result<Self, BongabdoError> {
Self::from_system_time(SystemTime::now(), standard)
}
pub fn from_ymd(
year: i32,
month: u32,
day: u32,
standard: CalendarStandard,
) -> Result<Self, BongabdoError> {
validate_gregorian_date(year, month, day)?;
let weekday = weekday_from_gregorian(year, month, day);
let (bengali_year, bengali_month, bengali_day) = match standard {
CalendarStandard::Bangladesh => {
gregorian_to_fixed_bangladesh(year, month, day, BangladeshVariant::Modern)
}
CalendarStandard::BangladeshLegacy => {
gregorian_to_fixed_bangladesh(year, month, day, BangladeshVariant::Legacy)
}
CalendarStandard::WestBengalTraditional => gregorian_to_west_bengal(year, month, day)?,
};
Ok(Bongabdo {
year: bengali_year,
month: bengali_month,
day: bengali_day,
weekday,
standard,
})
}
pub fn from_system_time(
time: SystemTime,
standard: CalendarStandard,
) -> Result<Self, BongabdoError> {
let unix_seconds = time
.duration_since(UNIX_EPOCH)
.map_err(|_| BongabdoError::SystemTimeBeforeUnixEpoch)?
.as_secs() as i64;
let local_seconds = unix_seconds + standard.default_utc_offset_seconds();
let (year, month, day) = gregorian_from_unix_seconds(local_seconds);
Self::from_ymd(year, month, day, standard)
}
#[deprecated(
note = "use Bongabdo::today(CalendarStandard::...) to make the calendar standard explicit"
)]
pub fn now() -> Self {
Self::today(CalendarStandard::BangladeshLegacy)
.expect("current system time should convert to a Bengali date")
}
pub fn month_name(&self) -> &'static str {
get_bengali_month_name(self.month)
}
pub fn month_name_roman(&self) -> &'static str {
get_roman_month_name(self.month)
}
pub fn weekday_name(&self) -> &'static str {
get_bengali_weekday_name(self.weekday)
}
pub fn weekday_name_roman(&self) -> &'static str {
get_roman_weekday_name(self.weekday)
}
pub fn to_bengali_string(&self) -> String {
format!(
"{} {} {}, {}",
self.weekday_name(),
to_bengali_digits(self.day),
self.month_name(),
to_bengali_digits(self.year)
)
}
pub fn to_roman_string(&self) -> String {
format!(
"{} {} {}, {}",
self.weekday_name_roman(),
self.day,
self.month_name_roman(),
self.year
)
}
}
impl fmt::Display for Bongabdo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_roman_string())
}
}
#[derive(Debug, Clone, Copy)]
enum BangladeshVariant {
Modern,
Legacy,
}
fn gregorian_to_fixed_bangladesh(
year: i32,
month: u32,
day: u32,
variant: BangladeshVariant,
) -> (i32, u32, u32) {
let absolute = absolute_from_gregorian(year, month, day);
let start_year = if (month, day) >= (4, 14) {
year
} else {
year - 1
};
let start_absolute = absolute_from_gregorian(start_year, 4, 14);
let day_index = (absolute - start_absolute) as u32;
let bengali_year = start_year - BENGALI_YEAR_OFFSET;
let leap_falgun = is_gregorian_leap_year(start_year + 1);
let month_lengths = bangladesh_month_lengths(variant, leap_falgun);
let (bengali_month, bengali_day) = month_day_from_day_index(day_index, &month_lengths);
(bengali_year, bengali_month, bengali_day)
}
fn gregorian_to_west_bengal(
year: i32,
month: u32,
day: u32,
) -> Result<(i32, u32, u32), BongabdoError> {
let absolute = absolute_from_gregorian(year, month, day);
let current_year_boishakh = west_bengal_month_start_absolute(year, 1)?;
let start_year = if absolute >= current_year_boishakh {
year
} else {
year - 1
};
let bengali_year = start_year - BENGALI_YEAR_OFFSET;
let mut month_starts = Vec::with_capacity(13);
for month_index in 1..=12 {
month_starts.push(west_bengal_month_start_absolute(start_year, month_index)?);
}
month_starts.push(west_bengal_month_start_absolute(start_year + 1, 1)?);
for (index, window) in month_starts.windows(2).enumerate() {
let month_start = window[0];
let next_month_start = window[1];
if absolute >= month_start && absolute < next_month_start {
let bengali_month = index as u32 + 1;
let bengali_day = (absolute - month_start) as u32 + 1;
return Ok((bengali_year, bengali_month, bengali_day));
}
}
unreachable!("Gregorian date must fall within exactly one Bengali month window")
}
fn bangladesh_month_lengths(variant: BangladeshVariant, leap_falgun: bool) -> [u32; 12] {
match variant {
BangladeshVariant::Modern => [
31,
31,
31,
31,
31,
31,
30,
30,
30,
30,
if leap_falgun { 30 } else { 29 },
30,
],
BangladeshVariant::Legacy => [
31,
31,
31,
31,
31,
30,
30,
30,
30,
30,
if leap_falgun { 31 } else { 30 },
30,
],
}
}
fn month_day_from_day_index(day_index: u32, month_lengths: &[u32; 12]) -> (u32, u32) {
let mut remaining = day_index;
for (index, length) in month_lengths.iter().enumerate() {
if remaining < *length {
return (index as u32 + 1, remaining + 1);
}
remaining -= *length;
}
unreachable!("day index must fall inside the Bengali year")
}
fn west_bengal_month_start_absolute(
bengali_year_start_gregorian_year: i32,
bengali_month: u32,
) -> Result<i64, BongabdoError> {
let (target_longitude, ingress_year, approx_month, approx_day) =
west_bengal_ingress_target(bengali_year_start_gregorian_year, bengali_month);
let ingress_julian_day =
find_sidereal_ingress(ingress_year, approx_month, approx_day, target_longitude);
let local_julian_day =
ingress_julian_day + (KOLKATA_UTC_OFFSET_SECONDS as f64 / SECONDS_PER_DAY as f64);
let (ingress_year_local, ingress_month_local, ingress_day_local) =
gregorian_from_julian_day(local_julian_day);
validate_gregorian_date(ingress_year_local, ingress_month_local, ingress_day_local)?;
Ok(absolute_from_gregorian(ingress_year_local, ingress_month_local, ingress_day_local) + 1)
}
fn west_bengal_ingress_target(
bengali_year_start_gregorian_year: i32,
bengali_month: u32,
) -> (f64, i32, u32, u32) {
match bengali_month {
1 => (0.0, bengali_year_start_gregorian_year, 4, 14),
2 => (30.0, bengali_year_start_gregorian_year, 5, 15),
3 => (60.0, bengali_year_start_gregorian_year, 6, 15),
4 => (90.0, bengali_year_start_gregorian_year, 7, 16),
5 => (120.0, bengali_year_start_gregorian_year, 8, 17),
6 => (150.0, bengali_year_start_gregorian_year, 9, 17),
7 => (180.0, bengali_year_start_gregorian_year, 10, 18),
8 => (210.0, bengali_year_start_gregorian_year, 11, 17),
9 => (240.0, bengali_year_start_gregorian_year, 12, 16),
10 => (270.0, bengali_year_start_gregorian_year + 1, 1, 14),
11 => (300.0, bengali_year_start_gregorian_year + 1, 2, 13),
12 => (330.0, bengali_year_start_gregorian_year + 1, 3, 15),
_ => unreachable!("invalid Bengali month"),
}
}
fn find_sidereal_ingress(year: i32, month: u32, day: u32, target_longitude: f64) -> f64 {
let start = julian_day_from_gregorian(year, month, day) - 4.0;
let end = julian_day_from_gregorian(year, month, day) + 4.0;
let mut previous_julian_day = start;
let mut previous_delta =
signed_longitude_delta(sidereal_solar_longitude(start), target_longitude);
let mut current_julian_day = start + (1.0 / 24.0);
while current_julian_day <= end {
let current_delta = signed_longitude_delta(
sidereal_solar_longitude(current_julian_day),
target_longitude,
);
if previous_delta <= 0.0 && current_delta > 0.0 {
let mut low = previous_julian_day;
let mut high = current_julian_day;
for _ in 0..50 {
let mid = (low + high) / 2.0;
let mid_delta =
signed_longitude_delta(sidereal_solar_longitude(mid), target_longitude);
if mid_delta <= 0.0 {
low = mid;
} else {
high = mid;
}
}
return (low + high) / 2.0;
}
previous_julian_day = current_julian_day;
previous_delta = current_delta;
current_julian_day += 1.0 / 24.0;
}
unreachable!("sidereal ingress should be found within the search window")
}
fn signed_longitude_delta(longitude: f64, target: f64) -> f64 {
let mut delta = normalize_degrees(longitude - target);
if delta >= 180.0 {
delta -= 360.0;
}
delta
}
fn sidereal_solar_longitude(julian_day: f64) -> f64 {
normalize_degrees(apparent_solar_longitude(julian_day) - lahiri_ayanamsa(julian_day))
}
fn apparent_solar_longitude(julian_day: f64) -> f64 {
let t = (julian_day - 2_451_545.0) / 36_525.0;
let mean_longitude = normalize_degrees(280.46646 + 36_000.769_83 * t + 0.0003032 * t * t);
let mean_anomaly = normalize_degrees(
357.52911 + 35_999.050_29 * t - 0.0001537 * t * t + t * t * t / 24_490_000.0,
);
let equation_of_center = (1.914602 - 0.004817 * t - 0.000014 * t * t)
* mean_anomaly.to_radians().sin()
+ (0.019993 - 0.000101 * t) * (2.0 * mean_anomaly).to_radians().sin()
+ 0.000289 * (3.0 * mean_anomaly).to_radians().sin();
let true_longitude = mean_longitude + equation_of_center;
let omega = 125.04 - 1_934.136 * t;
normalize_degrees(true_longitude - 0.00569 - 0.00478 * omega.to_radians().sin())
}
fn lahiri_ayanamsa(julian_day: f64) -> f64 {
let t = (julian_day - 2_451_545.0) / 36_525.0;
23.85306 + 1.39722 * t + 0.00018 * t * t - 0.000005 * t * t * t
}
fn normalize_degrees(value: f64) -> f64 {
value.rem_euclid(360.0)
}
fn gregorian_from_unix_seconds(unix_seconds: i64) -> (i32, u32, u32) {
let days_since_epoch = unix_seconds.div_euclid(SECONDS_PER_DAY);
let absolute = UNIX_EPOCH_ABSOLUTE + days_since_epoch;
gregorian_from_absolute(absolute)
}
fn validate_gregorian_date(year: i32, month: u32, day: u32) -> Result<(), BongabdoError> {
if month == 0 || month > 12 || day == 0 || day > days_in_gregorian_month(year, month) {
return Err(BongabdoError::InvalidGregorianDate { year, month, day });
}
Ok(())
}
fn gregorian_from_absolute(absolute: i64) -> (i32, u32, u32) {
let mut low = 1_i32;
let mut high = (absolute / 365 + 2) as i32;
while low < high {
let mid = (low + high + 1) / 2;
if absolute_from_gregorian(mid, 1, 1) <= absolute {
low = mid;
} else {
high = mid - 1;
}
}
let year = low;
let mut month = 1_u32;
while month < 12 && absolute_from_gregorian(year, month + 1, 1) <= absolute {
month += 1;
}
let day = (absolute - absolute_from_gregorian(year, month, 1) + 1) as u32;
(year, month, day)
}
fn julian_day_from_gregorian(year: i32, month: u32, day: u32) -> f64 {
absolute_from_gregorian(year, month, day) as f64 + JULIAN_DAY_OFFSET
}
fn gregorian_from_julian_day(julian_day: f64) -> (i32, u32, u32) {
let absolute = (julian_day - JULIAN_DAY_OFFSET).floor() as i64;
gregorian_from_absolute(absolute)
}
fn absolute_from_gregorian(year: i32, month: u32, day: u32) -> i64 {
let mut total = day as i64;
for current_month in 1..month {
total += days_in_gregorian_month(year, current_month) as i64;
}
let prior_year = (year - 1) as i64;
total + 365 * prior_year + prior_year / 4 - prior_year / 100 + prior_year / 400
}
fn weekday_from_gregorian(year: i32, month: u32, day: u32) -> u32 {
absolute_from_gregorian(year, month, day).rem_euclid(7) as u32
}
fn is_gregorian_leap_year(year: i32) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
fn days_in_gregorian_month(year: i32, month: u32) -> u32 {
match month {
4 | 6 | 9 | 11 => 30,
2 => {
if is_gregorian_leap_year(year) {
29
} else {
28
}
}
_ => 31,
}
}
fn get_bengali_weekday_name(weekday: u32) -> &'static str {
match weekday {
1 => "সোমবার",
2 => "মঙ্গলবার",
3 => "বুধবার",
4 => "বৃহস্পতিবার",
5 => "শুক্রবার",
6 => "শনিবার",
0 => "রবিবার",
_ => "",
}
}
fn get_roman_weekday_name(weekday: u32) -> &'static str {
match weekday {
1 => "Somabar",
2 => "Mangalbar",
3 => "Budhbar",
4 => "Brihospotibar",
5 => "Shukrobar",
6 => "Shonibar",
0 => "Robibar",
_ => "",
}
}
fn get_bengali_month_name(month: u32) -> &'static str {
match month {
1 => "বৈশাখ",
2 => "জ্যৈষ্ঠ",
3 => "আষাঢ়",
4 => "শ্রাবণ",
5 => "ভাদ্র",
6 => "আশ্বিন",
7 => "কার্তিক",
8 => "অগ্রহায়ণ",
9 => "পৌষ",
10 => "মাঘ",
11 => "ফাল্গুন",
12 => "চৈত্র",
_ => "",
}
}
fn get_roman_month_name(month: u32) -> &'static str {
match month {
1 => "Boishakh",
2 => "Joishtho",
3 => "Asharh",
4 => "Shrabon",
5 => "Bhadro",
6 => "Ashwin",
7 => "Kartik",
8 => "Agrahayon",
9 => "Poush",
10 => "Magh",
11 => "Falgun",
12 => "Chaitra",
_ => "",
}
}
fn to_bengali_digits<T: ToString>(input: T) -> String {
input
.to_string()
.chars()
.map(|character| match character {
'0' => '০',
'1' => '১',
'2' => '২',
'3' => '৩',
'4' => '৪',
'5' => '৫',
'6' => '৬',
'7' => '৭',
'8' => '৮',
'9' => '৯',
_ => character,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, UNIX_EPOCH};
fn assert_date(
date: Bongabdo,
standard: CalendarStandard,
year: i32,
month: u32,
day: u32,
weekday: u32,
) {
assert_eq!(date.standard, standard);
assert_eq!(
(date.year, date.month, date.day, date.weekday),
(year, month, day, weekday)
);
}
#[test]
fn bangladesh_modern_keeps_national_days_fixed() {
let language_martyrs_day =
Bongabdo::from_ymd(2026, 2, 21, CalendarStandard::Bangladesh).unwrap();
let victory_day = Bongabdo::from_ymd(2026, 12, 16, CalendarStandard::Bangladesh).unwrap();
assert_date(
language_martyrs_day,
CalendarStandard::Bangladesh,
1432,
11,
8,
6,
);
assert_date(victory_day, CalendarStandard::Bangladesh, 1433, 9, 1, 3);
}
#[test]
fn bangladesh_modern_new_year_boundary_is_fixed() {
let eve = Bongabdo::from_ymd(2026, 4, 13, CalendarStandard::Bangladesh).unwrap();
let new_year = Bongabdo::from_ymd(2026, 4, 14, CalendarStandard::Bangladesh).unwrap();
assert_date(eve, CalendarStandard::Bangladesh, 1432, 12, 30, 1);
assert_date(new_year, CalendarStandard::Bangladesh, 1433, 1, 1, 2);
}
#[test]
fn bangladesh_modern_month_lengths_match_2019_revision() {
let joishtho_start = Bongabdo::from_ymd(2026, 5, 15, CalendarStandard::Bangladesh).unwrap();
let asharh_start = Bongabdo::from_ymd(2026, 6, 15, CalendarStandard::Bangladesh).unwrap();
let kartik_start = Bongabdo::from_ymd(2026, 10, 17, CalendarStandard::Bangladesh).unwrap();
assert_date(joishtho_start, CalendarStandard::Bangladesh, 1433, 2, 1, 5);
assert_date(asharh_start, CalendarStandard::Bangladesh, 1433, 3, 1, 1);
assert_date(kartik_start, CalendarStandard::Bangladesh, 1433, 7, 1, 6);
}
#[test]
fn bangladesh_modern_falgun_varies_by_leap_year() {
let non_leap_falgun_start =
Bongabdo::from_ymd(2025, 2, 14, CalendarStandard::Bangladesh).unwrap();
let non_leap_chaitra_start =
Bongabdo::from_ymd(2025, 3, 15, CalendarStandard::Bangladesh).unwrap();
let leap_falgun_start =
Bongabdo::from_ymd(2024, 2, 14, CalendarStandard::Bangladesh).unwrap();
let leap_falgun_last =
Bongabdo::from_ymd(2024, 3, 14, CalendarStandard::Bangladesh).unwrap();
let leap_chaitra_start =
Bongabdo::from_ymd(2024, 3, 15, CalendarStandard::Bangladesh).unwrap();
assert_date(
non_leap_falgun_start,
CalendarStandard::Bangladesh,
1431,
11,
1,
5,
);
assert_date(
non_leap_chaitra_start,
CalendarStandard::Bangladesh,
1431,
12,
1,
6,
);
assert_date(
leap_falgun_start,
CalendarStandard::Bangladesh,
1430,
11,
1,
3,
);
assert_date(
leap_falgun_last,
CalendarStandard::Bangladesh,
1430,
11,
30,
4,
);
assert_date(
leap_chaitra_start,
CalendarStandard::Bangladesh,
1430,
12,
1,
5,
);
}
#[test]
fn bangladesh_legacy_matches_old_fixed_structure() {
let legacy_new_year_eve =
Bongabdo::from_ymd(2026, 4, 13, CalendarStandard::BangladeshLegacy).unwrap();
let legacy_new_year =
Bongabdo::from_ymd(2026, 4, 14, CalendarStandard::BangladeshLegacy).unwrap();
assert_date(
legacy_new_year_eve,
CalendarStandard::BangladeshLegacy,
1432,
12,
30,
1,
);
assert_date(
legacy_new_year,
CalendarStandard::BangladeshLegacy,
1433,
1,
1,
2,
);
}
#[test]
fn bangladesh_legacy_preserves_old_falgun_length() {
let leap_falgun_last =
Bongabdo::from_ymd(2024, 3, 14, CalendarStandard::BangladeshLegacy).unwrap();
let leap_chaitra_start =
Bongabdo::from_ymd(2024, 3, 15, CalendarStandard::BangladeshLegacy).unwrap();
let legacy_ashwin_start =
Bongabdo::from_ymd(2026, 9, 16, CalendarStandard::BangladeshLegacy).unwrap();
assert_date(
leap_falgun_last,
CalendarStandard::BangladeshLegacy,
1430,
11,
31,
4,
);
assert_date(
leap_chaitra_start,
CalendarStandard::BangladeshLegacy,
1430,
12,
1,
5,
);
assert_date(
legacy_ashwin_start,
CalendarStandard::BangladeshLegacy,
1433,
6,
1,
3,
);
}
#[test]
fn west_bengal_new_year_moves_with_traditional_rule() {
let year_2024 =
Bongabdo::from_ymd(2024, 4, 14, CalendarStandard::WestBengalTraditional).unwrap();
let year_2025_eve =
Bongabdo::from_ymd(2025, 4, 14, CalendarStandard::WestBengalTraditional).unwrap();
let year_2025 =
Bongabdo::from_ymd(2025, 4, 15, CalendarStandard::WestBengalTraditional).unwrap();
let year_2026 =
Bongabdo::from_ymd(2026, 4, 15, CalendarStandard::WestBengalTraditional).unwrap();
assert_date(
year_2024,
CalendarStandard::WestBengalTraditional,
1431,
1,
1,
0,
);
assert_date(
year_2025_eve,
CalendarStandard::WestBengalTraditional,
1431,
12,
31,
1,
);
assert_date(
year_2025,
CalendarStandard::WestBengalTraditional,
1432,
1,
1,
2,
);
assert_date(
year_2026,
CalendarStandard::WestBengalTraditional,
1433,
1,
1,
3,
);
}
#[test]
fn west_bengal_month_boundaries_are_not_fixed_offsets() {
let boishakh_end =
Bongabdo::from_ymd(2026, 5, 15, CalendarStandard::WestBengalTraditional).unwrap();
let joishtho_start =
Bongabdo::from_ymd(2026, 5, 16, CalendarStandard::WestBengalTraditional).unwrap();
let chaitra_end =
Bongabdo::from_ymd(2026, 4, 14, CalendarStandard::WestBengalTraditional).unwrap();
assert_date(
boishakh_end,
CalendarStandard::WestBengalTraditional,
1433,
1,
31,
5,
);
assert_date(
joishtho_start,
CalendarStandard::WestBengalTraditional,
1433,
2,
1,
6,
);
assert_date(
chaitra_end,
CalendarStandard::WestBengalTraditional,
1432,
12,
30,
2,
);
}
#[test]
fn parses_calendar_aliases() {
assert_eq!("BD".parse(), Ok(CalendarStandard::Bangladesh));
assert_eq!("BD_LEGACY".parse(), Ok(CalendarStandard::BangladeshLegacy));
assert_eq!("WB".parse(), Ok(CalendarStandard::WestBengalTraditional));
assert_eq!("IN".parse(), Ok(CalendarStandard::WestBengalTraditional));
assert_eq!("india".parse(), Ok(CalendarStandard::WestBengalTraditional));
}
#[test]
fn rejects_unknown_calendar_aliases() {
assert_eq!(
"XX".parse::<CalendarStandard>(),
Err(ParseCalendarStandardError)
);
assert_eq!(
"BANGLA".parse::<CalendarStandard>(),
Err(ParseCalendarStandardError)
);
}
#[test]
fn rejects_invalid_gregorian_dates() {
let february_overflow = Bongabdo::from_ymd(2026, 2, 29, CalendarStandard::Bangladesh);
let month_overflow = Bongabdo::from_ymd(2026, 13, 1, CalendarStandard::Bangladesh);
let zero_day = Bongabdo::from_ymd(2026, 1, 0, CalendarStandard::Bangladesh);
assert_eq!(
february_overflow,
Err(BongabdoError::InvalidGregorianDate {
year: 2026,
month: 2,
day: 29,
})
);
assert_eq!(
month_overflow,
Err(BongabdoError::InvalidGregorianDate {
year: 2026,
month: 13,
day: 1,
})
);
assert_eq!(
zero_day,
Err(BongabdoError::InvalidGregorianDate {
year: 2026,
month: 1,
day: 0,
})
);
}
#[test]
fn formatting_and_display_are_consistent() {
let bd = Bongabdo::from_ymd(2026, 4, 14, CalendarStandard::Bangladesh).unwrap();
let wb = Bongabdo::from_ymd(2026, 4, 15, CalendarStandard::WestBengalTraditional).unwrap();
assert_eq!(bd.month_name(), "বৈশাখ");
assert_eq!(bd.month_name_roman(), "Boishakh");
assert_eq!(bd.weekday_name(), "মঙ্গলবার");
assert_eq!(bd.weekday_name_roman(), "Mangalbar");
assert_eq!(bd.to_bengali_string(), "মঙ্গলবার ১ বৈশাখ, ১৪৩৩");
assert_eq!(bd.to_roman_string(), "Mangalbar 1 Boishakh, 1433");
assert_eq!(bd.to_string(), bd.to_roman_string());
assert_eq!(format!("{bd}"), "Mangalbar 1 Boishakh, 1433");
assert_eq!(wb.to_roman_string(), "Budhbar 1 Boishakh, 1433");
}
#[test]
fn from_system_time_uses_standard_specific_local_day_boundaries() {
let timestamp = UNIX_EPOCH + Duration::from_secs(1_776_104_100);
let bangladesh =
Bongabdo::from_system_time(timestamp, CalendarStandard::Bangladesh).unwrap();
let west_bengal =
Bongabdo::from_system_time(timestamp, CalendarStandard::WestBengalTraditional).unwrap();
assert_eq!(
bangladesh,
Bongabdo::from_ymd(2026, 4, 14, CalendarStandard::Bangladesh).unwrap()
);
assert_eq!(
west_bengal,
Bongabdo::from_ymd(2026, 4, 13, CalendarStandard::WestBengalTraditional).unwrap()
);
}
#[test]
fn from_system_time_matches_west_bengal_local_midnight_rollover() {
let timestamp = UNIX_EPOCH + Duration::from_secs(1_776_105_900);
let west_bengal =
Bongabdo::from_system_time(timestamp, CalendarStandard::WestBengalTraditional).unwrap();
assert_eq!(
west_bengal,
Bongabdo::from_ymd(2026, 4, 14, CalendarStandard::WestBengalTraditional).unwrap()
);
}
#[test]
fn from_system_time_rejects_pre_epoch_values() {
let result = Bongabdo::from_system_time(
UNIX_EPOCH - Duration::from_secs(1),
CalendarStandard::Bangladesh,
);
assert_eq!(result, Err(BongabdoError::SystemTimeBeforeUnixEpoch));
}
#[test]
fn weekday_mapping_matches_known_gregorian_dates() {
let sunday = Bongabdo::from_ymd(2026, 10, 18, CalendarStandard::Bangladesh).unwrap();
let monday = Bongabdo::from_ymd(2026, 10, 19, CalendarStandard::Bangladesh).unwrap();
assert_eq!(sunday.weekday, 0);
assert_eq!(sunday.weekday_name_roman(), "Robibar");
assert_eq!(monday.weekday, 1);
assert_eq!(monday.weekday_name_roman(), "Somabar");
}
#[test]
fn api_examples_match_documented_results() {
let bangladesh = Bongabdo::from_ymd(2026, 4, 14, CalendarStandard::Bangladesh).unwrap();
let west_bengal =
Bongabdo::from_ymd(2026, 4, 15, CalendarStandard::WestBengalTraditional).unwrap();
assert_eq!(
(bangladesh.year, bangladesh.month, bangladesh.day),
(1433, 1, 1)
);
assert_eq!(
(west_bengal.year, west_bengal.month, west_bengal.day),
(1433, 1, 1)
);
}
}