use crate::temporal_iso::{IsoDate, MAX_EPOCH_DAYS, MIN_EPOCH_DAYS, Overflow, Unit};
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
const CALENDAR_ALIASES: &[(&str, &[&str])] = &[
("iso8601", &[]),
("gregory", &["gregorian"]),
("buddhist", &[]),
("japanese", &[]),
("roc", &["minguo"]),
("persian", &[]),
("islamic-civil", &["islamicc"]),
("islamic-tbla", &[]),
("islamic-umalqura", &[]),
("hebrew", &[]),
("chinese", &[]),
("dangi", &[]),
("indian", &[]),
("coptic", &[]),
("ethiopic", &[]),
("ethioaa", &["ethiopic-amete-alem"]),
];
#[must_use]
pub(crate) fn canonicalize_calendar(s: &str) -> Option<&'static str> {
if !s.is_ascii() {
return None;
}
for &(canon, aliases) in CALENDAR_ALIASES {
if s.eq_ignore_ascii_case(canon) {
return Some(canon);
}
for &a in aliases {
if s.eq_ignore_ascii_case(a) {
return Some(canon);
}
}
}
None
}
#[must_use]
pub(crate) fn is_iso(cal: &str) -> bool {
cal == "iso8601"
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CalFields {
pub era: Option<String>,
pub era_year: Option<i64>,
pub year: i64,
pub month: i64,
pub month_code: String,
pub day: i64,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct FieldsInput {
pub era: Option<String>,
pub era_year: Option<i64>,
pub year: Option<i64>,
pub month: Option<i64>,
pub month_code: Option<String>,
pub day: i64,
}
#[derive(Clone, Debug)]
pub(crate) enum CalError {
Range(String),
MissingFields(String),
}
fn greg_to_jdn(year: i64, month: i64, day: i64) -> i64 {
let a = (14 - month).div_euclid(12);
let y = year + 4800 - a;
let m = month + 12 * a - 3;
day + (153 * m + 2).div_euclid(5) + 365 * y + y.div_euclid(4) - y.div_euclid(100)
+ y.div_euclid(400)
- 32045
}
fn jdn_to_greg(jdn: i64) -> (i64, i64, i64) {
let a = jdn + 32044;
let b = (4 * a + 3).div_euclid(146097);
let c = a - (146097 * b).div_euclid(4);
let d = (4 * c + 3).div_euclid(1461);
let e = c - (1461 * d).div_euclid(4);
let m = (5 * e + 2).div_euclid(153);
let day = e - (153 * m + 2).div_euclid(5) + 1;
let month = m + 3 - 12 * m.div_euclid(10);
let year = 100 * b + d - 4800 + m.div_euclid(10);
(year, month, day)
}
fn iso_to_jdn(iso: IsoDate) -> i64 {
greg_to_jdn(
i64::from(iso.year),
i64::from(iso.month),
i64::from(iso.day),
)
}
fn jdn_to_iso(jdn: i64) -> IsoDate {
let (y, m, d) = jdn_to_greg(jdn);
IsoDate {
year: y as i32,
month: m as u8,
day: d as u8,
}
}
fn greg_leap(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
const COPTIC_EPOCH: i64 = 1_825_030;
const ETHIOPIC_EPOCH: i64 = 1_724_221;
fn coptic_like_to_jdn(epoch: i64, year: i64, month: i64, day: i64) -> i64 {
epoch - 1 + 365 * (year - 1) + year.div_euclid(4) + 30 * (month - 1) + day
}
fn coptic_like_from_jdn(epoch: i64, jdn: i64) -> (i64, i64, i64) {
let mut year = (4 * (jdn - epoch) + 1463).div_euclid(1461);
while coptic_like_to_jdn(epoch, year, 1, 1) > jdn {
year -= 1;
}
while coptic_like_to_jdn(epoch, year + 1, 1, 1) <= jdn {
year += 1;
}
let doy = jdn - coptic_like_to_jdn(epoch, year, 1, 1); if doy < 360 {
(year, doy / 30 + 1, doy % 30 + 1)
} else {
(year, 13, doy - 360 + 1)
}
}
fn coptic_like_leap(year: i64) -> bool {
year.rem_euclid(4) == 3
}
fn indian_to_jdn(saka_year: i64, month: i64, day: i64) -> i64 {
let greg_year = saka_year + 78;
let leap = greg_leap(greg_year);
let chaitra1 = greg_to_jdn(greg_year, 3, if leap { 21 } else { 22 });
let mut jdn = chaitra1;
if month == 1 {
jdn += day - 1;
} else {
jdn += if leap { 31 } else { 30 }; let mut m = 2;
while m < month {
jdn += if m <= 6 { 31 } else { 30 };
m += 1;
}
jdn += day - 1;
}
jdn
}
fn indian_from_jdn(jdn: i64) -> (i64, i64, i64) {
let (gy, _, _) = jdn_to_greg(jdn);
let leap_gy = greg_leap(gy);
let chaitra1 = greg_to_jdn(gy, 3, if leap_gy { 21 } else { 22 });
let (saka_year, mut yday, leap) = if jdn >= chaitra1 {
(gy - 78, jdn - chaitra1, leap_gy)
} else {
let prev_leap = greg_leap(gy - 1);
let prev = greg_to_jdn(gy - 1, 3, if prev_leap { 21 } else { 22 });
(gy - 79, jdn - prev, prev_leap)
};
let chaitra_len = if leap { 31 } else { 30 };
if yday < chaitra_len {
(saka_year, 1, yday + 1)
} else {
yday -= chaitra_len;
if yday < 5 * 31 {
(saka_year, 2 + yday / 31, yday % 31 + 1)
} else {
yday -= 5 * 31;
(saka_year, 7 + yday / 30, yday % 30 + 1)
}
}
}
fn indian_leap(saka_year: i64) -> bool {
greg_leap(saka_year + 78)
}
fn islamic_delta(cal: &str) -> i64 {
match cal {
"islamic-tbla" => -1,
_ => 0,
}
}
const ISLAMIC_EPOCH: i64 = 1_948_440;
fn islamic_civil_to_jdn(year: i64, month: i64, day: i64) -> i64 {
let before = 29 * (month - 1) + month.div_euclid(2);
day + before + (year - 1) * 354 + (3 + 11 * year).div_euclid(30) + ISLAMIC_EPOCH - 1
}
fn islamic_civil_from_jdn(jdn: i64) -> (i64, i64, i64) {
let mut year = (30 * (jdn - ISLAMIC_EPOCH) + 10646).div_euclid(10631);
while islamic_civil_to_jdn(year, 1, 1) > jdn {
year -= 1;
}
while islamic_civil_to_jdn(year + 1, 1, 1) <= jdn {
year += 1;
}
let mut month = 1;
while month < 12 && islamic_civil_to_jdn(year, month + 1, 1) <= jdn {
month += 1;
}
let day = jdn - islamic_civil_to_jdn(year, month, 1) + 1;
(year, month, day)
}
#[cfg(feature = "intl")]
fn islamic_from_jdn(cal: &str, jdn: i64) -> (i64, i64, i64) {
if cal == "islamic-umalqura" {
let (y, _, _) = islamic_civil_from_jdn(jdn);
if (1300..=1600).contains(&y) {
return intl::calendar::jdn_to_umalqura(jdn);
}
return islamic_civil_from_jdn(jdn);
}
islamic_civil_from_jdn(jdn - islamic_delta(cal))
}
#[cfg(feature = "intl")]
fn islamic_to_jdn(cal: &str, y: i64, m: i64, d: i64) -> i64 {
if cal == "islamic-umalqura" {
if (1300..=1600).contains(&y) {
return intl::calendar::umalqura_to_jdn(y, m, d);
}
return islamic_civil_to_jdn(y, m, d);
}
islamic_civil_to_jdn(y, m, d) + islamic_delta(cal)
}
#[cfg(not(feature = "intl"))]
fn islamic_from_jdn(_cal: &str, jdn: i64) -> (i64, i64, i64) {
jdn_to_greg(jdn)
}
#[cfg(not(feature = "intl"))]
fn islamic_to_jdn(_cal: &str, y: i64, m: i64, d: i64) -> i64 {
greg_to_jdn(y, m, d)
}
const PERSIAN_EPOCH: i64 = 1_948_320;
fn persian_to_jdn(year: i64, month: i64, day: i64) -> i64 {
let month_days = if month <= 7 {
(month - 1) * 31
} else {
(month - 1) * 30 + 6
};
PERSIAN_EPOCH - 1 + 365 * (year - 1) + (8 * year + 21).div_euclid(33) + month_days + day
}
fn persian_from_jdn(jdn: i64) -> (i64, i64, i64) {
let mut year = 475 + (jdn - PERSIAN_EPOCH).div_euclid(366);
while persian_to_jdn(year, 1, 1) > jdn {
year -= 1;
}
while persian_to_jdn(year + 1, 1, 1) <= jdn {
year += 1;
}
let mut month = 1;
while month < 12 && persian_to_jdn(year, month + 1, 1) <= jdn {
month += 1;
}
let day = jdn - persian_to_jdn(year, month, 1) + 1;
(year, month, day)
}
#[cfg(feature = "intl")]
fn hebrew_from_jdn(jdn: i64) -> (i64, i64, i64) {
intl::calendar::jdn_to_hebrew(jdn)
}
#[cfg(feature = "intl")]
fn hebrew_to_jdn(y: i64, m: i64, d: i64) -> i64 {
intl::calendar::hebrew_to_jdn(y, m, d)
}
#[cfg(not(feature = "intl"))]
fn hebrew_from_jdn(jdn: i64) -> (i64, i64, i64) {
jdn_to_greg(jdn)
}
#[cfg(not(feature = "intl"))]
fn hebrew_to_jdn(y: i64, m: i64, d: i64) -> i64 {
greg_to_jdn(y, m, d)
}
fn hebrew_leap(year: i64) -> bool {
(7 * year + 1).rem_euclid(19) < 7
}
fn chinese_from_jdn(cal: &str, jdn: i64) -> Option<(i64, i64, i64, bool)> {
let d = super::temporal_astro::from_jdn(lunisolar_kind(cal), jdn)?;
Some((d.year, d.month, d.day, d.leap))
}
fn chinese_to_jdn(cal: &str, y: i64, m: i64, d: i64, leap: bool) -> Option<i64> {
super::temporal_astro::to_jdn(lunisolar_kind(cal), y, m, d, leap)
}
fn lunisolar_kind(cal: &str) -> super::temporal_astro::Lunisolar {
if cal == "dangi" {
super::temporal_astro::Lunisolar::Dangi
} else {
super::temporal_astro::Lunisolar::Chinese
}
}
fn chinese_leap_month(cal: &str, year: i64) -> i64 {
super::temporal_astro::leap_month_of_year(lunisolar_kind(cal), year)
}
struct Parts {
year: i64,
month: i64, day: i64,
code: String,
_leap: bool,
}
fn hebrew_month_list(year: i64) -> Vec<(i64, String)> {
let leap = hebrew_leap(year);
let seq: &[i64] = if leap {
&[7, 8, 9, 10, 11, 12, 13, 1, 2, 3, 4, 5, 6]
} else {
&[7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6]
};
seq.iter()
.map(|&m| {
let code = match m {
7 => "M01".to_string(),
8 => "M02".to_string(),
9 => "M03".to_string(),
10 => "M04".to_string(),
11 => "M05".to_string(),
12 => {
if leap {
"M05L".to_string()
} else {
"M06".to_string()
}
}
13 => "M06".to_string(),
1 => "M07".to_string(),
2 => "M08".to_string(),
3 => "M09".to_string(),
4 => "M10".to_string(),
5 => "M11".to_string(),
_ => "M12".to_string(),
};
(m, code)
})
.collect()
}
fn chinese_month_list(cal: &str, year: i64) -> Vec<(i64, bool, String)> {
let leap_m = chinese_leap_month(cal, year);
let mut out = Vec::new();
for m in 1..=12 {
out.push((m, false, format!("M{m:02}")));
if m == leap_m {
out.push((m, true, format!("M{m:02}L")));
}
}
out
}
fn parts_from_iso(cal: &str, iso: IsoDate) -> Parts {
let jdn = iso_to_jdn(iso);
match cal {
"gregory" | "japanese" => Parts {
year: i64::from(iso.year),
month: i64::from(iso.month),
day: i64::from(iso.day),
code: format!("M{:02}", iso.month),
_leap: false,
},
"buddhist" => Parts {
year: i64::from(iso.year) + 543,
month: i64::from(iso.month),
day: i64::from(iso.day),
code: format!("M{:02}", iso.month),
_leap: false,
},
"roc" => Parts {
year: i64::from(iso.year) - 1911,
month: i64::from(iso.month),
day: i64::from(iso.day),
code: format!("M{:02}", iso.month),
_leap: false,
},
"persian" => {
let (y, m, d) = persian_from_jdn(jdn);
Parts {
year: y,
month: m,
day: d,
code: format!("M{m:02}"),
_leap: false,
}
}
"islamic-civil" | "islamic-tbla" | "islamic-umalqura" => {
let (y, m, d) = islamic_from_jdn(cal, jdn);
Parts {
year: y,
month: m,
day: d,
code: format!("M{m:02}"),
_leap: false,
}
}
"indian" => {
let (y, m, d) = indian_from_jdn(jdn);
Parts {
year: y,
month: m,
day: d,
code: format!("M{m:02}"),
_leap: false,
}
}
"coptic" => {
let (y, m, d) = coptic_like_from_jdn(COPTIC_EPOCH, jdn);
Parts {
year: y,
month: m,
day: d,
code: format!("M{m:02}"),
_leap: false,
}
}
"ethiopic" => {
let (y, m, d) = coptic_like_from_jdn(ETHIOPIC_EPOCH, jdn);
Parts {
year: y,
month: m,
day: d,
code: format!("M{m:02}"),
_leap: false,
}
}
"ethioaa" => {
let (y, m, d) = coptic_like_from_jdn(ETHIOPIC_EPOCH, jdn);
Parts {
year: y + 5500,
month: m,
day: d,
code: format!("M{m:02}"),
_leap: false,
}
}
"hebrew" => {
let (y, im, d) = hebrew_from_jdn(jdn);
let list = hebrew_month_list(y);
let (ord, code) = list
.iter()
.enumerate()
.find(|(_, (m, _))| *m == im)
.map(|(i, (_, c))| (i as i64 + 1, c.clone()))
.unwrap_or((im, format!("M{im:02}")));
Parts {
year: y,
month: ord,
day: d,
code,
_leap: false,
}
}
"chinese" | "dangi" => {
if let Some((y, nominal, d, leap)) = chinese_from_jdn(cal, jdn) {
let list = chinese_month_list(cal, y);
let (ord, code) = list
.iter()
.enumerate()
.find(|(_, (m, lp, _))| *m == nominal && *lp == leap)
.map(|(i, (_, _, c))| (i as i64 + 1, c.clone()))
.unwrap_or((nominal, format!("M{nominal:02}")));
Parts {
year: y,
month: ord,
day: d,
code,
_leap: leap,
}
} else {
Parts {
year: i64::from(iso.year),
month: i64::from(iso.month),
day: i64::from(iso.day),
code: format!("M{:02}", iso.month),
_leap: false,
}
}
}
_ => Parts {
year: i64::from(iso.year),
month: i64::from(iso.month),
day: i64::from(iso.day),
code: format!("M{:02}", iso.month),
_leap: false,
},
}
}
fn iso_from_parts(cal: &str, year: i64, ord_month: i64, day: i64) -> Option<IsoDate> {
let iso = match cal {
"gregory" | "japanese" => jdn_to_iso(greg_to_jdn(year, ord_month, day)),
"buddhist" => jdn_to_iso(greg_to_jdn(year - 543, ord_month, day)),
"roc" => jdn_to_iso(greg_to_jdn(year + 1911, ord_month, day)),
"persian" => jdn_to_iso(persian_to_jdn(year, ord_month, day)),
"islamic-civil" | "islamic-tbla" | "islamic-umalqura" => {
jdn_to_iso(islamic_to_jdn(cal, year, ord_month, day))
}
"indian" => jdn_to_iso(indian_to_jdn(year, ord_month, day)),
"coptic" => jdn_to_iso(coptic_like_to_jdn(COPTIC_EPOCH, year, ord_month, day)),
"ethiopic" => jdn_to_iso(coptic_like_to_jdn(ETHIOPIC_EPOCH, year, ord_month, day)),
"ethioaa" => jdn_to_iso(coptic_like_to_jdn(
ETHIOPIC_EPOCH,
year - 5500,
ord_month,
day,
)),
"hebrew" => {
let list = hebrew_month_list(year);
let intl_m = list.get((ord_month - 1) as usize).map(|(m, _)| *m)?;
jdn_to_iso(hebrew_to_jdn(year, intl_m, day))
}
"chinese" | "dangi" => {
let list = chinese_month_list(cal, year);
let (nominal, leap) = list
.get((ord_month - 1) as usize)
.map(|(m, l, _)| (*m, *l))?;
jdn_to_iso(chinese_to_jdn(cal, year, nominal, day, leap)?)
}
_ => jdn_to_iso(greg_to_jdn(year, ord_month, day)),
};
Some(iso)
}
#[must_use]
pub(crate) fn has_eras(cal: &str) -> bool {
!matches!(cal, "iso8601" | "chinese" | "dangi")
}
fn derive_era(cal: &str, year: i64, iso: IsoDate) -> (Option<String>, Option<i64>) {
let dual = |pos: &str, neg: &str, y: i64| {
if y >= 1 {
(Some(pos.to_string()), Some(y))
} else {
(Some(neg.to_string()), Some(1 - y))
}
};
match cal {
"iso8601" | "chinese" | "dangi" => (None, None),
"gregory" => dual("ce", "bce", i64::from(iso.year)),
"japanese" => derive_japanese_era(iso),
"buddhist" => (Some("be".to_string()), Some(year)),
"roc" => dual("roc", "broc", year),
"persian" => (Some("ap".to_string()), Some(year)),
"islamic-civil" | "islamic-tbla" | "islamic-umalqura" => dual("ah", "bh", year),
"hebrew" => (Some("am".to_string()), Some(year)),
"indian" => (Some("shaka".to_string()), Some(year)),
"coptic" => (Some("am".to_string()), Some(year)),
"ethioaa" => (Some("aa".to_string()), Some(year)),
"ethiopic" => {
if year >= 1 {
(Some("am".to_string()), Some(year))
} else {
(Some("aa".to_string()), Some(year + 5500))
}
}
_ => (None, None),
}
}
fn derive_japanese_era(iso: IsoDate) -> (Option<String>, Option<i64>) {
let ymd = (
i64::from(iso.year),
i64::from(iso.month),
i64::from(iso.day),
);
if ymd >= (1868, 10, 23) {
let (name, ey) = japanese_era_name(iso);
if name == "meiji" && ymd < (1873, 1, 1) {
return (Some("ce".to_string()), Some(i64::from(iso.year)));
}
(Some(name), Some(ey))
} else if iso.year >= 1 {
(Some("ce".to_string()), Some(i64::from(iso.year)))
} else {
(Some("bce".to_string()), Some(1 - i64::from(iso.year)))
}
}
fn japanese_era_name(iso: IsoDate) -> (String, i64) {
#[cfg(feature = "intl")]
{
let (name, ey) = intl::calendar::japanese_era(
i64::from(iso.year),
i64::from(iso.month),
i64::from(iso.day),
);
(name.to_ascii_lowercase(), ey)
}
#[cfg(not(feature = "intl"))]
{
const ERAS: [(i64, i64, i64, &str); 5] = [
(1868, 10, 23, "meiji"),
(1912, 7, 30, "taisho"),
(1926, 12, 25, "showa"),
(1989, 1, 8, "heisei"),
(2019, 5, 1, "reiwa"),
];
let (y, m, d) = (
i64::from(iso.year),
i64::from(iso.month),
i64::from(iso.day),
);
for &(sy, sm, sd, name) in ERAS.iter().rev() {
if (y, m, d) >= (sy, sm, sd) {
return (name.to_string(), y - sy + 1);
}
}
("ce".to_string(), y)
}
}
fn era_to_year(cal: &str, era: &str, era_year: i64) -> Option<i64> {
let era = match era {
"ad" => "ce",
"bc" => "bce",
other => other,
};
let dual = |pos: &str, neg: &str| {
if era == pos {
Some(era_year)
} else if era == neg {
Some(1 - era_year)
} else {
None
}
};
match cal {
"gregory" => dual("ce", "bce"),
"japanese" => match era {
"ce" => Some(era_year),
"bce" => Some(1 - era_year),
"meiji" => Some(1867 + era_year),
"taisho" => Some(1911 + era_year),
"showa" => Some(1925 + era_year),
"heisei" => Some(1988 + era_year),
"reiwa" => Some(2018 + era_year),
_ => None,
},
"buddhist" => (era == "be").then_some(era_year),
"roc" => dual("roc", "broc"),
"persian" => (era == "ap").then_some(era_year),
"islamic-civil" | "islamic-tbla" | "islamic-umalqura" => dual("ah", "bh"),
"hebrew" => (era == "am").then_some(era_year),
"indian" => (era == "shaka").then_some(era_year),
"coptic" => (era == "am").then_some(era_year),
"ethioaa" => (era == "aa").then_some(era_year),
"ethiopic" => match era {
"am" => Some(era_year),
"aa" => Some(era_year - 5500),
_ => None,
},
_ => None,
}
}
#[must_use]
pub(crate) fn iso_to_fields(cal: &str, iso: IsoDate) -> CalFields {
let parts = parts_from_iso(cal, iso);
let (era, era_year) = derive_era(cal, parts.year, iso);
CalFields {
era,
era_year,
year: parts.year,
month: parts.month,
month_code: parts.code,
day: parts.day,
}
}
fn parse_month_code(code: &str) -> Option<(i64, bool)> {
let b = code.as_bytes();
let leap = b.len() == 4 && b[3] == b'L';
if !((b.len() == 3 || leap) && b[0] == b'M' && b[1].is_ascii_digit() && b[2].is_ascii_digit()) {
return None;
}
let n = i64::from(b[1] - b'0') * 10 + i64::from(b[2] - b'0');
Some((n, leap))
}
fn month_code_to_ordinal(cal: &str, year: i64, code: &str) -> Option<i64> {
match cal {
"hebrew" => hebrew_month_list(year)
.iter()
.position(|(_, c)| c == code)
.map(|i| i as i64 + 1),
"chinese" | "dangi" => chinese_month_list(cal, year)
.iter()
.position(|(_, _, c)| c == code)
.map(|i| i as i64 + 1),
_ => {
let (num, leap) = parse_month_code(code)?;
if leap || num < 1 || num > months_in_year_by(cal, year) {
None
} else {
Some(num)
}
}
}
}
fn month_code_constrain_ordinal(cal: &str, year: i64, code: &str) -> Option<i64> {
let base = constrain_leap_base_code(cal, code)?;
month_code_to_ordinal(cal, year, &base)
}
#[must_use]
pub(crate) fn constrain_leap_base_code(cal: &str, code: &str) -> Option<String> {
let (num, leap) = parse_month_code(code)?;
if !leap {
return None;
}
match cal {
"hebrew" => (num == 5).then(|| "M06".to_string()),
"chinese" | "dangi" => (1..=12).contains(&num).then(|| format!("M{num:02}")),
_ => None,
}
}
fn resolve_ordinal(
cal: &str,
year: i64,
month: Option<i64>,
month_code: Option<&str>,
overflow: Overflow,
) -> Result<i64, CalError> {
let n_months = months_in_year_by(cal, year);
if let Some(code) = month_code {
parse_month_code(code)
.ok_or_else(|| CalError::Range(format!("invalid monthCode '{code}'")))?;
let ord = match month_code_to_ordinal(cal, year, code) {
Some(o) => o,
None => {
let not_found = || {
CalError::Range(format!(
"monthCode '{code}' does not occur in {cal} year {year}"
))
};
match overflow {
Overflow::Constrain => {
month_code_constrain_ordinal(cal, year, code).ok_or_else(not_found)?
}
Overflow::Reject => return Err(not_found()),
}
}
};
if let Some(m) = month
&& m != ord
{
return Err(CalError::Range("month and monthCode disagree".to_string()));
}
Ok(ord)
} else if let Some(m) = month {
if m < 1 {
return Err(CalError::Range("month must be positive".to_string()));
}
Ok(match overflow {
Overflow::Constrain => m.min(n_months),
Overflow::Reject => {
if m > n_months {
return Err(CalError::Range(format!(
"month {m} is out of range for {cal} year {year}"
)));
}
m
}
})
} else {
Err(CalError::MissingFields(
"month or monthCode is required".to_string(),
))
}
}
pub(crate) fn fields_to_iso(
cal: &str,
input: &FieldsInput,
overflow: Overflow,
) -> Result<IsoDate, CalError> {
if has_eras(cal) {
match (input.era.as_deref(), input.era_year) {
(Some(era), Some(ey)) => {
if era_to_year(cal, era, ey).is_none() {
return Err(CalError::Range(format!(
"{era} is not a valid era in calendar {cal}"
)));
}
}
(None, None) => {}
_ => {
return Err(CalError::MissingFields(
"era and eraYear must be provided together".to_string(),
));
}
}
}
let year = if let Some(y) = input.year {
y
} else if has_eras(cal) {
match (input.era.as_deref(), input.era_year) {
(Some(era), Some(ey)) => era_to_year(cal, era, ey)
.ok_or_else(|| CalError::Range(format!("invalid era '{era}' for {cal}")))?,
_ => {
return Err(CalError::MissingFields(
"year, or era and eraYear, are required".to_string(),
));
}
}
} else {
return Err(CalError::MissingFields("year is required".to_string()));
};
let ord = resolve_ordinal(
cal,
year,
input.month,
input.month_code.as_deref(),
overflow,
)?;
let day = input.day;
if day < 1 {
return Err(CalError::Range("day must be positive".to_string()));
}
let dim = days_in_month_by(cal, year, ord);
let day = match overflow {
Overflow::Constrain => day.min(dim.max(1)),
Overflow::Reject => {
if day > dim {
return Err(CalError::Range(format!(
"day {day} is out of range for {cal} {year}-M{ord:02}"
)));
}
day
}
};
iso_from_parts(cal, year, ord, day).ok_or_else(|| {
CalError::Range(format!(
"{cal} date {year}-{ord}-{day} is not representable"
))
})
}
fn month_start_jdn(cal: &str, year: i64, month: i64) -> Option<i64> {
let n = months_in_year_by(cal, year);
if month <= n {
iso_from_parts(cal, year, month, 1).map(iso_to_jdn)
} else {
iso_from_parts(cal, year + 1, 1, 1).map(iso_to_jdn)
}
}
fn months_in_year_by(cal: &str, year: i64) -> i64 {
match cal {
"coptic" | "ethiopic" | "ethioaa" => 13,
"hebrew" if hebrew_leap(year) => 13,
"chinese" | "dangi" if chinese_leap_month(cal, year) != 0 => 13,
_ => 12,
}
}
fn days_in_month_by(cal: &str, year: i64, month: i64) -> i64 {
match (
month_start_jdn(cal, year, month),
month_start_jdn(cal, year, month + 1),
) {
(Some(a), Some(b)) => (b - a).max(1),
_ => 30,
}
}
#[must_use]
pub(crate) fn months_in_year(cal: &str, iso: IsoDate) -> i64 {
let p = parts_from_iso(cal, iso);
months_in_year_by(cal, p.year)
}
#[must_use]
pub(crate) fn days_in_month(cal: &str, iso: IsoDate) -> i64 {
let p = parts_from_iso(cal, iso);
days_in_month_by(cal, p.year, p.month)
}
#[must_use]
pub(crate) fn days_in_year(cal: &str, iso: IsoDate) -> i64 {
let p = parts_from_iso(cal, iso);
match (
iso_from_parts(cal, p.year, 1, 1).map(iso_to_jdn),
iso_from_parts(cal, p.year + 1, 1, 1).map(iso_to_jdn),
) {
(Some(a), Some(b)) => (b - a).max(1),
_ => 365,
}
}
#[must_use]
pub(crate) fn in_leap_year(cal: &str, iso: IsoDate) -> bool {
let p = parts_from_iso(cal, iso);
match cal {
"gregory" | "japanese" => greg_leap(i64::from(iso.year)),
"buddhist" => greg_leap(i64::from(iso.year)),
"roc" => greg_leap(i64::from(iso.year)),
"coptic" | "ethiopic" => coptic_like_leap(p.year),
"ethioaa" => coptic_like_leap(p.year - 5500),
"indian" => indian_leap(p.year),
"hebrew" => hebrew_leap(p.year),
"chinese" | "dangi" => chinese_leap_month(cal, p.year) != 0,
_ => days_in_year(cal, iso) > days_in_year_min(cal),
}
}
fn days_in_year_min(cal: &str) -> i64 {
match cal {
"islamic-civil" | "islamic-tbla" | "islamic-umalqura" => 354,
"persian" => 365,
_ => 365,
}
}
#[must_use]
pub(crate) fn day_of_week(iso: IsoDate) -> i64 {
iso_to_jdn(iso).rem_euclid(7) + 1
}
#[must_use]
pub(crate) fn days_in_week() -> i64 {
7
}
#[must_use]
pub(crate) fn day_of_year(cal: &str, iso: IsoDate) -> i64 {
let p = parts_from_iso(cal, iso);
match iso_from_parts(cal, p.year, 1, 1).map(iso_to_jdn) {
Some(start) => iso_to_jdn(iso) - start + 1,
None => 1,
}
}
#[must_use]
pub(crate) fn week_of_year(cal: &str, iso: IsoDate) -> Option<(i64, i64)> {
if cal != "iso8601" {
return None;
}
let jdn = iso_to_jdn(iso);
let weekday = jdn.rem_euclid(7) + 1; let thursday = jdn - (weekday - 4);
let (iso_year, _, _) = jdn_to_greg(thursday);
let jan4 = greg_to_jdn(iso_year, 1, 4);
let jan4_weekday = jan4.rem_euclid(7) + 1;
let week1_monday = jan4 - (jan4_weekday - 1);
let week = (jdn - week1_monday) / 7 + 1;
Some((week, iso_year))
}
#[must_use]
pub(crate) fn year_of_week(cal: &str, iso: IsoDate) -> Option<i64> {
week_of_year(cal, iso).map(|(_, y)| y)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct DateDurationParts {
pub years: i64,
pub months: i64,
pub weeks: i64,
pub days: i64,
}
const CAL_YEAR_LIMIT: i64 = 500_000;
fn add_ordinal_months(cal: &str, mut year: i64, mut ord_month: i64, months: i64) -> (i64, i64) {
if months >= 0 {
let mut rem = months;
while rem > 0 {
let n = months_in_year_by(cal, year);
let room = n - ord_month; if rem <= room {
ord_month += rem;
rem = 0;
} else {
rem -= room + 1; year += 1;
ord_month = 1;
}
}
} else {
let mut rem = -months;
while rem > 0 {
let room = ord_month - 1; if rem <= room {
ord_month -= rem;
rem = 0;
} else {
rem -= ord_month; year -= 1;
ord_month = months_in_year_by(cal, year);
}
}
}
(year, ord_month)
}
pub(crate) fn calendar_date_add(
cal: &str,
iso: IsoDate,
years: i64,
months: i64,
weeks: i64,
days: i64,
overflow: Overflow,
) -> Result<IsoDate, CalError> {
let f = iso_to_fields(cal, iso);
let year_after = f.year + years;
let start_ord = resolve_ordinal(cal, year_after, None, Some(&f.month_code), overflow)?;
let (year, ord_month) = add_ordinal_months(cal, year_after, start_ord, months);
if !(-CAL_YEAR_LIMIT..=CAL_YEAR_LIMIT).contains(&year) {
return Err(CalError::Range(format!(
"{cal} year {year} is out of the representable range"
)));
}
let dim = days_in_month_by(cal, year, ord_month);
let day = match overflow {
Overflow::Constrain => f.day.min(dim.max(1)),
Overflow::Reject => {
if f.day > dim {
return Err(CalError::Range(format!(
"day {} is out of range for {cal} {year}-M{ord_month:02}",
f.day
)));
}
f.day
}
};
let base = iso_from_parts(cal, year, ord_month, day).ok_or_else(|| {
CalError::Range(format!(
"{cal} date {year}-{ord_month}-{day} is not representable"
))
})?;
let jdn = iso_to_jdn(base) + weeks * 7 + days;
let epoch_days = jdn - greg_to_jdn(1970, 1, 1);
if !(MIN_EPOCH_DAYS - 2..=MAX_EPOCH_DAYS + 2).contains(&epoch_days) {
return Err(CalError::Range(
"result is outside the representable range".to_string(),
));
}
Ok(jdn_to_iso(jdn))
}
fn ordinal_month_distance(cal: &str, y_from: i64, o_from: i64, y_to: i64, o_to: i64) -> i64 {
if (y_from, o_from) == (y_to, o_to) {
return 0;
}
let forward = (y_to, o_to) > (y_from, o_from);
let (ya, oa, yb, ob) = if forward {
(y_from, o_from, y_to, o_to)
} else {
(y_to, o_to, y_from, o_from)
};
let mut sum = 0;
for y in ya..yb {
sum += months_in_year_by(cal, y);
}
sum = sum - oa + ob;
if forward { sum } else { -sum }
}
pub(crate) fn calendar_date_until(
cal: &str,
iso1: IsoDate,
iso2: IsoDate,
largest_unit: Unit,
) -> DateDurationParts {
if matches!(largest_unit, Unit::Week | Unit::Day) {
let mut days = iso_to_jdn(iso2) - iso_to_jdn(iso1);
let mut weeks = 0;
if largest_unit == Unit::Week {
weeks = days / 7;
days %= 7;
}
return DateDurationParts {
weeks,
days,
..Default::default()
};
}
let jdn1 = iso_to_jdn(iso1);
let jdn2 = iso_to_jdn(iso2);
if jdn1 == jdn2 {
return DateDurationParts::default();
}
let sign = if jdn2 > jdn1 { 1 } else { -1 };
let f1 = iso_to_fields(cal, iso1);
let f2 = iso_to_fields(cal, iso2);
let f1_day = f1.day;
let add = |y: i64, m: i64| -> IsoDate {
calendar_date_add(cal, iso1, y, m, 0, 0, Overflow::Constrain).unwrap_or(iso1)
};
let passes = |y: i64, m: i64| -> bool {
let tf = iso_to_fields(cal, add(y, m));
let a = (tf.year, tf.month, f1_day);
let b = (f2.year, f2.month, f2.day);
if sign > 0 { a > b } else { a < b }
};
let diff_days = f2.day - f1.day;
let diff_in_year_sign = if f2.month_code > f1.month_code {
1
} else if f2.month_code < f1.month_code {
-1
} else {
diff_days.signum()
};
let mut years = if diff_in_year_sign * sign < 0 {
(f2.year - f1.year) - sign
} else {
f2.year - f1.year
};
if passes(years, 0) {
years -= sign;
}
let mut months = 0;
while !passes(years, months + sign) {
months += sign;
}
let mid = add(years, months);
let days = iso_to_jdn(iso2) - iso_to_jdn(mid);
if largest_unit == Unit::Month {
let mid_f = iso_to_fields(cal, mid);
let total_months = ordinal_month_distance(cal, f1.year, f1.month, mid_f.year, mid_f.month);
return DateDurationParts {
months: total_months,
days,
..Default::default()
};
}
DateDurationParts {
years,
months,
days,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn iso(y: i32, m: u8, d: u8) -> IsoDate {
IsoDate {
year: y,
month: m,
day: d,
}
}
#[test]
fn canonicalize() {
assert_eq!(canonicalize_calendar("ISO8601"), Some("iso8601"));
assert_eq!(canonicalize_calendar("islamicc"), Some("islamic-civil"));
assert_eq!(canonicalize_calendar("gregorian"), Some("gregory"));
assert_eq!(canonicalize_calendar("minguo"), Some("roc"));
assert_eq!(
canonicalize_calendar("ethiopic-amete-alem"),
Some("ethioaa")
);
assert_eq!(canonicalize_calendar("islamic"), None);
assert_eq!(canonicalize_calendar("islamic-rgsa"), None);
assert_eq!(canonicalize_calendar("bogus"), None);
}
#[test]
fn gregory_family() {
let f = iso_to_fields("gregory", iso(2000, 3, 6));
assert_eq!(f.era.as_deref(), Some("ce"));
assert_eq!(f.era_year, Some(2000));
assert_eq!(f.year, 2000);
let f = iso_to_fields("buddhist", iso(2000, 1, 1));
assert_eq!(f.year, 2543);
assert_eq!(f.era.as_deref(), Some("be"));
let f = iso_to_fields("roc", iso(2025, 1, 1));
assert_eq!(f.year, 114);
assert_eq!(f.era.as_deref(), Some("roc"));
}
#[cfg(feature = "intl")]
#[test]
fn spot_check_known_dates() {
let h = iso_to_fields("hebrew", iso(2000, 1, 1));
assert_eq!(h.year, 5760);
assert_eq!(h.day, 23);
assert_eq!(h.month_code, "M04");
let p = iso_to_fields("persian", iso(2000, 1, 1));
assert_eq!(p.year, 1378);
assert_eq!(p.month, 10);
assert_eq!(p.day, 11);
let ic = iso_to_fields("islamic-civil", iso(2000, 1, 1));
assert_eq!((ic.year, ic.month, ic.day), (1420, 9, 24));
let it = iso_to_fields("islamic-tbla", iso(2000, 1, 1));
assert_eq!((it.year, it.month, it.day), (1420, 9, 25));
let iu = iso_to_fields("islamic-umalqura", iso(2000, 1, 1));
assert_eq!((iu.year, iu.month, iu.day), (1420, 9, 24));
let j = iso_to_fields("japanese", iso(2000, 1, 1));
assert_eq!(j.era.as_deref(), Some("heisei"));
assert_eq!(j.era_year, Some(12));
assert_eq!(j.year, 2000);
}
#[test]
fn roundtrip_iso_all() {
let cals = [
"gregory", "buddhist", "roc", "japanese", "coptic", "ethiopic", "ethioaa", "indian",
];
for cal in cals {
let d = iso(2023, 6, 15);
let f = iso_to_fields(cal, d);
let input = FieldsInput {
era: f.era.clone(),
era_year: f.era_year,
year: Some(f.year),
month: Some(f.month),
month_code: None,
day: f.day,
};
let back = fields_to_iso(cal, &input, Overflow::Reject)
.unwrap_or_else(|_| panic!("{cal} roundtrip failed"));
assert_eq!(back, d, "calendar {cal} round-trip");
}
}
#[test]
fn arith_gregory_matches_iso() {
use crate::temporal_iso::{Unit, add_iso_date, difference_iso_date};
let cases = [
(iso(2020, 1, 31), 0_i64, 1_i64, 0_i64, 0_i64), (iso(2021, 1, 31), 0, 1, 0, 0), (iso(2020, 2, 29), 1, 0, 0, 0), (iso(2023, 6, 15), 2, 5, 1, 10),
(iso(2023, 12, 15), 0, 0, 0, 40),
(iso(2023, 3, 15), -1, -4, -2, -5),
];
for (d, y, m, w, days) in cases {
let got = calendar_date_add("gregory", d, y, m, w, days, Overflow::Constrain).unwrap();
let want = add_iso_date(d, y, m, w, days, Overflow::Constrain).unwrap();
assert_eq!(got, want, "gregory add {d:?} +{y}y{m}m{w}w{days}d");
}
let pairs = [
(iso(2020, 1, 15), iso(2023, 6, 20)),
(iso(2023, 6, 20), iso(2020, 1, 15)),
(iso(2020, 2, 29), iso(2021, 3, 1)),
(iso(2019, 12, 31), iso(2020, 1, 1)),
];
for lu in [Unit::Year, Unit::Month, Unit::Week, Unit::Day] {
for (a, b) in pairs {
let got = calendar_date_until("gregory", a, b, lu);
let (wy, wm, ww, wd) = difference_iso_date(a, b, lu);
assert_eq!(
(got.years, got.months, got.weeks, got.days),
(wy, wm, ww, wd),
"gregory until {a:?}->{b:?} largest {lu:?}"
);
}
}
}
#[test]
fn arith_add_until_inverse() {
use crate::temporal_iso::Unit;
let cals = ["gregory", "coptic", "ethiopic", "indian", "buddhist", "roc"];
for cal in cals {
let a = iso(2015, 5, 10);
let b = calendar_date_add(cal, a, 2, 3, 0, 0, Overflow::Constrain).unwrap();
let diff = calendar_date_until(cal, a, b, Unit::Year);
let back = calendar_date_add(
cal,
a,
diff.years,
diff.months,
diff.weeks,
diff.days,
Overflow::Constrain,
)
.unwrap();
assert_eq!(back, b, "{cal} add/until inverse");
}
}
#[cfg(feature = "intl")]
fn from_code(cal: &str, year: i64, code: &str, day: i64) -> IsoDate {
fields_to_iso(
cal,
&FieldsInput {
year: Some(year),
month_code: Some(code.to_string()),
day,
..Default::default()
},
Overflow::Reject,
)
.unwrap_or_else(|_| panic!("{cal} {year}-{code}-{day} should be representable"))
}
#[cfg(feature = "intl")]
#[test]
fn arith_hebrew_leap_month() {
use crate::temporal_iso::Unit;
let code_of = |d: IsoDate| iso_to_fields("hebrew", d).month_code;
let year_of = |d: IsoDate| iso_to_fields("hebrew", d).year;
let add =
|d, y, m| calendar_date_add("hebrew", d, y, m, 0, 0, Overflow::Constrain).unwrap();
let nisan_leap = from_code("hebrew", 5782, "M07", 1);
let plus1 = add(nisan_leap, 1, 0);
assert_eq!(year_of(plus1), 5783, "hebrew +1yr advances year");
assert_eq!(code_of(plus1), "M07", "hebrew +1yr preserves monthCode");
let adar1 = from_code("hebrew", 5784, "M05L", 1);
let c = add(adar1, 1, 0);
assert_eq!(
(year_of(c), code_of(c).as_str()),
(5785, "M06"),
"Adar I +1yr → Adar"
);
assert!(
matches!(
calendar_date_add("hebrew", adar1, 1, 0, 0, 0, Overflow::Reject),
Err(CalError::Range(_))
),
"Adar I +1yr rejects when the leap month is absent"
);
let tevet = from_code("hebrew", 5784, "M04", 1);
assert_eq!(code_of(add(tevet, 0, 2)), "M05L", "M04 +2mo → Adar I");
assert_eq!(code_of(add(tevet, 0, 3)), "M06", "M04 +3mo → Adar II");
let leap_shevat = from_code("hebrew", 5784, "M05", 1);
let common2_shevat = from_code("hebrew", 5785, "M05", 1);
let y = calendar_date_until("hebrew", leap_shevat, common2_shevat, Unit::Year);
assert_eq!((y.years, y.months), (1, 0), "M05→M05 leap→common is 1y");
let m = calendar_date_until("hebrew", leap_shevat, common2_shevat, Unit::Month);
assert_eq!(m.months, 13, "M05→M05 leap→common is 13mo not 12mo");
let common2_adar = from_code("hebrew", 5785, "M06", 1);
let ya = calendar_date_until("hebrew", adar1, common2_adar, Unit::Year);
assert_eq!((ya.years, ya.months), (1, 0), "M05L→M06 is 1y");
let ys = calendar_date_until("hebrew", adar1, common2_shevat, Unit::Year);
assert_eq!((ys.years, ys.months), (0, 12), "M05L→M05 is 12mo not 1y");
}
#[cfg(feature = "intl")]
#[test]
fn arith_chinese_leap_month() {
let code_of = |d: IsoDate| iso_to_fields("chinese", d).month_code;
let year_of = |d: IsoDate| iso_to_fields("chinese", d).year;
let leap_year = (2000..=2030)
.find(|&y| chinese_leap_month("chinese", y) != 0)
.expect("a chinese leap year exists in range");
let lm = chinese_leap_month("chinese", leap_year);
let base = format!("M{lm:02}");
let leap = format!("M{lm:02}L");
let m08 = from_code("chinese", leap_year, "M08", 1);
let plus1 = calendar_date_add("chinese", m08, 1, 0, 0, 0, Overflow::Constrain).unwrap();
assert_eq!(year_of(plus1), leap_year + 1, "chinese +1yr advances year");
assert_eq!(code_of(plus1), "M08", "chinese +1yr preserves monthCode");
let base_iso = from_code("chinese", leap_year, &base, 1);
let stepped =
calendar_date_add("chinese", base_iso, 0, 1, 0, 0, Overflow::Constrain).unwrap();
assert_eq!(code_of(stepped), leap, "M{lm:02} +1mo → M{lm:02}L");
let common_year = (leap_year + 1..=2035)
.find(|&y| chinese_leap_month("chinese", y) == 0)
.expect("a chinese common year exists in range");
let fi = |ov| {
fields_to_iso(
"chinese",
&FieldsInput {
year: Some(common_year),
month_code: Some(leap.clone()),
day: 1,
..Default::default()
},
ov,
)
};
assert!(
matches!(fi(Overflow::Reject), Err(CalError::Range(_))),
"leap code rejects in common year"
);
let constrained = fi(Overflow::Constrain).expect("leap code constrains in common year");
assert_eq!(
code_of(constrained),
base,
"leap code constrains onto base month"
);
}
#[cfg(feature = "intl")]
#[test]
fn dangi_and_umalqura_use_dedicated_tables() {
let u = iso_to_fields("islamic-umalqura", iso(2016, 6, 6));
assert_eq!((u.year, u.month, u.day), (1437, 9, 1));
let c = iso_to_fields("islamic-civil", iso(2016, 6, 6));
assert_eq!((c.year, c.month, c.day), (1437, 8, 29));
assert_eq!(chinese_leap_month("chinese", 2017), 6);
assert_eq!(chinese_leap_month("dangi", 2017), 5);
for cal in ["islamic-umalqura", "dangi"] {
let d = iso(2023, 6, 15);
let f = iso_to_fields(cal, d);
let input = FieldsInput {
era: f.era.clone(),
era_year: f.era_year,
year: Some(f.year),
month: Some(f.month),
month_code: None,
day: f.day,
};
let back = fields_to_iso(cal, &input, Overflow::Reject)
.unwrap_or_else(|_| panic!("{cal} roundtrip failed"));
assert_eq!(back, d, "calendar {cal} round-trip");
}
}
#[cfg(feature = "intl")]
#[test]
fn leap_code_validity() {
let fi = |cal: &str, y: i64, code: &str, ov| {
fields_to_iso(
cal,
&FieldsInput {
year: Some(y),
month_code: Some(code.to_string()),
day: 1,
..Default::default()
},
ov,
)
};
assert!(fi("hebrew", 5784, "M05L", Overflow::Reject).is_ok());
assert!(fi("hebrew", 5783, "M05L", Overflow::Reject).is_err());
assert!(fi("hebrew", 5783, "M05L", Overflow::Constrain).is_ok());
assert!(fi("hebrew", 5784, "M02L", Overflow::Constrain).is_err());
assert!(fi("hebrew", 5779, "M13", Overflow::Constrain).is_err());
assert!(fi("chinese", 2001, "M12L", Overflow::Constrain).is_ok());
assert!(fi("chinese", 2001, "M01L", Overflow::Constrain).is_ok());
assert!(fi("chinese", 2001, "M12L", Overflow::Reject).is_err());
assert!(fi("chinese", 2001, "M13", Overflow::Constrain).is_err());
assert!(fi("chinese", 2001, "M13L", Overflow::Constrain).is_err());
}
#[cfg(feature = "intl")]
#[test]
fn arith_islamic_constrain() {
let mut found = None;
for off in 0..40 {
let d = jdn_to_iso(iso_to_jdn(iso(2000, 1, 1)) + off);
let f = iso_to_fields("islamic-civil", d);
if f.day == 30 {
found = Some((d, f));
break;
}
}
let (d, f) = found.expect("a day-30 islamic-civil date exists in range");
let next = calendar_date_add("islamic-civil", d, 0, 1, 0, 0, Overflow::Constrain).unwrap();
let nf = iso_to_fields("islamic-civil", next);
let dim = days_in_month("islamic-civil", next);
assert!(nf.day <= dim, "islamic constrain: day {} <= {dim}", nf.day);
assert!(nf.day <= f.day, "islamic constrain never grows the day");
if dim < 30 {
let rejected = calendar_date_add("islamic-civil", d, 0, 1, 0, 0, Overflow::Reject);
assert!(
matches!(rejected, Err(CalError::Range(_))),
"islamic reject overflows"
);
}
}
#[test]
fn coptic_epoch() {
let f = iso_to_fields("coptic", iso(284, 8, 29));
assert_eq!(f.year, 1);
assert_eq!(f.month, 1);
assert_eq!(f.day, 1);
}
#[test]
fn greg_jdn_roundtrip_bc() {
for &j in &[
-300000i64, -284654, -50000, -32045, -32044, 0, 2_451_545, 3_000_000,
] {
let (y, m, d) = jdn_to_greg(j);
assert_eq!(greg_to_jdn(y, m, d), j, "greg roundtrip at jdn {j}");
}
assert_eq!(jdn_to_greg(2_451_545), (2000, 1, 1));
}
fn from_era(cal: &str, era: &str, ey: i64, code: &str) -> Result<IsoDate, CalError> {
fields_to_iso(
cal,
&FieldsInput {
era: Some(era.to_string()),
era_year: Some(ey),
month_code: Some(code.to_string()),
day: 1,
..Default::default()
},
Overflow::Reject,
)
}
#[cfg(feature = "intl")]
#[test]
fn non_positive_single_era_year_roundtrips() {
for (cal, era) in [
("ethioaa", "aa"),
("coptic", "am"),
("buddhist", "be"),
("hebrew", "am"),
("indian", "shaka"),
("persian", "ap"),
] {
for ey in [-1i64, 0, 1] {
let d = from_era(cal, era, ey, "M01").expect("resolves");
let f = iso_to_fields(cal, d);
assert_eq!(f.era.as_deref(), Some(era), "{cal} era");
assert_eq!(f.era_year, Some(ey), "{cal} eraYear {ey} round-trips");
assert_eq!(f.year, ey, "{cal} year == eraYear for {ey}");
}
}
}
#[cfg(feature = "intl")]
#[test]
fn persian_year_zero_exists() {
let jm1 = iso_to_jdn(from_era("persian", "ap", -1, "M01").unwrap());
let j0 = iso_to_jdn(from_era("persian", "ap", 0, "M01").unwrap());
let j1 = iso_to_jdn(from_era("persian", "ap", 1, "M01").unwrap());
assert!(jm1 < j0 && j0 < j1, "persian years -1 < 0 < 1 are ordered");
assert!((365..=366).contains(&(j1 - j0)), "persian year 0 length");
assert!((365..=366).contains(&(j0 - jm1)), "persian year -1 length");
}
#[cfg(feature = "intl")]
#[test]
fn japanese_meiji_era_label_boundary() {
let f = iso_to_fields("japanese", iso(1868, 10, 23));
assert_eq!(f.era.as_deref(), Some("ce"));
assert_eq!(f.era_year, Some(1868));
let f = iso_to_fields("japanese", iso(1872, 12, 31));
assert_eq!(f.era.as_deref(), Some("ce"));
let f = iso_to_fields("japanese", iso(1873, 1, 1));
assert_eq!(f.era.as_deref(), Some("meiji"));
assert_eq!(f.era_year, Some(6));
let f = iso_to_fields("japanese", iso(2019, 5, 1));
assert_eq!(f.era.as_deref(), Some("reiwa"));
assert_eq!(f.era_year, Some(1));
}
#[test]
fn era_erayear_pairing_and_validity() {
let only_era = fields_to_iso(
"gregory",
&FieldsInput {
era: Some("ce".to_string()),
year: Some(2000),
month_code: Some("M01".to_string()),
day: 1,
..Default::default()
},
Overflow::Reject,
);
assert!(matches!(only_era, Err(CalError::MissingFields(_))));
let only_erayear = fields_to_iso(
"gregory",
&FieldsInput {
era_year: Some(1),
month_code: Some("M01".to_string()),
day: 1,
..Default::default()
},
Overflow::Reject,
);
assert!(matches!(only_erayear, Err(CalError::MissingFields(_))));
let bad_era = fields_to_iso(
"buddhist",
&FieldsInput {
era: Some("xyz".to_string()),
era_year: Some(2025),
year: Some(2025),
month_code: Some("M01".to_string()),
day: 1,
..Default::default()
},
Overflow::Reject,
);
assert!(matches!(bad_era, Err(CalError::Range(_))));
assert!(
fields_to_iso(
"iso8601",
&FieldsInput {
era: Some("xyz".to_string()),
era_year: Some(1),
year: Some(1970),
month_code: Some("M01".to_string()),
day: 1,
..Default::default()
},
Overflow::Reject,
)
.is_ok()
);
assert!(from_era("gregory", "ad", 2024, "M01").is_ok());
}
}