Skip to content

Commit

Permalink
Audit era names, fix bugs (#6248)
Browse files Browse the repository at this point in the history
The bug fix is that Japanese's gregorian eras were mixed up in datetime
  • Loading branch information
robertbastian authored Mar 7, 2025
1 parent 54f680a commit 1fc4f03
Show file tree
Hide file tree
Showing 137 changed files with 1,868 additions and 2,137 deletions.
28 changes: 13 additions & 15 deletions components/calendar/src/any_calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ define_preferences!(
///
/// // construct from era code, year, month code, day, and a calendar
/// // This is March 28, 15 Heisei
/// let manual_date = Date::try_new_from_codes(Some(Era(tinystr!(16, "heisei"))), 15, MonthCode(tinystr!(4, "M03")), 28, calendar.clone())
/// let manual_date = Date::try_new_from_codes(Some("heisei"), 15, MonthCode(tinystr!(4, "M03")), 28, calendar.clone())
/// .expect("Failed to construct Date manually");
///
///
Expand All @@ -65,7 +65,7 @@ define_preferences!(
///
/// // Construct a date in the appropriate typed calendar and convert
/// let japanese_calendar = Japanese::new();
/// let japanese_date = Date::try_new_japanese_with_calendar(Era(tinystr!(16, "heisei")), 15, 3, 28,
/// let japanese_date = Date::try_new_japanese_with_calendar("heisei", 15, 3, 28,
/// japanese_calendar).unwrap();
/// // This is a Date<AnyCalendar>
/// let any_japanese_date = japanese_date.to_any();
Expand Down Expand Up @@ -197,7 +197,7 @@ impl Calendar for AnyCalendar {
type DateInner = AnyDateInner;
fn date_from_codes(
&self,
era: Option<types::Era>,
era: Option<&str>,
year: i32,
month_code: types::MonthCode,
day: u8,
Expand Down Expand Up @@ -1647,7 +1647,6 @@ mod tests {
month_code: &str,
day: u8,
) {
let era = types::Era(era.parse().expect("era must parse"));
let month = types::MonthCode(month_code.parse().expect("month code must parse"));

let date =
Expand Down Expand Up @@ -1687,7 +1686,6 @@ mod tests {
day: u8,
error: DateError,
) {
let era = types::Era(era.parse().expect("era must parse"));
let month = types::MonthCode(month_code.parse().expect("month code must parse"));

let date = Date::try_new_from_codes(Some(era), year, month, day, calendar);
Expand Down Expand Up @@ -1749,22 +1747,22 @@ mod tests {
DateError::UnknownMonthCode(MonthCode(tinystr!(4, "M13"))),
);

single_test_roundtrip(coptic, "ad", 100, "M03", 1);
single_test_roundtrip(coptic, "ad", 2000, "M03", 1);
single_test_roundtrip(coptic, "coptic", 100, "M03", 1);
single_test_roundtrip(coptic, "coptic", 2000, "M03", 1);
// fails ISO roundtrip
// single_test_roundtrip(coptic, "bd", 100, "M03", 1);
single_test_roundtrip(coptic, "ad", 100, "M13", 1);
single_test_roundtrip(coptic, "coptic", 100, "M13", 1);
single_test_error(
coptic,
"ad",
"coptic",
100,
"M14",
1,
DateError::UnknownMonthCode(MonthCode(tinystr!(4, "M14"))),
);
single_test_error(
coptic,
"ad",
"coptic",
0,
"M03",
1,
Expand All @@ -1777,7 +1775,7 @@ mod tests {
);
single_test_error(
coptic,
"bd",
"coptic-inverse",
0,
"M03",
1,
Expand Down Expand Up @@ -2001,12 +1999,12 @@ mod tests {
DateError::UnknownMonthCode(MonthCode(tinystr!(4, "M13"))),
);

single_test_roundtrip(persian, "ah", 477, "M03", 1);
single_test_roundtrip(persian, "ah", 2083, "M07", 21);
single_test_roundtrip(persian, "ah", 1600, "M12", 20);
single_test_roundtrip(persian, "ap", 477, "M03", 1);
single_test_roundtrip(persian, "ap", 2083, "M07", 21);
single_test_roundtrip(persian, "ap", 1600, "M12", 20);
single_test_error(
persian,
"ah",
"ap",
100,
"M9",
1,
Expand Down
11 changes: 5 additions & 6 deletions components/calendar/src/cal/buddhist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const BUDDHIST_ERA_OFFSET: i32 = 543;
///
/// # Era codes
///
/// This calendar supports one era, `"be"`, with 1 B.E. being 543 BCE.
/// This calendar uses a single era code, `buddhist` (alias `be`), with 1 B.E. being 543 BCE.
///
/// # Month codes
///
Expand All @@ -54,15 +54,14 @@ impl Calendar for Buddhist {

fn date_from_codes(
&self,
era: Option<types::Era>,
era: Option<&str>,
year: i32,
month_code: types::MonthCode,
day: u8,
) -> Result<Self::DateInner, DateError> {
if let Some(era) = era {
if era.0 != tinystr!(16, "be") {
return Err(DateError::UnknownEra(era));
}
match era {
Some("buddhist" | "be") | None => {}
_ => return Err(DateError::UnknownEra),
}
let year = year - BUDDHIST_ERA_OFFSET;

Expand Down
21 changes: 9 additions & 12 deletions components/calendar/src/cal/chinese.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ use crate::{types, Calendar, Date, DateDuration, DateDurationUnit};
use core::cmp::Ordering;
use core::num::NonZeroU8;
use icu_provider::prelude::*;
use tinystr::tinystr;

/// The [Chinese Calendar](https://en.wikipedia.org/wiki/Chinese_calendar)
///
Expand All @@ -56,6 +55,8 @@ use tinystr::tinystr;
///
/// # Year and Era codes
///
/// This Calendar uses a single era code, `chinese`.
///
/// Unlike the Gregorian calendar, the Chinese calendar does not traditionally count years in an infinitely
/// increasing sequence. Instead, 10 "celestial stems" and 12 "terrestrial branches" are combined to form a
/// cycle of year names which repeats every 60 years. However, for the purposes of calendar calculations and
Expand Down Expand Up @@ -167,25 +168,20 @@ impl Calendar for Chinese {
// Construct a date from era/month codes and fields
fn date_from_codes(
&self,
era: Option<types::Era>,
era: Option<&str>,
year: i32,
month_code: types::MonthCode,
day: u8,
) -> Result<Self::DateInner, DateError> {
let year_info = self.get_precomputed_data().load_or_compute_info(year);

let month = if let Some(ordinal) =
chinese_based_ordinal_lunar_month_from_code(month_code, year_info)
{
ordinal
} else {
let Some(month) = chinese_based_ordinal_lunar_month_from_code(month_code, year_info) else {
return Err(DateError::UnknownMonthCode(month_code));
};

if let Some(era) = era {
if era.0 != tinystr!(16, "chinese") {
return Err(DateError::UnknownEra(era));
}
match era {
Some("chinese") | None => {}
_ => return Err(DateError::UnknownEra),
}

Inner::new_from_ordinals(year, month, day, year_info)
Expand Down Expand Up @@ -361,10 +357,11 @@ impl Chinese {

#[cfg(test)]
mod test {

use super::*;
use crate::types::MonthCode;
use calendrical_calculations::{iso::fixed_from_iso, rata_die::RataDie};
use tinystr::tinystr;

/// Run a test twice, with two calendars
fn do_twice(
chinese_calculating: &Chinese,
Expand Down
36 changes: 7 additions & 29 deletions components/calendar/src/cal/coptic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use crate::cal::iso::Iso;
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
use crate::error::DateError;
use crate::error::{year_check, DateError};
use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
use calendrical_calculations::helpers::I32CastError;
use calendrical_calculations::rata_die::RataDie;
Expand All @@ -35,7 +35,7 @@ use tinystr::tinystr;
///
/// # Era codes
///
/// This calendar supports two era codes: `"bd"`, and `"ad"`, corresponding to the Before Diocletian and After Diocletian/Anno Martyrum
/// This calendar uses two era codes: `coptic-inverse`, and `coptic`, corresponding to the Before Diocletian and After Diocletian/Anno Martyrum
/// eras. 1 A.M. is equivalent to 284 C.E.
///
/// # Month codes
Expand Down Expand Up @@ -96,37 +96,15 @@ impl Calendar for Coptic {
type DateInner = CopticDateInner;
fn date_from_codes(
&self,
era: Option<types::Era>,
era: Option<&str>,
year: i32,
month_code: types::MonthCode,
day: u8,
) -> Result<Self::DateInner, DateError> {
let year = if let Some(era) = era {
if era.0 == tinystr!(16, "ad") || era.0 == tinystr!(16, "coptic") {
if year <= 0 {
return Err(DateError::Range {
field: "year",
value: year,
min: 1,
max: i32::MAX,
});
}
year
} else if era.0 == tinystr!(16, "bd") || era.0 == tinystr!(16, "coptic-inverse") {
if year <= 0 {
return Err(DateError::Range {
field: "year",
value: year,
min: 1,
max: i32::MAX,
});
}
1 - year
} else {
return Err(DateError::UnknownEra(era));
}
} else {
year
let year = match era {
Some("coptic") | None => year_check(year, 1..)?,
Some("coptic-inverse") => 1 - year_check(year, 1..)?,
Some(_) => return Err(DateError::UnknownEra),
};

ArithmeticDate::new_from_codes(self, year, month_code, day).map(CopticDateInner)
Expand Down
19 changes: 6 additions & 13 deletions components/calendar/src/cal/dangi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ use crate::{cal::chinese_based::ChineseBasedDateInner, types, Calendar, Date, Is
use core::cmp::Ordering;
use core::num::NonZeroU8;
use icu_provider::prelude::*;
use tinystr::tinystr;

/// The [Traditional Korean (Dangi) Calendar](https://en.wikipedia.org/wiki/Korean_calendar)
///
Expand Down Expand Up @@ -66,7 +65,7 @@ use tinystr::tinystr;
/// ```
/// # Era codes
///
/// This Calendar supports a single era code "dangi" based on the year -2332 ISO (2333 BCE) as year 1. Typically
/// This Calendar uses a single era code `dangi` based on the year -2332 ISO (2333 BCE) as year 1. Typically
/// years will be formatted using cyclic years and the related ISO year.
///
/// # Month codes
Expand Down Expand Up @@ -158,25 +157,19 @@ impl Calendar for Dangi {

fn date_from_codes(
&self,
era: Option<crate::types::Era>,
era: Option<&str>,
year: i32,
month_code: crate::types::MonthCode,
day: u8,
) -> Result<Self::DateInner, DateError> {
let year_info = self.get_precomputed_data().load_or_compute_info(year);

let month = if let Some(ordinal) =
chinese_based_ordinal_lunar_month_from_code(month_code, year_info)
{
ordinal
} else {
let Some(month) = chinese_based_ordinal_lunar_month_from_code(month_code, year_info) else {
return Err(DateError::UnknownMonthCode(month_code));
};

if let Some(era) = era {
if era.0 != tinystr!(16, "dangi") {
return Err(DateError::UnknownEra(era));
}
match era {
Some("dangi") | None => {}
_ => return Err(DateError::UnknownEra),
}

Inner::new_from_ordinals(year, month, day, year_info)
Expand Down
48 changes: 15 additions & 33 deletions components/calendar/src/cal/ethiopian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
use crate::cal::iso::Iso;
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
use crate::error::DateError;
use crate::types::Era;
use crate::error::{year_check, DateError};
use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
use calendrical_calculations::helpers::I32CastError;
use calendrical_calculations::rata_die::RataDie;
Expand Down Expand Up @@ -53,8 +52,9 @@ pub enum EthiopianEraStyle {
///
/// # Era codes
///
/// This calendar always uses the `"mundi` era, where 1 Anno Mundi is 5493 BCE. Dates before that use negative year numbers.
/// In the Amete Mihret scheme it uses the additional `"incar"` era, 1 Incarnation is 9 CE.
/// This calendar always uses the `ethioaa` era (aliases `mundi`, `ethiopic-amete-alem`), where 1 Anno Mundi is 5493 BCE.
/// Dates before that use negative year numbers.
/// In the Amete Mihret scheme it uses the additional `ethiopic` era (alias `incar`), 1 Incarnation is 9 CE.
///
/// # Month codes
///
Expand Down Expand Up @@ -114,42 +114,24 @@ impl Calendar for Ethiopian {
type DateInner = EthiopianDateInner;
fn date_from_codes(
&self,
era: Option<types::Era>,
era: Option<&str>,
year: i32,
month_code: types::MonthCode,
day: u8,
) -> Result<Self::DateInner, DateError> {
const INCAR: Era = Era(tinystr!(16, "incar"));
const ETHIOPIC: Era = Era(tinystr!(16, "ethiopic"));
const MUNDI: Era = Era(tinystr!(16, "mundi"));
const ETHIOAA: Era = Era(tinystr!(16, "ethioaa"));

let year = match (self.era_style(), era) {
(EthiopianEraStyle::AmeteMihret, Some(INCAR | ETHIOPIC) | None) => {
if year <= 0 {
return Err(DateError::Range {
field: "year",
value: year,
min: 1,
max: i32::MAX,
});
}
year + INCARNATION_OFFSET
(EthiopianEraStyle::AmeteMihret, Some("incar" | "ethiopic") | None) => {
year_check(year, 1..)? + INCARNATION_OFFSET
}
(EthiopianEraStyle::AmeteMihret, Some(MUNDI | ETHIOAA)) => {
if year > INCARNATION_OFFSET {
return Err(DateError::Range {
field: "year",
value: year,
min: i32::MIN,
max: INCARNATION_OFFSET,
});
}
year
(EthiopianEraStyle::AmeteMihret, Some("mundi" | "ethioaa" | "ethiopic-amete-alem")) => {
year_check(year, ..=INCARNATION_OFFSET)?
}
(EthiopianEraStyle::AmeteAlem, Some(MUNDI | ETHIOAA) | None) => year,
(_, Some(era)) => {
return Err(DateError::UnknownEra(era));
(
EthiopianEraStyle::AmeteAlem,
Some("mundi" | "ethioaa" | "ethiopic-amete-alem") | None,
) => year,
(_, Some(_)) => {
return Err(DateError::UnknownEra);
}
};
ArithmeticDate::new_from_codes(self, year, month_code, day).map(EthiopianDateInner)
Expand Down
Loading

0 comments on commit 1fc4f03

Please sign in to comment.