Skip to content

Commit

Permalink
Add NeoDateFormatter for AnyCalendar (#4567)
Browse files Browse the repository at this point in the history
  • Loading branch information
sffc authored Jan 31, 2024
1 parent b4a2caf commit 2a25844
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 44 deletions.
33 changes: 4 additions & 29 deletions components/datetime/src/any/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ use crate::provider::calendar::*;
use crate::{calendar, options::length, raw};
use crate::{input::DateInput, DateTimeError, FormattedDateTime};
use alloc::string::String;
use icu_calendar::any_calendar::{AnyCalendar, AnyCalendarKind};
use icu_calendar::any_calendar::AnyCalendar;
use icu_calendar::provider::{
ChineseCacheV1Marker, DangiCacheV1Marker, JapaneseErasV1Marker, JapaneseExtendedErasV1Marker,
WeekDataV1Marker,
};
use icu_calendar::Date;
use icu_decimal::provider::DecimalSymbolsV1Marker;
use icu_plurals::provider::OrdinalV1Marker;
use icu_provider::prelude::*;
Expand Down Expand Up @@ -108,6 +107,8 @@ impl DateFormatter {
/// "Sep 1, 2020"
/// );
/// ```
///
/// [`AnyCalendarKind`]: icu_calendar::AnyCalendarKind
#[inline(never)]
#[cfg(feature = "compiled_data")]
pub fn try_new_with_length(
Expand Down Expand Up @@ -233,7 +234,7 @@ impl DateFormatter {
where
T: DateInput<Calendar = AnyCalendar>,
{
if let Some(converted) = self.convert_if_necessary(value)? {
if let Some(converted) = calendar::convert_if_necessary(&self.1, value)? {
Ok(self.0.format(&converted))
} else {
Ok(self.0.format(value))
Expand All @@ -252,32 +253,6 @@ impl DateFormatter {
) -> Result<String, DateTimeError> {
Ok(self.format(value)?.write_to_string().into_owned())
}

/// Converts a date to the correct calendar if necessary
///
/// Returns `Err` if the date is not ISO or compatible with the current calendar, returns `Ok(None)`
/// if the date is compatible with the current calendar and doesn't need conversion
fn convert_if_necessary<'a>(
&'a self,
value: &impl DateInput<Calendar = AnyCalendar>,
) -> Result<Option<Date<icu_calendar::Ref<'a, AnyCalendar>>>, DateTimeError> {
let this_calendar = self.1.kind();
let date_calendar = value.any_calendar_kind();

if Some(this_calendar) != date_calendar {
if date_calendar != Some(AnyCalendarKind::Iso) {
return Err(DateTimeError::MismatchedAnyCalendar(
this_calendar,
date_calendar,
));
}
let date = value.to_iso().to_any();
let converted = self.1.convert_any_date(&date);
Ok(Some(converted))
} else {
Ok(None)
}
}
}

#[test]
Expand Down
149 changes: 149 additions & 0 deletions components/datetime/src/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::provider::calendar::*;
use icu_calendar::any_calendar::AnyCalendarKind;
use icu_calendar::chinese::Chinese;
use icu_calendar::roc::Roc;
use icu_calendar::AnyCalendar;
use icu_calendar::{
buddhist::Buddhist, coptic::Coptic, dangi::Dangi, ethiopian::Ethiopian, hebrew::Hebrew,
indian::Indian, islamic::IslamicCivil, islamic::IslamicObservational, islamic::IslamicTabular,
Expand Down Expand Up @@ -74,6 +75,54 @@ pub trait CldrCalendar: InternalCldrCalendar {
}
}

#[cfg(feature = "experimental")]
pub(crate) trait YearNamesV1Provider<M: DataMarker> {
fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError>;
}

#[cfg(feature = "experimental")]
impl<M, P> YearNamesV1Provider<M> for P
where
M: KeyedDataMarker<Yokeable = YearNamesV1<'static>>,
P: DataProvider<M> + ?Sized,
{
fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError> {
DataProvider::<M>::load(self, req)
}
}

#[cfg(feature = "experimental")]
pub(crate) trait MonthNamesV1Provider<M: DataMarker> {
fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError>;
}

#[cfg(feature = "experimental")]
impl<M, P> MonthNamesV1Provider<M> for P
where
M: KeyedDataMarker<Yokeable = MonthNamesV1<'static>>,
P: DataProvider<M> + ?Sized,
{
fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError> {
DataProvider::<M>::load(self, req)
}
}

#[cfg(feature = "experimental")]
pub(crate) trait DatePatternV1Provider<M: DataMarker> {
fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError>;
}

#[cfg(feature = "experimental")]
impl<M, P> DatePatternV1Provider<M> for P
where
M: KeyedDataMarker<Yokeable = DatePatternV1<'static>>,
P: DataProvider<M> + ?Sized,
{
fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError> {
DataProvider::<M>::load(self, req)
}
}

/// Check if the provided value is of the form `islamic-{subcal}`
fn is_islamic_subcal(value: &Value, subcal: TinyAsciiStr<8>) -> bool {
if let &[first, second] = value.as_tinystr_slice() {
Expand Down Expand Up @@ -588,3 +637,103 @@ where
};
Ok(payload)
}

#[cfg(feature = "experimental")]
pub(crate) struct AnyCalendarProvider<'a, P: ?Sized> {
pub(crate) provider: &'a P,
pub(crate) kind: AnyCalendarKind,
}

#[cfg(feature = "experimental")]
macro_rules! impl_load_any_calendar {
([$(($trait:ident, $erased:ident, $marker:ident)),+], [$($kind_cal:ident),+], [$($kind:ident => $cal:ident),+]) => {
impl_load_any_calendar!(@expand [$(($trait, $erased, $marker)),+], [$($kind_cal),+], [$($kind => $cal),+]);
};
(@expand [$(($trait:ident, $erased:ident, $marker:ident)),+], $tail1:tt, $tail2:tt) => {
$(impl_load_any_calendar!(@single_impl $trait, $erased, $marker, $tail1, $tail2);)+
};
(@single_impl $trait:ident, $erased:ident, $marker:ident, [$($kind_cal:ident),+], [$($kind:ident => $cal:ident),+]) => {
impl<P> $trait<$erased> for AnyCalendarProvider<'_, P>
where
P: ?Sized + $(DataProvider::<<$kind_cal as CldrCalendar>::$marker> +)+
{
fn load(
&self,
req: DataRequest,
) -> Result<DataResponse<$erased>, DataError> {
match self.kind {
$(
AnyCalendarKind::$kind_cal => DataProvider
::<<$kind_cal as CldrCalendar>::$marker>
::load(self.provider, req)
.map(DataResponse::cast),
)+
$(
AnyCalendarKind::$kind => DataProvider
::<<$cal as CldrCalendar>::$marker>
::load(self.provider, req)
.map(DataResponse::cast),
)+
_ => Err(
DataError::custom("Don't know how to load data for specified calendar")
.with_debug_context(&self.kind)),
}
}
}
};
}

#[cfg(feature = "experimental")]
impl_load_any_calendar!([
(DatePatternV1Provider, ErasedDatePatternV1Marker, DatePatternV1Marker),
(YearNamesV1Provider, ErasedYearNamesV1Marker, YearNamesV1Marker),
(MonthNamesV1Provider, ErasedMonthNamesV1Marker, MonthNamesV1Marker)
], [
Buddhist,
Chinese,
Coptic,
Dangi,
Ethiopian,
Gregorian,
Hebrew,
Indian,
IslamicCivil,
IslamicObservational,
IslamicTabular,
IslamicUmmAlQura,
Japanese,
JapaneseExtended,
Persian,
Roc
], [
EthiopianAmeteAlem => Ethiopian
]);

/// Converts a date to the correct calendar if necessary
///
/// Returns `Err` if the date is not ISO or compatible with the current calendar, returns `Ok(None)`
/// if the date is compatible with the current calendar and doesn't need conversion
pub(crate) fn convert_if_necessary<'a>(
any_calendar: &'a AnyCalendar,
value: &impl crate::input::DateInput<Calendar = AnyCalendar>,
) -> Result<
Option<icu_calendar::Date<icu_calendar::Ref<'a, AnyCalendar>>>,
crate::MismatchedCalendarError,
> {
let this_kind = any_calendar.kind();
let date_kind = value.any_calendar_kind();

if Some(this_kind) != date_kind {
if date_kind != Some(AnyCalendarKind::Iso) {
return Err(crate::MismatchedCalendarError {
this_kind,
date_kind,
});
}
let date = value.to_iso().to_any();
let converted = any_calendar.convert_any_date(&date);
Ok(Some(converted))
} else {
Ok(None)
}
}
18 changes: 18 additions & 0 deletions components/datetime/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ pub enum DateTimeError {
DuplicateField(Field),
}

/// An error from mixing calendar types in [`DateTimeFormatter`](crate::DateTimeFormatter)
#[derive(Display, Debug, Copy, Clone, PartialEq)]
#[displaydoc("DateTimeFormatter for {this_kind} calendar was given a {date_kind:?} calendar")]
#[non_exhaustive]
pub struct MismatchedCalendarError {
/// The calendar kind of the target object (formatter).
pub this_kind: AnyCalendarKind,
/// The calendar kind of the input object (date being formatted).
/// Can be `None` if the input calendar was not specified.
pub date_kind: Option<AnyCalendarKind>,
}

impl From<PatternError> for DateTimeError {
fn from(e: PatternError) -> Self {
DateTimeError::Pattern(e)
Expand Down Expand Up @@ -123,3 +135,9 @@ impl From<DecimalError> for DateTimeError {
DateTimeError::FixedDecimalFormatter(e)
}
}

impl From<MismatchedCalendarError> for DateTimeError {
fn from(e: MismatchedCalendarError) -> Self {
DateTimeError::MismatchedAnyCalendar(e.this_kind, e.date_kind)
}
}
18 changes: 9 additions & 9 deletions components/datetime/src/format/neo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use super::datetime::write_pattern;
use crate::calendar::CldrCalendar;
use crate::calendar::{CldrCalendar, MonthNamesV1Provider, YearNamesV1Provider};
use crate::error::DateTimeError as Error;
use crate::external_loaders::*;
use crate::fields::{self, FieldLength, FieldSymbol};
Expand Down Expand Up @@ -698,8 +698,8 @@ impl RawDateTimeNames {
field_length: FieldLength,
) -> Result<(), Error>
where
P: DataProvider<M> + ?Sized,
M: KeyedDataMarker<Yokeable = YearNamesV1<'static>>,
P: YearNamesV1Provider<M> + ?Sized,
M: DataMarker<Yokeable = YearNamesV1<'static>>,
{
let field = fields::Field {
symbol: FieldSymbol::Era,
Expand Down Expand Up @@ -741,8 +741,8 @@ impl RawDateTimeNames {
field_length: FieldLength,
) -> Result<(), Error>
where
P: DataProvider<M> + ?Sized,
M: KeyedDataMarker<Yokeable = MonthNamesV1<'static>>,
P: MonthNamesV1Provider<M> + ?Sized,
M: DataMarker<Yokeable = MonthNamesV1<'static>>,
{
let field = fields::Field {
symbol: FieldSymbol::Month(field_symbol),
Expand Down Expand Up @@ -917,8 +917,8 @@ impl RawDateTimeNames {
#[allow(clippy::too_many_arguments)]
pub(crate) fn load_for_pattern<YearMarker, MonthMarker>(
&mut self,
year_provider: Option<&(impl DataProvider<YearMarker> + ?Sized)>,
month_provider: Option<&(impl DataProvider<MonthMarker> + ?Sized)>,
year_provider: Option<&(impl YearNamesV1Provider<YearMarker> + ?Sized)>,
month_provider: Option<&(impl MonthNamesV1Provider<MonthMarker> + ?Sized)>,
weekday_provider: Option<&(impl DataProvider<WeekdayNamesV1Marker> + ?Sized)>,
dayperiod_provider: Option<&(impl DataProvider<DayPeriodNamesV1Marker> + ?Sized)>,
fixed_decimal_formatter_loader: Option<&impl FixedDecimalFormatterLoader>,
Expand All @@ -927,8 +927,8 @@ impl RawDateTimeNames {
pattern_items: impl Iterator<Item = PatternItem>,
) -> Result<(), Error>
where
YearMarker: KeyedDataMarker<Yokeable = YearNamesV1<'static>>,
MonthMarker: KeyedDataMarker<Yokeable = MonthNamesV1<'static>>,
YearMarker: DataMarker<Yokeable = YearNamesV1<'static>>,
MonthMarker: DataMarker<Yokeable = MonthNamesV1<'static>>,
{
let fields = pattern_items.filter_map(|p| match p {
PatternItem::Field(field) => Some(field),
Expand Down
1 change: 1 addition & 0 deletions components/datetime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ pub use calendar::CldrCalendar;
pub use calendar::InternalCldrCalendar;
pub use datetime::{TimeFormatter, TypedDateFormatter, TypedDateTimeFormatter};
pub use error::DateTimeError;
pub use error::MismatchedCalendarError;
pub use format::datetime::FormattedDateTime;
#[cfg(feature = "experimental")]
pub use format::neo::{FormattedDateTimePattern, TypedDateTimeNames};
Expand Down
Loading

0 comments on commit 2a25844

Please sign in to comment.