Skip to content

Commit

Permalink
Add YearAmbiguity (#5790)
Browse files Browse the repository at this point in the history
#4478

The input should declare how ambiguous the year is. There should not be
any calendar-specific logic in `icu_datetime`.
  • Loading branch information
robertbastian authored Nov 7, 2024
1 parent 98361ac commit f426f3e
Show file tree
Hide file tree
Showing 15 changed files with 70 additions and 68 deletions.
1 change: 1 addition & 0 deletions components/calendar/src/buddhist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ fn iso_year_as_buddhist(year: i32) -> types::YearInfo {
standard_era: tinystr!(16, "buddhist").into(),
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "BE")),
era_year: buddhist_year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
}
Expand Down
2 changes: 2 additions & 0 deletions components/calendar/src/coptic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ fn year_as_coptic(year: i32) -> types::YearInfo {
standard_era: tinystr!(16, "coptic").into(),
formatting_era: types::FormattingEra::Index(1, tinystr!(16, "AD")),
era_year: year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
} else {
Expand All @@ -322,6 +323,7 @@ fn year_as_coptic(year: i32) -> types::YearInfo {
standard_era: tinystr!(16, "coptic-inverse").into(),
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "BD")),
era_year: 1 - year,
ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
},
)
}
Expand Down
3 changes: 3 additions & 0 deletions components/calendar/src/ethiopian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ impl Ethiopian {
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 {
Expand All @@ -313,6 +314,7 @@ impl Ethiopian {
standard_era: tinystr!(16, "ethiopic").into(),
formatting_era: types::FormattingEra::Index(2, tinystr!(16, "Incarnation")),
era_year: year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
} else {
Expand All @@ -322,6 +324,7 @@ impl Ethiopian {
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,
},
)
}
Expand Down
7 changes: 7 additions & 0 deletions components/calendar/src/gregorian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ fn year_as_gregorian(year: i32) -> types::YearInfo {
standard_era: tinystr!(16, "gregory").into(),
formatting_era: types::FormattingEra::Index(1, tinystr!(16, "CE")),
era_year: year,
ambiguity: match year {
..=999 => types::YearAmbiguity::EraAndCenturyRequired,
1000..=1949 => types::YearAmbiguity::CenturyRequired,
1950..=2049 => types::YearAmbiguity::Unambiguous,
2050.. => types::YearAmbiguity::CenturyRequired,
},
},
)
} else {
Expand All @@ -247,6 +253,7 @@ fn year_as_gregorian(year: i32) -> types::YearInfo {
standard_era: tinystr!(16, "gregory-inverse").into(),
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "BCE")),
era_year: 1_i32.saturating_sub(year),
ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
},
)
}
Expand Down
1 change: 1 addition & 0 deletions components/calendar/src/hebrew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ impl Hebrew {
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "AM")),
standard_era: tinystr!(16, "hebrew").into(),
era_year: civil_year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
}
Expand Down
1 change: 1 addition & 0 deletions components/calendar/src/indian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ fn year_as_saka(year: i32) -> types::YearInfo {
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "Saka")),
standard_era: tinystr!(16, "saka").into(),
era_year: year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
}
Expand Down
1 change: 1 addition & 0 deletions components/calendar/src/islamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fn year_as_islamic(standard_era: tinystr::TinyStr16, year: i32) -> types::YearIn
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "AH")),
standard_era: standard_era.into(),
era_year: year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
}
Expand Down
1 change: 1 addition & 0 deletions components/calendar/src/iso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ impl Iso {
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "")),
standard_era: tinystr!(16, "default").into(),
era_year: year,
ambiguity: types::YearAmbiguity::Unambiguous,
},
)
}
Expand Down
1 change: 1 addition & 0 deletions components/calendar/src/japanese.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ impl Calendar for Japanese {
formatting_era: types::FormattingEra::Code(date.era.into()),
standard_era: date.era.into(),
era_year: date.adjusted_year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
}
Expand Down
2 changes: 2 additions & 0 deletions components/calendar/src/julian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ fn year_as_julian(year: i32) -> types::YearInfo {
standard_era: tinystr!(16, "julian").into(),
formatting_era: types::FormattingEra::Index(1, tinystr!(16, "AD")),
era_year: year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
} else {
Expand All @@ -237,6 +238,7 @@ fn year_as_julian(year: i32) -> types::YearInfo {
standard_era: tinystr!(16, "julian-inverse").into(),
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "BC")),
era_year: 1_i32.saturating_sub(year),
ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
},
)
}
Expand Down
1 change: 1 addition & 0 deletions components/calendar/src/persian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ impl Persian {
standard_era: tinystr!(16, "persian").into(),
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "AH")),
era_year: year,
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
}
Expand Down
2 changes: 2 additions & 0 deletions components/calendar/src/roc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ pub(crate) fn year_as_roc(year: i64) -> types::YearInfo {
standard_era: tinystr!(16, "roc").into(),
formatting_era: types::FormattingEra::Index(1, tinystr!(16, "ROC")),
era_year: year_i32.saturating_sub(ROC_ERA_OFFSET),
ambiguity: types::YearAmbiguity::CenturyRequired,
},
)
} else {
Expand All @@ -284,6 +285,7 @@ pub(crate) fn year_as_roc(year: i64) -> types::YearInfo {
standard_era: tinystr!(16, "roc-inverse").into(),
formatting_era: types::FormattingEra::Index(0, tinystr!(16, "B. ROC")),
era_year: (ROC_ERA_OFFSET + 1).saturating_sub(year_i32),
ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
},
)
}
Expand Down
27 changes: 27 additions & 0 deletions components/calendar/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ impl YearInfo {
}
}

