Skip to content

Commit

Permalink
Align Ethiopian eras with CLDR (#6246)
Browse files Browse the repository at this point in the history
We currently use a `pre-incar` era, which is not defined in CLDR.
Instead, we should use the `mundi` era before the `incar` era.
  • Loading branch information
robertbastian authored Mar 6, 2025
1 parent 788852e commit 54f680a
Show file tree
Hide file tree
Showing 62 changed files with 183 additions and 226 deletions.
12 changes: 6 additions & 6 deletions components/calendar/src/any_calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1793,7 +1793,7 @@ mod tests {
single_test_roundtrip(ethiopian, "incar", 2000, "M03", 1);
single_test_roundtrip(ethiopian, "incar", 2000, "M13", 1);
// Fails ISO roundtrip due to https://github.com/unicode-org/icu4x/issues/2254
// single_test_roundtrip(ethiopian, "pre-incar", 100, "M03", 1);
// single_test_roundtrip(ethiopian, "mundi", 5400, "M03", 1);
single_test_error(
ethiopian,
"incar",
Expand All @@ -1809,15 +1809,15 @@ mod tests {
);
single_test_error(
ethiopian,
"pre-incar",
0,
"mundi",
5600,
"M03",
1,
DateError::Range {
field: "year",
value: 0,
min: 1,
max: i32::MAX,
value: 5600,
min: i32::MIN,
max: 5500,
},
);
single_test_error(
Expand Down
121 changes: 68 additions & 53 deletions components/calendar/src/cal/ethiopian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@
use crate::cal::iso::Iso;
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
use crate::error::DateError;
use crate::types::Era;
use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
use calendrical_calculations::helpers::I32CastError;
use calendrical_calculations::rata_die::RataDie;
use tinystr::tinystr;

/// The number of years the Amete Alem epoch precedes the Amete Mihret epoch
const AMETE_ALEM_OFFSET: i32 = 5500;
const INCARNATION_OFFSET: i32 = 5500;

/// Which era style the ethiopian calendar uses
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub enum EthiopianEraStyle {
/// Use an era scheme of pre- and post- Incarnation eras,
/// anchored at the date of the Incarnation of Jesus in this calendar
/// Use the Anno Mundi era, anchored at the date of Creation, followed by the
/// Incarnation era, anchored at the date of the Incarnation of Jesus
AmeteMihret,
/// Use an era scheme of the Anno Mundi era, anchored at the date of Creation
/// in this calendar
/// Use the single Anno Mundi era, anchored at the date of Creation
AmeteAlem,
}

Expand All @@ -53,9 +53,8 @@ pub enum EthiopianEraStyle {
///
/// # Era codes
///
/// This calendar supports three era codes, based on what mode it is in. In the Amete Mihret scheme it has
/// the `"incar"` and `"pre-incar"` eras, 1 Incarnation is 9 CE. In the Amete Alem scheme, it instead has a single era,
/// `"mundi`, where 1 Anno Mundi is 5493 BCE. Dates before that use negative year numbers.
/// 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.
///
/// # Month codes
///
Expand Down Expand Up @@ -120,8 +119,13 @@ impl Calendar for Ethiopian {
month_code: types::MonthCode,
day: u8,
) -> Result<Self::DateInner, DateError> {
let year = if let Some(era) = era {
if era.0 == tinystr!(16, "incar") || era.0 == tinystr!(16, "ethiopic") {
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",
Expand All @@ -130,26 +134,23 @@ impl Calendar for Ethiopian {
max: i32::MAX,
});
}
year
} else if era.0 == tinystr!(16, "pre-incar")
|| era.0 == tinystr!(16, "ethiopic-inverse")
{
if year <= 0 {
year + INCARNATION_OFFSET
}
(EthiopianEraStyle::AmeteMihret, Some(MUNDI | ETHIOAA)) => {
if year > INCARNATION_OFFSET {
return Err(DateError::Range {
field: "year",
value: year,
min: 1,
max: i32::MAX,
min: i32::MIN,
max: INCARNATION_OFFSET,
});
}
1 - year
} else if era.0 == tinystr!(16, "mundi") || era.0 == tinystr!(16, "ethioaa") {
year - AMETE_ALEM_OFFSET
} else {
year
}
(EthiopianEraStyle::AmeteAlem, Some(MUNDI | ETHIOAA) | None) => year,
(_, Some(era)) => {
return Err(DateError::UnknownEra(era));
}
} else {
year
};
ArithmeticDate::new_from_codes(self, year, month_code, day).map(EthiopianDateInner)
}
Expand Down Expand Up @@ -252,7 +253,12 @@ impl Ethiopian {
}

fn fixed_from_ethiopian(date: ArithmeticDate<Ethiopian>) -> RataDie {
calendrical_calculations::ethiopian::fixed_from_ethiopian(date.year, date.month, date.day)
// calendrical calculations expects years in the Incarnation era
calendrical_calculations::ethiopian::fixed_from_ethiopian(
date.year - INCARNATION_OFFSET,
date.month,
date.day,
)
}

fn ethiopian_from_fixed(date: RataDie) -> EthiopianDateInner {
Expand All @@ -266,7 +272,12 @@ impl Ethiopian {
}
Ok(ymd) => ymd,
};
EthiopianDateInner(ArithmeticDate::new_unchecked(year, month, day))
// calendrical calculations returns years in the Incarnation era
EthiopianDateInner(ArithmeticDate::new_unchecked(
year + INCARNATION_OFFSET,
month,
day,
))
}

fn days_in_year_direct(year: i32) -> u16 {
Expand All @@ -277,34 +288,24 @@ impl Ethiopian {
}
}
fn year_as_ethiopian(year: i32, amete_alem: bool) -> types::YearInfo {
if amete_alem {
if amete_alem || year <= INCARNATION_OFFSET {
types::YearInfo::new(
year,
types::EraYear {
standard_era: tinystr!(16, "ethioaa").into(),
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "Anno Mundi")),
era_year: year + AMETE_ALEM_OFFSET,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
} else if year > 0 {
types::YearInfo::new(
year,
types::EraYear {
standard_era: tinystr!(16, "ethiopic").into(),
formatting_era: types::FormattingEra::Index(2, tinystr!(16, "Incarnation")),
era_year: year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
} else {
types::YearInfo::new(
year,
year - INCARNATION_OFFSET,
types::EraYear {
standard_era: tinystr!(16, "ethiopic-inverse").into(),
formatting_era: types::FormattingEra::Index(1, tinystr!(16, "Pre-Incarnation")),
era_year: 1 - year,
ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
standard_era: tinystr!(16, "ethiopic").into(),
formatting_era: types::FormattingEra::Index(1, tinystr!(16, "Incarnation")),
era_year: year - INCARNATION_OFFSET,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
}
Expand All @@ -314,10 +315,6 @@ impl Ethiopian {
impl Date<Ethiopian> {
/// Construct new Ethiopian Date.
///
/// For the Amete Mihret era style, negative years work with
/// year 0 as 1 pre-Incarnation, year -1 as 2 pre-Incarnation,
/// and so on.
///
/// ```rust
/// use icu::calendar::cal::EthiopianEraStyle;
/// use icu::calendar::Date;
Expand All @@ -337,7 +334,7 @@ impl Date<Ethiopian> {
day: u8,
) -> Result<Date<Ethiopian>, RangeError> {
if era_style == EthiopianEraStyle::AmeteAlem {
year -= AMETE_ALEM_OFFSET;
year -= INCARNATION_OFFSET;
}
ArithmeticDate::new_from_ordinals(year, month, day)
.map(EthiopianDateInner)
Expand All @@ -353,20 +350,38 @@ mod test {
fn test_leap_year() {
// 11th September 2023 in gregorian is 6/13/2015 in ethiopian
let iso_date = Date::try_new_iso(2023, 9, 11).unwrap();
let ethiopian_date = Ethiopian::new().date_from_iso(iso_date);
assert_eq!(ethiopian_date.0.year, 2015);
assert_eq!(ethiopian_date.0.month, 13);
assert_eq!(ethiopian_date.0.day, 6);
let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian::new());
assert_eq!(date_ethiopian.year().extended_year, 2015);
assert_eq!(date_ethiopian.month().ordinal, 13);
assert_eq!(date_ethiopian.day_of_month().0, 6);
}

#[test]
fn test_iso_to_ethiopian_conversion_and_back() {
let iso_date = Date::try_new_iso(1970, 1, 2).unwrap();
let date_ethiopian = Date::new_from_iso(iso_date, Ethiopian::new());

assert_eq!(date_ethiopian.inner.0.year, 1962);
assert_eq!(date_ethiopian.inner.0.month, 4);
assert_eq!(date_ethiopian.inner.0.day, 24);
assert_eq!(date_ethiopian.year().extended_year, 1962);
assert_eq!(date_ethiopian.month().ordinal, 4);
assert_eq!(date_ethiopian.day_of_month().0, 24);

assert_eq!(
date_ethiopian.to_iso(),
Date::try_new_iso(1970, 1, 2).unwrap()
);
}

#[test]
fn test_iso_to_ethiopian_aa_conversion_and_back() {
let iso_date = Date::try_new_iso(1970, 1, 2).unwrap();
let date_ethiopian = Date::new_from_iso(
iso_date,
Ethiopian::new_with_era_style(EthiopianEraStyle::AmeteAlem),
);

assert_eq!(date_ethiopian.year().extended_year, 7462);
assert_eq!(date_ethiopian.month().ordinal, 4);
assert_eq!(date_ethiopian.day_of_month().0, 24);

assert_eq!(
date_ethiopian.to_iso(),
Expand Down
2 changes: 1 addition & 1 deletion components/calendar/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ pub enum FormattingEra {
///
/// In this context, chronological ordering of eras is obtained by ordering by their start date (or in the case of
/// negative eras, their end date) first, and for eras sharing a date, put the negative one first. For example,
/// bce < ce, and mundi < pre-incar < incar for Ethiopian.
/// bce < ce, and mundi < incar for Ethiopian.
///
/// The TInyStr16 is a fallback string for the era when a display name is not available. It need not be an era code, it should
/// be something sensible (or empty).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@
"en-u-ca-buddhist": "2563 BE",
"en-u-ca-coptic": "1736 ERA1",
"fr-u-ca-coptic": "1736 ap. D.",
"en-u-ca-ethiopic" : "2012 ERA0",
"fr-u-ca-ethiopic" : "2012 av. Inc.",
"fr-u-ca-ethioaa" : "7512 ERA0"
"en-u-ca-ethiopic" : "2012 ERA1",
"fr-u-ca-ethiopic" : "2012 ap. Inc.",
"fr-u-ca-ethioaa" : "7512 av. Inc."
}
}
},
Expand Down
6 changes: 3 additions & 3 deletions components/datetime/tests/fixtures/tests/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
"fr-u-ca-persian": "mardi 1 bahman 1398 A. P., 08:25:07",
"en-u-ca-hebrew": "Tuesday, 24 Tevet 5780 AM, 08:25:07",
"fr-u-ca-hebrew": "mardi 24 téveth 5780 A. M., 08:25:07",
"en-u-ca-ethiopic": "Tuesday, Ter 12, 2012 ERA0, 08:25:07",
"fr-u-ca-ethiopic": "mardi 12 ter 2012 av. Inc., 08:25:07",
"fr-u-ca-ethioaa": "mardi 12 ter 7512 ERA0, 08:25:07",
"en-u-ca-ethiopic": "Tuesday, Ter 12, 2012 ERA1, 08:25:07",
"fr-u-ca-ethiopic": "mardi 12 ter 2012 ap. Inc., 08:25:07",
"fr-u-ca-ethioaa": "mardi 12 ter 7512 av. Inc., 08:25:07",
"en-u-ca-roc": "Tuesday, January 21, 109 Minguo, 08:25:07",
"fr-u-ca-roc": "mardi 21 janvier 109 RdC, 08:25:07",
"zh-u-ca-roc": "民国109年1月21日星期二 08:25:07"
Expand Down
Loading

0 comments on commit 54f680a

Please sign in to comment.