use std::{borrow::Cow, fmt};
use crate::{
date::TimeZoneInput, format::time_zone::FormattedTimeZone, pattern::Error as PatternError,
provider, DateTimeFormatError,
};
use crate::{format::time_zone, provider::time_zones::TimeZoneFormatsV1};
use icu_locid::{LanguageIdentifier, Locale};
use icu_provider::{DataProvider, DataRequest, ResourceKey, ResourceOptions, ResourcePath};
use crate::fields::{FieldSymbol, TimeZone};
use crate::pattern::{Pattern, PatternItem};
fn load_resource<'d, D, L, P>(
locale: &L,
resource_key: ResourceKey,
destination: &mut Option<Cow<'d, D>>,
provider: &P,
) -> Result<(), DateTimeFormatError>
where
D: std::fmt::Debug + Clone + 'd,
L: Clone + Into<LanguageIdentifier>,
P: DataProvider<'d, D> + ?Sized,
{
if destination.is_none() {
*destination = Some(
provider
.load_payload(&DataRequest {
resource_path: ResourcePath {
key: resource_key,
options: ResourceOptions {
variant: None,
langid: Some(locale.clone().into()),
},
},
})?
.payload
.take()?,
);
}
Ok(())
}
pub(super) struct TimeZoneFormat<'d> {
pub(super) pattern: Pattern,
pub(super) zone_formats: Cow<'d, provider::time_zones::TimeZoneFormatsV1<'d>>,
pub(super) exemplar_cities: Option<Cow<'d, provider::time_zones::ExemplarCitiesV1<'d>>>,
pub(super) mz_generic_long:
Option<Cow<'d, provider::time_zones::MetaZoneGenericNamesLongV1<'d>>>,
pub(super) mz_generic_short:
Option<Cow<'d, provider::time_zones::MetaZoneGenericNamesShortV1<'d>>>,
pub(super) mz_specific_long:
Option<Cow<'d, provider::time_zones::MetaZoneSpecificNamesLongV1<'d>>>,
pub(super) mz_specific_short:
Option<Cow<'d, provider::time_zones::MetaZoneSpecificNamesShortV1<'d>>>,
}
impl<'d> TimeZoneFormat<'d> {
pub(super) fn try_new<L, ZP>(
locale: L,
pattern: Pattern,
zone_provider: &ZP,
) -> Result<Self, DateTimeFormatError>
where
L: Into<Locale>,
ZP: DataProvider<'d, provider::time_zones::TimeZoneFormatsV1<'d>>
+ DataProvider<'d, provider::time_zones::ExemplarCitiesV1<'d>>
+ DataProvider<'d, provider::time_zones::MetaZoneGenericNamesLongV1<'d>>
+ DataProvider<'d, provider::time_zones::MetaZoneGenericNamesShortV1<'d>>
+ DataProvider<'d, provider::time_zones::MetaZoneSpecificNamesLongV1<'d>>
+ DataProvider<'d, provider::time_zones::MetaZoneSpecificNamesShortV1<'d>>
+ ?Sized,
{
let locale = locale.into();
let zone_formats: Cow<TimeZoneFormatsV1> = zone_provider
.load_payload(&DataRequest {
resource_path: ResourcePath {
key: provider::key::TIMEZONE_FORMATS_V1,
options: ResourceOptions {
variant: None,
langid: Some(locale.clone().into()),
},
},
})?
.payload
.take()?;
let mut time_zone_format = Self {
pattern,
zone_formats,
exemplar_cities: None,
mz_generic_long: None,
mz_generic_short: None,
mz_specific_long: None,
mz_specific_short: None,
};
let zone_symbols = time_zone_format
.pattern
.items()
.iter()
.filter_map(|item| match item {
PatternItem::Field(field) => Some(field),
_ => None,
})
.filter_map(|field| match field.symbol {
FieldSymbol::TimeZone(zone) => Some((u8::from(field.length), zone)),
_ => None,
});
for (length, symbol) in zone_symbols {
match symbol {
TimeZone::LowerZ => match length {
1..=3 => load_resource(
&locale,
provider::key::TIMEZONE_SPECIFIC_NAMES_SHORT_V1,
&mut time_zone_format.mz_specific_short,
zone_provider,
)?,
4 => load_resource(
&locale,
provider::key::TIMEZONE_SPECIFIC_NAMES_LONG_V1,
&mut time_zone_format.mz_specific_long,
zone_provider,
)?,
_ => {
return Err(DateTimeFormatError::Pattern(
PatternError::FieldLengthInvalid(FieldSymbol::TimeZone(symbol)),
))
}
},
TimeZone::LowerV => match length {
1 => {
load_resource(
&locale,
provider::key::TIMEZONE_GENERIC_NAMES_SHORT_V1,
&mut time_zone_format.mz_generic_short,
zone_provider,
)?;
load_resource(
&locale,
provider::key::TIMEZONE_EXEMPLAR_CITIES_V1,
&mut time_zone_format.exemplar_cities,
zone_provider,
)?;
}
4 => {
load_resource(
&locale,
provider::key::TIMEZONE_GENERIC_NAMES_LONG_V1,
&mut time_zone_format.mz_generic_long,
zone_provider,
)?;
load_resource(
&locale,
provider::key::TIMEZONE_EXEMPLAR_CITIES_V1,
&mut time_zone_format.exemplar_cities,
zone_provider,
)?;
}
_ => {
return Err(DateTimeFormatError::Pattern(
PatternError::FieldLengthInvalid(FieldSymbol::TimeZone(symbol)),
))
}
},
TimeZone::UpperV => match length {
1 => (), 2 => (), 3 | 4 => load_resource(
&locale,
provider::key::TIMEZONE_EXEMPLAR_CITIES_V1,
&mut time_zone_format.exemplar_cities,
zone_provider,
)?,
_ => {
return Err(DateTimeFormatError::Pattern(
PatternError::FieldLengthInvalid(FieldSymbol::TimeZone(symbol)),
))
}
},
TimeZone::LowerX | TimeZone::UpperX | TimeZone::UpperZ | TimeZone::UpperO => (),
}
}
Ok(time_zone_format)
}
#[allow(unused)]
pub(super) fn format<'l: 'd, T>(&'l self, value: &'l T) -> FormattedTimeZone<'l, T>
where
T: TimeZoneInput,
{
FormattedTimeZone {
time_zone_format: self,
time_zone: value,
}
}
#[allow(unused)]
pub(super) fn format_to_write(
&self,
w: &mut impl std::fmt::Write,
value: &impl TimeZoneInput,
) -> fmt::Result {
time_zone::write_pattern(self, value, w).map_err(|_| std::fmt::Error)
}
#[allow(unused)]
pub(super) fn format_to_string(&self, value: &impl TimeZoneInput) -> String {
let mut s = String::new();
self.format_to_write(&mut s, value)
.expect("Failed to write to a String.");
s
}
pub(super) fn generic_location_format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<(), DateTimeFormatError> {
sink.write_str(
&self
.exemplar_cities
.as_ref()
.and_then(|cities| time_zone.time_zone_id().and_then(|id| cities.get(id)))
.map(|location| self.zone_formats.region_format.replace("{0}", location))
.ok_or(fmt::Error)?,
)
.map_err(DateTimeFormatError::from)
}
pub(super) fn short_generic_non_location_format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<(), DateTimeFormatError> {
sink.write_str(
self.mz_generic_short
.as_ref()
.and_then(|metazones| time_zone.metazone_id().and_then(|mz| metazones.get(mz)))
.ok_or(fmt::Error)?,
)
.map_err(DateTimeFormatError::from)
}
pub(super) fn long_generic_non_location_format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<(), DateTimeFormatError> {
sink.write_str(
self.mz_generic_long
.as_ref()
.and_then(|metazones| time_zone.metazone_id().and_then(|mz| metazones.get(mz)))
.ok_or(fmt::Error)?,
)
.map_err(DateTimeFormatError::from)
}
pub(super) fn short_specific_non_location_format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<(), DateTimeFormatError> {
sink.write_str(
self.mz_specific_short
.as_ref()
.and_then(|metazones| time_zone.metazone_id().and_then(|mz| metazones.get(mz)))
.and_then(|specific_names| {
time_zone
.time_variant()
.and_then(|variant| specific_names.get(variant))
})
.ok_or_else(|| DateTimeFormatError::from(fmt::Error))?,
)
.map_err(DateTimeFormatError::from)
}
pub(super) fn long_specific_non_location_format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<(), DateTimeFormatError> {
sink.write_str(
self.mz_specific_long
.as_ref()
.and_then(|metazones| time_zone.metazone_id().and_then(|mz| metazones.get(mz)))
.and_then(|specific_names| {
time_zone
.time_variant()
.and_then(|variant| specific_names.get(variant))
})
.ok_or(fmt::Error)?,
)
.map_err(DateTimeFormatError::from)
}
pub(super) fn localized_gmt_format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<(), DateTimeFormatError> {
let gmt_offset = time_zone.gmt_offset();
if gmt_offset.is_zero() {
sink.write_str(&self.zone_formats.gmt_zero_format.clone())
.map_err(DateTimeFormatError::from)
} else {
sink.write_str(
&self
.zone_formats
.gmt_format
.replace(
"{0}",
if gmt_offset.is_positive() {
&self.zone_formats.hour_format.0
} else {
&self.zone_formats.hour_format.1
},
)
.replace("HH", &self.format_offset_hours(time_zone, ZeroPadding::On))
.replace("mm", &self.format_offset_minutes(time_zone))
.replace("H", &self.format_offset_hours(time_zone, ZeroPadding::Off)),
)
.map_err(DateTimeFormatError::from)
}
}
pub(super) fn exemplar_city<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<(), DateTimeFormatError> {
sink.write_str(
self.exemplar_cities
.as_ref()
.and_then(|cities| time_zone.time_zone_id().and_then(|id| cities.get(id)))
.ok_or(fmt::Error)?,
)
.map_err(DateTimeFormatError::from)
}
pub(super) fn unknown_city<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
) -> Result<(), DateTimeFormatError> {
sink.write_str(
self.exemplar_cities
.as_ref()
.and_then(|cities| cities.get("Etc/Unknown"))
.unwrap_or(&Cow::Borrowed("Unknown")),
)
.map_err(DateTimeFormatError::from)
}
fn format_time_segment(n: u8, padding: ZeroPadding) -> String {
debug_assert!((0..60).contains(&n));
match padding {
ZeroPadding::On => format!("{:>02}", n),
ZeroPadding::Off => format!("{}", n),
}
}
pub(super) fn format_offset_hours(
&self,
time_zone: &impl TimeZoneInput,
padding: ZeroPadding,
) -> String {
Self::format_time_segment(
(time_zone.gmt_offset().raw_offset_seconds() / 3600).abs() as u8,
padding,
)
}
pub(super) fn format_offset_minutes(&self, time_zone: &impl TimeZoneInput) -> String {
Self::format_time_segment(
(time_zone.gmt_offset().raw_offset_seconds() % 3600 / 60).abs() as u8,
ZeroPadding::On,
)
}
pub(super) fn format_offset_seconds(&self, time_zone: &impl TimeZoneInput) -> String {
Self::format_time_segment(
(time_zone.gmt_offset().raw_offset_seconds() % 3600 % 60).abs() as u8,
ZeroPadding::On,
)
}
pub(super) fn iso8601_format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
format: IsoFormat,
minutes: IsoMinutes,
seconds: IsoSeconds,
) -> Result<(), DateTimeFormatError> {
let gmt_offset = time_zone.gmt_offset();
if gmt_offset.is_zero() && matches!(format, IsoFormat::UtcBasic | IsoFormat::UtcExtended) {
return sink.write_char('Z').map_err(|err| err.into());
}
let extended_format = matches!(format, IsoFormat::Extended | IsoFormat::UtcExtended);
sink.write_char(if gmt_offset.is_positive() { '+' } else { '-' })?;
sink.write_str(&self.format_offset_hours(time_zone, ZeroPadding::On))?;
match minutes {
IsoMinutes::Required => {
if extended_format {
sink.write_char(':')?;
}
sink.write_str(&self.format_offset_minutes(time_zone))?;
}
IsoMinutes::Optional => {
if gmt_offset.has_minutes() {
if extended_format {
sink.write_char(':')?;
}
sink.write_str(&self.format_offset_minutes(time_zone))?;
}
}
}
if let IsoSeconds::Optional = seconds {
if gmt_offset.has_seconds() {
if extended_format {
sink.write_char(':')?;
}
sink.write_str(&self.format_offset_seconds(time_zone))?;
}
}
Ok(())
}
}
pub(super) enum IsoFormat {
Basic,
Extended,
UtcBasic,
UtcExtended,
}
pub(super) enum IsoMinutes {
Required,
Optional,
}
pub(super) enum IsoSeconds {
Optional,
Never,
}
pub(super) enum ZeroPadding {
On,
Off,
}