/// Get the year ambiguity.
pub fn year_ambiguity(self) -> YearAmbiguity {
match self.kind {
YearKind::Cyclic(_) => YearAmbiguity::EraRequired,
YearKind::Era(e) => e.ambiguity,
}
}

/// Get *some* year number that can be displayed
///
/// Gets the eraYear for era dates, otherwise falls back to Extended Year
Expand Down Expand Up @@ -123,6 +131,23 @@ impl YearInfo {
}
}

/// Defines whether the era or century is required to interpret the year.
///
/// For example 2024 AD can be formatted as `2024`, or even `24`, but 1931 AD
/// should not be formatted as `31`, and 2024 BC should not be formatted as `2024`.
#[derive(Copy, Clone, Debug, PartialEq)]
#[allow(clippy::exhaustive_enums)] // logically complete
pub enum YearAmbiguity {
/// The year is unambiguous without a century or era.
Unambiguous,
/// The century is required, the era may be included.
CenturyRequired,
/// The era is required, the century may be included.
EraRequired,
/// The century and era are required.
EraAndCenturyRequired,
}

/// Information about the era as usable for formatting
///
/// This is optimized for storing datetime formatting data.
Expand Down Expand Up @@ -172,6 +197,8 @@ pub struct EraYear {
pub standard_era: Era,
/// The numeric year in that era
pub era_year: i32,
/// The ambiguity when formatting this year
pub ambiguity: YearAmbiguity,
}

/// Year information for a year that is specified as a cyclic year
Expand Down
86 changes: 18 additions & 68 deletions components/datetime/src/raw/neo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::provider::pattern::{
GenericPatternItem, PatternItem,
};
use crate::provider::{neo::*, ErasedPackedPatterns, PackedSkeletonVariant};
use icu_calendar::types::YearAmbiguity;
use icu_provider::prelude::*;
use marker_attrs::GlueType;
use zerovec::ule::AsULE;
Expand Down Expand Up @@ -194,7 +195,23 @@ impl DatePatternSelectionData {
match self {
DatePatternSelectionData::SkeletonDate { options, payload } => {
let year_style = options.year_style.unwrap_or(YearStyle::Auto);
let variant = input.resolve_year_style(year_style);
let variant = match (
year_style,
input
.year
.map(|y| y.year_ambiguity())
.unwrap_or(YearAmbiguity::EraAndCenturyRequired),
) {
(YearStyle::Always, _) | (_, YearAmbiguity::EraAndCenturyRequired) => {
PackedSkeletonVariant::Variant1
}
(YearStyle::Full, _) | (_, YearAmbiguity::CenturyRequired) => {
PackedSkeletonVariant::Variant0
}
(YearStyle::Auto, YearAmbiguity::Unambiguous | YearAmbiguity::EraRequired) => {
PackedSkeletonVariant::Standard
}
};
DatePatternDataBorrowed::Resolved(
payload.get().get(options.length, variant),
options.alignment,
Expand All @@ -205,73 +222,6 @@ impl DatePatternSelectionData {
}

impl ExtractedInput {
fn resolve_year_style(&self, year_style: YearStyle) -> PackedSkeletonVariant {
use icu_calendar::AnyCalendarKind;
enum YearDistance {
/// A nearby year that could be rendered with partial-precision format.
Near,
/// A year with implied era but for which partial-precision should not be used.
Medium,
/// A year for which the era should always be displayed.
Distant,
}

if matches!(year_style, YearStyle::Always) {
return PackedSkeletonVariant::Variant1;
}
let year_distance = match self.any_calendar_kind {
// Unknown calendar: always display the era
None => YearDistance::Distant,
// TODO(#4478): This is extremely oversimplistic and it should be data-driven.
Some(AnyCalendarKind::Buddhist)
| Some(AnyCalendarKind::Coptic)
| Some(AnyCalendarKind::Ethiopian)
| Some(AnyCalendarKind::EthiopianAmeteAlem)
| Some(AnyCalendarKind::Hebrew)
| Some(AnyCalendarKind::Indian)
| Some(AnyCalendarKind::IslamicCivil)
| Some(AnyCalendarKind::IslamicObservational)
| Some(AnyCalendarKind::IslamicTabular)
| Some(AnyCalendarKind::IslamicUmmAlQura)
| Some(AnyCalendarKind::Japanese)
| Some(AnyCalendarKind::JapaneseExtended)
| Some(AnyCalendarKind::Persian)
| Some(AnyCalendarKind::Roc) => YearDistance::Medium,
Some(AnyCalendarKind::Chinese)
| Some(AnyCalendarKind::Dangi)
| Some(AnyCalendarKind::Iso) => YearDistance::Near,
Some(AnyCalendarKind::Gregorian) => match self.year {
None => YearDistance::Distant,
Some(year) if year.era_year_or_extended() < 1000 => YearDistance::Distant,
Some(year)
if !matches!(
year.formatting_era(),
Some(icu_calendar::types::FormattingEra::Index(1, _fallback))
) =>
{
YearDistance::Distant
}
Some(year)
if year.era_year_or_extended() < 1950
|| year.era_year_or_extended() >= 2050 =>
{
YearDistance::Medium
}
Some(_) => YearDistance::Near,
},
Some(_) => {
debug_assert!(false, "unknown calendar during year style resolution");
YearDistance::Distant
}
};

match (year_style, year_distance) {
(YearStyle::Always, _) | (_, YearDistance::Distant) => PackedSkeletonVariant::Variant1,
(YearStyle::Full, _) | (_, YearDistance::Medium) => PackedSkeletonVariant::Variant0,
(YearStyle::Auto, YearDistance::Near) => PackedSkeletonVariant::Standard,
}
}

fn resolve_time_precision(
&self,
time_precision: TimePrecision,
Expand Down
2 changes: 2 additions & 0 deletions ffi/capi/tests/missing_apis.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# Please check in with @Manishearth, @robertbastian, or @sffc if you have questions


icu::calendar::types::YearAmbiguity#Enum
icu::calendar::types::YearInfo::year_ambiguity#FnInStruct
icu::datetime::DateTimeFormatter#Struct
icu::datetime::DateTimeFormatter::convert_and_format#FnInStruct
icu::datetime::DateTimeFormatter::strict_format#FnInStruct
Expand Down

0 comments on commit f426f3e

Please sign in to comment.