// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
//! A formatter specifically for the time zone.
use crate::provider::time_zones::TimeZoneBcp47Id;
use alloc::borrow::Cow;
use alloc::format;
use alloc::string::String;
use core::fmt;
use smallvec::SmallVec;
use tinystr::tinystr;
use crate::{
error::DateTimeError,
fields::{FieldSymbol, TimeZone},
format::time_zone::FormattedTimeZone,
input::TimeZoneInput,
pattern::{PatternError, PatternItem},
provider::{self, calendar::patterns::PatternPluralsFromPatternsV1Marker},
};
use icu_provider::prelude::*;
use writeable::Writeable;
#[cfg(doc)]
use crate::ZonedDateTimeFormatter;
/// Loads a resource into its destination if the destination has not already been filled.
fn load<D, P>(
locale: &DataLocale,
destination: &mut Option<DataPayload<D>>,
provider: &P,
) -> Result<(), DateTimeError>
where
D: KeyedDataMarker,
P: DataProvider<D> + ?Sized,
{
if destination.is_none() {
*destination = Some(
provider
.load(DataRequest {
locale,
metadata: Default::default(),
})?
.take_payload()?,
);
}
Ok(())
}
/// [`TimeZoneFormatter`] is available for users who need to separately control the formatting of time
/// zones. Note: most users might prefer [`ZonedDateTimeFormatter`], which includes default time zone
/// formatting according to the calendar.
///
/// [`TimeZoneFormatter`] uses data from the [data provider] and the selected locale
/// to format time zones into that locale.
///
/// The various time-zone configs specified in UTS-35 require different sets of data for
/// formatting. As such,[`TimeZoneFormatter`] will pull in only the resources needed to format the
/// config that it is given upon construction.
///
/// For that reason, one should think of the process of formatting a time zone in two steps:
/// first, a computationally heavy construction of [`TimeZoneFormatter`], and then fast formatting
/// of the time-zone data using the instance.
///
/// [`CustomTimeZone`] can be used as formatting input.
///
/// # Examples
///
/// Here, we configure the [`TimeZoneFormatter`] to first look for time zone formatting symbol
/// data for `generic_non_location_short`, and if it does not exist, to subsequently check
/// for `generic_non_location_long` data.
///
/// ```
/// use icu::calendar::DateTime;
/// use icu::timezone::{CustomTimeZone, MetazoneCalculator, IanaToBcp47Mapper};
/// use icu::datetime::{DateTimeError, time_zone::TimeZoneFormatter};
/// use icu::locid::locale;
/// use tinystr::tinystr;
/// use writeable::assert_writeable_eq;
///
/// // Set up the time zone. Note: the inputs here are
/// // 1. The GMT offset
/// // 2. The IANA time zone ID
/// // 3. A datetime (for metazone resolution)
/// // 4. Note: we do not need the zone variant because of `load_generic_*()`
///
/// // Set up the Metazone calculator, time zone ID mapper,
/// // and the DateTime to use in calculation
/// let mzc = MetazoneCalculator::new();
/// let mapper = IanaToBcp47Mapper::new();
/// let datetime = DateTime::try_new_iso_datetime(2022, 8, 29, 0, 0, 0)
/// .unwrap();
///
/// // Set up the formatter
/// let mut tzf = TimeZoneFormatter::try_new(
/// &locale!("en").into(),
/// Default::default(),
/// )
/// .unwrap();
/// tzf.include_generic_non_location_short()?
/// .include_generic_non_location_long()?;
///
/// // "uschi" - has metazone symbol data for generic_non_location_short
/// let mut time_zone = "-0600".parse::<CustomTimeZone>().unwrap();
/// time_zone.time_zone_id = mapper.as_borrowed().get("America/Chicago");
/// time_zone.maybe_calculate_metazone(&mzc, &datetime);
/// assert_writeable_eq!(
/// tzf.format(&time_zone),
/// "CT"
/// );
///
/// // "ushnl" - has time zone override symbol data for generic_non_location_short
/// let mut time_zone = "-1000".parse::<CustomTimeZone>().unwrap();
/// time_zone.time_zone_id = Some(tinystr!(8, "ushnl").into());
/// time_zone.maybe_calculate_metazone(&mzc, &datetime);
/// assert_writeable_eq!(
/// tzf.format(&time_zone),
/// "HST"
/// );
///
/// // "frpar" - does not have symbol data for generic_non_location_short, so falls
/// // back to generic_non_location_long
/// let mut time_zone = "+0100".parse::<CustomTimeZone>().unwrap();
/// time_zone.time_zone_id = Some(tinystr!(8, "frpar").into());
/// time_zone.maybe_calculate_metazone(&mzc, &datetime);
/// assert_writeable_eq!(
/// tzf.format(&time_zone),
/// "Central European Time"
/// );
///
/// // GMT with offset - used when metazone is not available
/// let mut time_zone = "+0530".parse::<CustomTimeZone>().unwrap();
/// assert_writeable_eq!(
/// tzf.format(&time_zone),
/// "GMT+05:30"
/// );
///
/// # Ok::<(), DateTimeError>(())
/// ```
///
/// [data provider]: icu_provider
/// [`CustomTimeZone`]: icu_timezone::CustomTimeZone
#[derive(Debug)]
pub struct TimeZoneFormatter {
pub(super) locale: DataLocale,
pub(super) data_payloads: TimeZoneDataPayloads,
pub(super) format_units: SmallVec<[TimeZoneFormatterUnit; 3]>,
pub(super) fallback_unit: TimeZoneFormatterUnit,
}
/// A container contains all data payloads for CustomTimeZone.
#[derive(Debug)]
pub(super) struct TimeZoneDataPayloads {
/// The data that contains meta information about how to display content.
pub(super) zone_formats: DataPayload<provider::time_zones::TimeZoneFormatsV1Marker>,
/// The exemplar cities for time zones.
pub(super) exemplar_cities: Option<DataPayload<provider::time_zones::ExemplarCitiesV1Marker>>,
/// The generic long metazone names, e.g. Pacific Time
pub(super) mz_generic_long:
Option<DataPayload<provider::time_zones::MetazoneGenericNamesLongV1Marker>>,
/// The generic short metazone names, e.g. PT
pub(super) mz_generic_short:
Option<DataPayload<provider::time_zones::MetazoneGenericNamesShortV1Marker>>,
/// The specific long metazone names, e.g. Pacific Daylight Time
pub(super) mz_specific_long:
Option<DataPayload<provider::time_zones::MetazoneSpecificNamesLongV1Marker>>,
/// The specific short metazone names, e.g. Pacific Daylight Time
pub(super) mz_specific_short:
Option<DataPayload<provider::time_zones::MetazoneSpecificNamesShortV1Marker>>,
}
impl TimeZoneFormatter {
/// Constructor that selectively loads data based on what is required to
/// format the given pattern into the given locale.
pub(super) fn try_new_for_pattern<ZP>(
zone_provider: &ZP,
locale: &DataLocale,
patterns: DataPayload<PatternPluralsFromPatternsV1Marker>,
options: &TimeZoneFormatterOptions,
) -> Result<Self, DateTimeError>
where
ZP: DataProvider<provider::time_zones::TimeZoneFormatsV1Marker>
+ DataProvider<provider::time_zones::ExemplarCitiesV1Marker>
+ DataProvider<provider::time_zones::MetazoneGenericNamesLongV1Marker>
+ DataProvider<provider::time_zones::MetazoneGenericNamesShortV1Marker>
+ DataProvider<provider::time_zones::MetazoneSpecificNamesLongV1Marker>
+ DataProvider<provider::time_zones::MetazoneSpecificNamesShortV1Marker>
+ ?Sized,
{
let format_units = SmallVec::<[TimeZoneFormatterUnit; 3]>::new();
let data_payloads = TimeZoneDataPayloads {
zone_formats: zone_provider
.load(DataRequest {
locale,
metadata: Default::default(),
})?
.take_payload()?,
exemplar_cities: None,
mz_generic_long: None,
mz_generic_short: None,
mz_specific_long: None,
mz_specific_short: None,
};
let zone_symbols = patterns
.get()
.0
.patterns_iter()
.flat_map(|pattern| pattern.items.iter())
.filter_map(|item| match item {
PatternItem::Field(field) => Some(field),
_ => None,
})
.filter_map(|field| match field.symbol {
FieldSymbol::TimeZone(zone) => Some((field.length.idx(), zone)),
_ => None,
});
let mut tz_format: TimeZoneFormatter = Self {
data_payloads,
// TODO(#2237): Determine whether we need to save the locale in the formatter
locale: locale.clone(),
format_units,
fallback_unit: TimeZoneFormatter::get_fallback_unit(options.fallback_format),
};
let mut prev_length = None;
let mut prev_symbol = None;
for (length, symbol) in zone_symbols {
if prev_length.is_none() && prev_symbol.is_none() {
prev_length = Some(length);
prev_symbol = Some(symbol);
} else if prev_length != Some(length) && prev_symbol != Some(symbol) {
// We don't support the pattern that has multiple different timezone fields of different types.
return Err(DateTimeError::Pattern(PatternError::UnsupportedPluralPivot));
}
match symbol {
TimeZone::LowerZ => match length {
1..=3 => {
tz_format.load_specific_non_location_short(zone_provider)?;
}
4 => {
tz_format.load_specific_non_location_long(zone_provider)?;
}
_ => {
return Err(DateTimeError::Pattern(PatternError::FieldLengthInvalid(
FieldSymbol::TimeZone(symbol),
)))
}
},
TimeZone::LowerV => match length {
1 => {
tz_format.load_generic_non_location_short(zone_provider)?;
}
4 => {
tz_format.load_generic_non_location_long(zone_provider)?;
}
_ => {
return Err(DateTimeError::Pattern(PatternError::FieldLengthInvalid(
FieldSymbol::TimeZone(symbol),
)))
}
},
TimeZone::UpperV => match length {
1 => (), // BCP-47 identifier, no CLDR-data necessary.
2 => (), // IANA time-zone ID, no CLDR data necessary.
3 => {
tz_format.load_exemplar_city_format(zone_provider)?;
}
4 => {
tz_format.load_generic_location_format(zone_provider)?;
}
_ => {
return Err(DateTimeError::Pattern(PatternError::FieldLengthInvalid(
FieldSymbol::TimeZone(symbol),
)))
}
},
TimeZone::UpperZ => match length {
1..=3 => {
tz_format.include_iso_8601_format(
IsoFormat::Basic,
IsoMinutes::Required,
IsoSeconds::Optional,
)?;
}
4 => {
tz_format.include_localized_gmt_format()?;
}
5 => {
tz_format.include_iso_8601_format(
IsoFormat::UtcExtended,
IsoMinutes::Required,
IsoSeconds::Optional,
)?;
}
_ => {
return Err(DateTimeError::Pattern(PatternError::FieldLengthInvalid(
FieldSymbol::TimeZone(symbol),
)))
}
},
TimeZone::LowerX => match length {
1 => {
tz_format.include_iso_8601_format(
IsoFormat::UtcBasic,
IsoMinutes::Optional,
IsoSeconds::Never,
)?;
}
2 => {
tz_format.include_iso_8601_format(
IsoFormat::UtcBasic,
IsoMinutes::Required,
IsoSeconds::Never,
)?;
}
3 => {
tz_format.include_iso_8601_format(
IsoFormat::UtcExtended,
IsoMinutes::Required,
IsoSeconds::Never,
)?;
}
4 => {
tz_format.include_iso_8601_format(
IsoFormat::UtcBasic,
IsoMinutes::Required,
IsoSeconds::Optional,
)?;
}
5 => {
tz_format.include_iso_8601_format(
IsoFormat::UtcExtended,
IsoMinutes::Required,
IsoSeconds::Optional,
)?;
}
_ => {
return Err(DateTimeError::Pattern(PatternError::FieldLengthInvalid(
FieldSymbol::TimeZone(symbol),
)))
}
},
TimeZone::UpperX => match length {
1 => {
tz_format.include_iso_8601_format(
IsoFormat::Basic,
IsoMinutes::Optional,
IsoSeconds::Never,
)?;
}
2 => {
tz_format.include_iso_8601_format(
IsoFormat::Basic,
IsoMinutes::Required,
IsoSeconds::Never,
)?;
}
3 => {
tz_format.include_iso_8601_format(
IsoFormat::Extended,
IsoMinutes::Required,
IsoSeconds::Never,
)?;
}
4 => {
tz_format.include_iso_8601_format(
IsoFormat::Basic,
IsoMinutes::Required,
IsoSeconds::Optional,
)?;
}
5 => {
tz_format.include_iso_8601_format(
IsoFormat::Extended,
IsoMinutes::Required,
IsoSeconds::Optional,
)?;
}
_ => {
return Err(DateTimeError::Pattern(PatternError::FieldLengthInvalid(
FieldSymbol::TimeZone(symbol),
)))
}
},
TimeZone::UpperO => match length {
1..=4 => {
tz_format.include_localized_gmt_format()?;
}
_ => {
return Err(DateTimeError::Pattern(PatternError::FieldLengthInvalid(
FieldSymbol::TimeZone(symbol),
)))
}
},
}
}
Ok(tz_format)
}
icu_provider::gen_any_buffer_data_constructors!(
locale: include,
options: TimeZoneFormatterOptions,
error: DateTimeError,
/// Creates a new [`TimeZoneFormatter`] with a GMT or ISO format using compiled data.
///
/// To enable other time zone styles, use one of the `with` (compiled data) or `load` (runtime
/// data provider) methods.
///
/// ✨ *Enabled with the `compiled_data` Cargo feature.*
///
/// [📚 Help choosing a constructor](icu_provider::constructors)
///
/// # Examples
///
/// Default format is Localized GMT:
///
/// ```
/// use icu::datetime::time_zone::{
/// TimeZoneFormatter, TimeZoneFormatterOptions,
/// };
/// use icu::locid::locale;
/// use icu::timezone::CustomTimeZone;
/// use writeable::assert_writeable_eq;
///
/// let tzf = TimeZoneFormatter::try_new(
/// &locale!("es").into(),
/// TimeZoneFormatterOptions::default(),
/// )
/// .unwrap();
///
/// let time_zone = "-0700".parse::<CustomTimeZone>().unwrap();
///
/// assert_writeable_eq!(tzf.format(&time_zone), "GMT-07:00");
/// ```
);
#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
pub fn try_new_unstable<P>(
provider: &P,
locale: &DataLocale,
options: TimeZoneFormatterOptions,
) -> Result<Self, DateTimeError>
where
P: DataProvider<provider::time_zones::TimeZoneFormatsV1Marker> + ?Sized,
{
let format_units = SmallVec::<[TimeZoneFormatterUnit; 3]>::new();
let data_payloads = TimeZoneDataPayloads {
zone_formats: provider
.load(DataRequest {
locale,
metadata: Default::default(),
})?
.take_payload()?,
exemplar_cities: None,
mz_generic_long: None,
mz_generic_short: None,
mz_specific_long: None,
mz_specific_short: None,
};
Ok(Self {
data_payloads,
locale: locale.clone(),
format_units,
fallback_unit: TimeZoneFormatter::get_fallback_unit(options.fallback_format),
})
}
/// Include generic-non-location-long format for timezone from compiled data. For example, "Pacific Time".
#[cfg(feature = "compiled_data")]
pub fn include_generic_non_location_long(
&mut self,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.load_generic_non_location_long(&crate::provider::Baked)
}
/// Include generic-non-location-short format for timezone from compiled data. For example, "PT".
#[cfg(feature = "compiled_data")]
pub fn include_generic_non_location_short(
&mut self,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.load_generic_non_location_short(&crate::provider::Baked)
}
/// Include specific-non-location-long format for timezone from compiled data. For example, "Pacific Standard Time".
#[cfg(feature = "compiled_data")]
pub fn include_specific_non_location_long(
&mut self,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.load_specific_non_location_long(&crate::provider::Baked)
}
/// Include specific-non-location-short format for timezone from compiled data. For example, "PDT".
#[cfg(feature = "compiled_data")]
pub fn include_specific_non_location_short(
&mut self,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.load_specific_non_location_short(&crate::provider::Baked)
}
/// Include generic-location format for timezone from compiled data. For example, "Los Angeles Time".
#[cfg(feature = "compiled_data")]
pub fn include_generic_location_format(
&mut self,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.load_generic_location_format(&crate::provider::Baked)
}
/// Include localized-GMT format for timezone. For example, "GMT-07:00".
pub fn include_localized_gmt_format(
&mut self,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.format_units
.push(TimeZoneFormatterUnit::LocalizedGmt(LocalizedGmtFormat {}));
Ok(self)
}
/// Include ISO-8601 format for timezone. For example, "-07:00".
pub fn include_iso_8601_format(
&mut self,
format: IsoFormat,
minutes: IsoMinutes,
seconds: IsoSeconds,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.format_units
.push(TimeZoneFormatterUnit::Iso8601(Iso8601Format {
format,
minutes,
seconds,
}));
Ok(self)
}
/// Load generic-non-location-long format for timezone. For example, "Pacific Time".
pub fn load_generic_non_location_long<ZP>(
&mut self,
zone_provider: &ZP,
) -> Result<&mut TimeZoneFormatter, DateTimeError>
where
ZP: DataProvider<provider::time_zones::MetazoneGenericNamesLongV1Marker> + ?Sized,
{
if self.data_payloads.mz_generic_long.is_none() {
load(
&self.locale,
&mut self.data_payloads.mz_generic_long,
zone_provider,
)?;
}
self.format_units
.push(TimeZoneFormatterUnit::GenericNonLocationLong(
GenericNonLocationLongFormat {},
));
Ok(self)
}
/// Load generic-non-location-short format for timezone. For example, "PT".
pub fn load_generic_non_location_short<ZP>(
&mut self,
zone_provider: &ZP,
) -> Result<&mut TimeZoneFormatter, DateTimeError>
where
ZP: DataProvider<provider::time_zones::MetazoneGenericNamesShortV1Marker> + ?Sized,
{
if self.data_payloads.mz_generic_short.is_none() {
load(
&self.locale,
&mut self.data_payloads.mz_generic_short,
zone_provider,
)?;
}
self.format_units
.push(TimeZoneFormatterUnit::GenericNonLocationShort(
GenericNonLocationShortFormat {},
));
Ok(self)
}
/// Load specific-non-location-long format for timezone. For example, "Pacific Standard Time".
pub fn load_specific_non_location_long<ZP>(
&mut self,
zone_provider: &ZP,
) -> Result<&mut TimeZoneFormatter, DateTimeError>
where
ZP: DataProvider<provider::time_zones::MetazoneSpecificNamesLongV1Marker> + ?Sized,
{
if self.data_payloads.mz_specific_long.is_none() {
load(
&self.locale,
&mut self.data_payloads.mz_specific_long,
zone_provider,
)?;
}
self.format_units
.push(TimeZoneFormatterUnit::SpecificNonLocationLong(
SpecificNonLocationLongFormat {},
));
Ok(self)
}
/// Load specific-non-location-short format for timezone. For example, "PDT".
pub fn load_specific_non_location_short<ZP>(
&mut self,
zone_provider: &ZP,
) -> Result<&mut TimeZoneFormatter, DateTimeError>
where
ZP: DataProvider<provider::time_zones::MetazoneSpecificNamesShortV1Marker> + ?Sized,
{
if self.data_payloads.mz_specific_short.is_none() {
load(
&self.locale,
&mut self.data_payloads.mz_specific_short,
zone_provider,
)?;
}
self.format_units
.push(TimeZoneFormatterUnit::SpecificNonLocationShort(
SpecificNonLocationShortFormat {},
));
Ok(self)
}
/// Load generic-location format for timezone. For example, "Los Angeles Time".
pub fn load_generic_location_format<ZP>(
&mut self,
zone_provider: &ZP,
) -> Result<&mut TimeZoneFormatter, DateTimeError>
where
ZP: DataProvider<provider::time_zones::ExemplarCitiesV1Marker> + ?Sized,
{
if self.data_payloads.exemplar_cities.is_none() {
load(
&self.locale,
&mut self.data_payloads.exemplar_cities,
zone_provider,
)?;
}
self.format_units
.push(TimeZoneFormatterUnit::GenericLocation(
GenericLocationFormat {},
));
Ok(self)
}
/// Load exemplar-city format for timezone. For example, "Los Angeles".
fn load_exemplar_city_format<ZP>(
&mut self,
zone_provider: &ZP,
) -> Result<&mut TimeZoneFormatter, DateTimeError>
where
ZP: DataProvider<provider::time_zones::ExemplarCitiesV1Marker> + ?Sized,
{
if self.data_payloads.exemplar_cities.is_none() {
load(
&self.locale,
&mut self.data_payloads.exemplar_cities,
zone_provider,
)?;
}
self.format_units
.push(TimeZoneFormatterUnit::ExemplarCity(ExemplarCityFormat {}));
Ok(self)
}
/// Alias to [`TimeZoneFormatter::include_localized_gmt_format`].
#[deprecated(since = "1.3.0", note = "renamed to `include_localized_gmt_format`")]
pub fn load_localized_gmt_format(&mut self) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.include_localized_gmt_format()
}
/// Alias to [`TimeZoneFormatter::include_iso_8601_format`].
#[deprecated(since = "1.3.0", note = "renamed to `include_iso_8601_format`")]
pub fn load_iso_8601_format(
&mut self,
format: IsoFormat,
minutes: IsoMinutes,
seconds: IsoSeconds,
) -> Result<&mut TimeZoneFormatter, DateTimeError> {
self.include_iso_8601_format(format, minutes, seconds)
}
/// Load a fallback format for timezone. The fallback format will be executed if there are no
/// matching format results.
pub(super) fn get_fallback_unit(fallback_format: FallbackFormat) -> TimeZoneFormatterUnit {
match fallback_format {
FallbackFormat::LocalizedGmt => {
TimeZoneFormatterUnit::LocalizedGmt(LocalizedGmtFormat {})
}
FallbackFormat::Iso8601(format, minutes, seconds) => {
TimeZoneFormatterUnit::Iso8601(Iso8601Format {
format,
minutes,
seconds,
})
}
}
}
/// Takes a [`TimeZoneInput`] implementer and returns an instance of a [`FormattedTimeZone`]
/// that contains all information necessary to display a formatted time zone and operate on it.
///
/// # Examples
///
/// ```
/// use icu::datetime::time_zone::{
/// TimeZoneFormatter, TimeZoneFormatterOptions,
/// };
/// use icu::locid::locale;
/// use icu::timezone::CustomTimeZone;
/// use writeable::assert_writeable_eq;
///
/// let tzf = TimeZoneFormatter::try_new(
/// &locale!("en").into(),
/// TimeZoneFormatterOptions::default(),
/// )
/// .expect("Failed to create TimeZoneFormatter");
///
/// let time_zone = CustomTimeZone::utc();
///
/// assert_writeable_eq!(tzf.format(&time_zone), "GMT");
/// ```
pub fn format<'l, T>(&'l self, value: &'l T) -> FormattedTimeZone<'l, T>
where
T: TimeZoneInput,
{
FormattedTimeZone {
time_zone_format: self,
time_zone: value,
}
}
/// Takes a [`TimeZoneInput`] implementer and returns a string with the formatted value.
pub fn format_to_string(&self, value: &impl TimeZoneInput) -> String {
self.format(value).write_to_string().into_owned()
}
/// Formats a time segment with optional zero-padding.
fn format_time_segment(n: u8, padding: ZeroPadding) -> String {
debug_assert!((0..60).contains(&n));
match padding {
ZeroPadding::On => format!("{n:>02}"),
ZeroPadding::Off => format!("{n}"),
}
}
/// Formats the hours as a [`String`] with optional zero-padding.
fn format_offset_hours(
time_zone: &impl TimeZoneInput,
padding: ZeroPadding,
) -> Result<String, DateTimeError> {
if let Some(gmt_offset) = time_zone.gmt_offset() {
Ok(TimeZoneFormatter::format_time_segment(
(gmt_offset.offset_seconds() / 3600).unsigned_abs() as u8,
padding,
))
} else {
Err(DateTimeError::MissingInputField(Some("gmt_offset")))
}
}
/// Formats the minutes as a [`String`] with zero-padding.
fn format_offset_minutes(time_zone: &impl TimeZoneInput) -> Result<String, DateTimeError> {
if let Some(gmt_offset) = time_zone.gmt_offset() {
Ok(TimeZoneFormatter::format_time_segment(
(gmt_offset.offset_seconds() % 3600 / 60).unsigned_abs() as u8,
ZeroPadding::On,
))
} else {
Err(DateTimeError::MissingInputField(Some("gmt_offset")))
}
}
/// Formats the seconds as a [`String`] with zero-padding.
fn format_offset_seconds<W: fmt::Write + ?Sized>(
sink: &mut W,
time_zone: &impl TimeZoneInput,
) -> Result<fmt::Result, DateTimeError> {
if let Some(gmt_offset) = time_zone.gmt_offset() {
Ok(sink.write_str(&TimeZoneFormatter::format_time_segment(
(gmt_offset.offset_seconds() % 3600 % 60).unsigned_abs() as u8,
ZeroPadding::On,
)))
} else {
Err(DateTimeError::MissingInputField(Some("gmt_offset")))
}
}
}
/// Determines which ISO-8601 format should be used to format a [`GmtOffset`](icu_timezone::GmtOffset).
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(clippy::exhaustive_enums)] // this type is stable
pub enum IsoFormat {
/// ISO-8601 Basic Format.
/// Formats zero-offset numerically.
/// e.g. +0500, +0000
Basic,
/// ISO-8601 Extended Format.
/// Formats zero-offset numerically.
/// e.g. +05:00, +00:00
Extended,
/// ISO-8601 Basic Format.
/// Formats zero-offset with the ISO-8601 UTC indicator: "Z"
/// e.g. +0500, Z
UtcBasic,
/// ISO-8601 Extended Format.
/// Formats zero-offset with the ISO-8601 UTC indicator: "Z"
/// e.g. +05:00, Z
UtcExtended,
}
/// Whether the minutes field should be optional or required in ISO-8601 format.
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(clippy::exhaustive_enums)] // this type is stable
pub enum IsoMinutes {
/// Minutes are always displayed.
Required,
/// Minutes are displayed only if they are non-zero.
Optional,
}
/// Whether the seconds field should be optional or excluded in ISO-8601 format.
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(clippy::exhaustive_enums)] // this type is stable
pub enum IsoSeconds {
/// Seconds are displayed only if they are non-zero.
Optional,
/// Seconds are not displayed.
Never,
}
/// Whether a field should be zero-padded in ISO-8601 format.
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(clippy::exhaustive_enums)] // this type is stable
pub(crate) enum ZeroPadding {
/// Add zero-padding.
On,
/// Do not add zero-padding.
Off,
}
/// An enum for time zone fallback formats.
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
#[derive(Default)]
pub enum FallbackFormat {
/// The ISO 8601 format for time zone format fallback.
Iso8601(IsoFormat, IsoMinutes, IsoSeconds),
/// The localized GMT format for time zone format fallback.
///
/// See [UTS 35 on Dates](https://unicode.org/reports/tr35/tr35-dates.html#71-time-zone-format-terminology) for more information.
#[default]
LocalizedGmt,
}
/// A bag of options to define how time zone will be formatted.
#[derive(Default, Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct TimeZoneFormatterOptions {
/// The time zone format fallback option.
///
/// See [UTS 35 on Dates](https://unicode.org/reports/tr35/tr35-dates.html#71-time-zone-format-terminology) for more information.
pub fallback_format: FallbackFormat,
}
impl From<FallbackFormat> for TimeZoneFormatterOptions {
fn from(fallback_format: FallbackFormat) -> Self {
Self { fallback_format }
}
}
// Pacific Time
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct GenericNonLocationLongFormat {}
// PT
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct GenericNonLocationShortFormat {}
// Pacific Standard Time
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct SpecificNonLocationLongFormat {}
// PDT
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct SpecificNonLocationShortFormat {}
// Los Angeles Time
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct GenericLocationFormat {}
// GMT-07:00
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct LocalizedGmtFormat {}
// -07:00
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct Iso8601Format {
format: IsoFormat,
minutes: IsoMinutes,
seconds: IsoSeconds,
}
// It is only used for pattern in special case and not public to users.
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct ExemplarCityFormat {}
// An enum for time zone format unit.
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum TimeZoneFormatterUnit {
GenericNonLocationLong(GenericNonLocationLongFormat),
GenericNonLocationShort(GenericNonLocationShortFormat),
SpecificNonLocationLong(SpecificNonLocationLongFormat),
SpecificNonLocationShort(SpecificNonLocationShortFormat),
GenericLocation(GenericLocationFormat),
LocalizedGmt(LocalizedGmtFormat),
Iso8601(Iso8601Format),
ExemplarCity(ExemplarCityFormat),
}
impl Default for TimeZoneFormatterUnit {
fn default() -> Self {
TimeZoneFormatterUnit::LocalizedGmt(LocalizedGmtFormat {})
}
}
pub(super) trait FormatTimeZone {
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError>;
}
impl FormatTimeZone for TimeZoneFormatterUnit {
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
match self {
Self::GenericNonLocationLong(unit) => unit.format(sink, time_zone, data_payloads),
Self::GenericNonLocationShort(unit) => unit.format(sink, time_zone, data_payloads),
Self::SpecificNonLocationLong(unit) => unit.format(sink, time_zone, data_payloads),
Self::SpecificNonLocationShort(unit) => unit.format(sink, time_zone, data_payloads),
Self::GenericLocation(unit) => unit.format(sink, time_zone, data_payloads),
Self::LocalizedGmt(unit) => unit.format(sink, time_zone, data_payloads),
Self::Iso8601(unit) => unit.format(sink, time_zone, data_payloads),
Self::ExemplarCity(unit) => unit.format(sink, time_zone, data_payloads),
}
}
}
impl FormatTimeZone for GenericNonLocationLongFormat {
/// Writes the time zone in long generic non-location format as defined by the UTS-35 spec.
/// e.g. Pacific Time
/// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
let formatted_time_zone: Option<&str> = data_payloads
.mz_generic_long
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone
.time_zone_id()
.and_then(|tz| metazones.overrides.get(&tz))
})
.or_else(|| {
data_payloads
.mz_generic_long
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone
.metazone_id()
.and_then(|mz| metazones.defaults.get(&mz))
})
});
match formatted_time_zone {
Some(ftz) => Ok(sink.write_str(ftz)),
None => Err(DateTimeError::UnsupportedOptions),
}
}
}
impl FormatTimeZone for GenericNonLocationShortFormat {
/// Writes the time zone in short generic non-location format as defined by the UTS-35 spec.
/// e.g. PT
/// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
let formatted_time_zone: Option<&str> = data_payloads
.mz_generic_short
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone
.time_zone_id()
.and_then(|tz| metazones.overrides.get(&tz))
})
.or_else(|| {
data_payloads
.mz_generic_short
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone
.metazone_id()
.and_then(|mz| metazones.defaults.get(&mz))
})
});
match formatted_time_zone {
Some(ftz) => Ok(sink.write_str(ftz)),
None => Err(DateTimeError::UnsupportedOptions),
}
}
}
impl FormatTimeZone for SpecificNonLocationShortFormat {
/// Writes the time zone in short specific non-location format as defined by the UTS-35 spec.
/// e.g. PDT
/// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
let formatted_time_zone: Option<&str> = data_payloads
.mz_specific_short
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone.time_zone_id().and_then(|tz| {
time_zone
.zone_variant()
.and_then(|variant| metazones.overrides.get_2d(&tz, &variant))
})
})
.or_else(|| {
data_payloads
.mz_specific_short
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone.metazone_id().and_then(|mz| {
time_zone
.zone_variant()
.and_then(|variant| metazones.defaults.get_2d(&mz, &variant))
})
})
});
match formatted_time_zone {
Some(ftz) => Ok(sink.write_str(ftz)),
None => Err(DateTimeError::UnsupportedOptions),
}
}
}
impl FormatTimeZone for SpecificNonLocationLongFormat {
/// Writes the time zone in long specific non-location format as defined by the UTS-35 spec.
/// e.g. Pacific Daylight Time
/// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
let formatted_time_zone: Option<&str> = data_payloads
.mz_specific_long
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone.time_zone_id().and_then(|tz| {
time_zone
.zone_variant()
.and_then(|variant| metazones.overrides.get_2d(&tz, &variant))
})
})
.or_else(|| {
data_payloads
.mz_specific_long
.as_ref()
.map(|p| p.get())
.and_then(|metazones| {
time_zone.metazone_id().and_then(|mz| {
time_zone
.zone_variant()
.and_then(|variant| metazones.defaults.get_2d(&mz, &variant))
})
})
});
match formatted_time_zone {
Some(ftz) => Ok(sink.write_str(ftz)),
None => Err(DateTimeError::UnsupportedOptions),
}
}
}
impl FormatTimeZone for LocalizedGmtFormat {
/// Writes the time zone in localized GMT format according to the CLDR localized hour format.
/// This goes explicitly against the UTS-35 spec, which specifies long or short localized
/// GMT formats regardless of locale.
///
/// You can see more information about our decision to resolve this conflict here:
/// https://docs.google.com/document/d/16GAqaDRS6hzL8jNYjus5MglSevGBflISM-BrIS7bd4A/edit?usp=sharing
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
if let Some(gmt_offset) = time_zone.gmt_offset() {
return if gmt_offset.is_zero() {
Ok(sink.write_str(&data_payloads.zone_formats.get().gmt_zero_format.clone()))
} else {
// TODO(blocked on #277) Use formatter utility instead of replacing "{0}".
Ok(sink.write_str(
&data_payloads
.zone_formats
.get()
.gmt_format
.replace(
"{0}",
if gmt_offset.is_positive() {
&data_payloads.zone_formats.get().hour_format.0
} else {
&data_payloads.zone_formats.get().hour_format.1
},
)
// support all combos of "(HH|H):mm" by replacing longest patterns first.
.replace(
"HH",
if let Ok(offset_hours) =
&TimeZoneFormatter::format_offset_hours(time_zone, ZeroPadding::On)
{
offset_hours
} else {
return Err(DateTimeError::MissingInputField(Some("gmt_offset")));
},
)
.replace(
"mm",
if let Ok(offset_minutes) =
&TimeZoneFormatter::format_offset_minutes(time_zone)
{
offset_minutes
} else {
return Err(DateTimeError::MissingInputField(Some("gmt_offset")));
},
)
.replace(
'H',
if let Ok(offset_hours) =
&TimeZoneFormatter::format_offset_hours(time_zone, ZeroPadding::Off)
{
offset_hours
} else {
return Err(DateTimeError::MissingInputField(Some("gmt_offset")));
},
),
))
};
};
Err(DateTimeError::MissingInputField(Some("gmt_offset")))
}
}
impl FormatTimeZone for GenericLocationFormat {
/// Writes the time zone in generic location format as defined by the UTS-35 spec.
/// e.g. France Time
/// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
// TODO(blocked on #277) Use formatter utility instead of replacing "{0}".
let formatted_time_zone: Option<alloc::string::String> = data_payloads
.exemplar_cities
.as_ref()
.map(|p| p.get())
.and_then(|cities| time_zone.time_zone_id().and_then(|id| cities.0.get(&id)))
.map(|location| {
data_payloads
.zone_formats
.get()
.region_format
.replace("{0}", location)
});
match formatted_time_zone {
Some(ftz) => Ok(sink.write_str(&ftz)),
None => Err(DateTimeError::UnsupportedOptions),
}
}
}
impl FormatTimeZone for Iso8601Format {
/// Writes a [`GmtOffset`](crate::input::GmtOffset) in ISO-8601 format according to the
/// given formatting options.
///
/// [`IsoFormat`] determines whether the format should be Basic or Extended,
/// and whether a zero-offset should be formatted numerically or with
/// The UTC indicator: "Z"
/// - Basic e.g. +0800
/// - Extended e.g. +08:00
///
/// [`IsoMinutes`] can be required or optional.
/// [`IsoSeconds`] can be optional or never.
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
_data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
if let Some(gmt_offset) = time_zone.gmt_offset() {
if gmt_offset.is_zero()
&& matches!(self.format, IsoFormat::UtcBasic | IsoFormat::UtcExtended)
{
if let Err(e) = sink.write_char('Z') {
return Ok(Err(e));
}
}
let extended_format =
matches!(self.format, IsoFormat::Extended | IsoFormat::UtcExtended);
if let Err(e) = sink.write_char(if gmt_offset.is_positive() { '+' } else { '-' }) {
return Ok(Err(e));
}
if let Ok(offset_hours) =
&TimeZoneFormatter::format_offset_hours(time_zone, ZeroPadding::On)
{
if let Err(e) = sink.write_str(offset_hours) {
return Ok(Err(e));
}
} else {
return Err(DateTimeError::MissingInputField(Some("gmt_offset")));
}
match self.minutes {
IsoMinutes::Required => {
if extended_format {
if let Err(e) = sink.write_char(':') {
return Ok(Err(e));
}
}
if let Ok(offset_minutes) = &TimeZoneFormatter::format_offset_minutes(time_zone)
{
if let Err(e) = sink.write_str(offset_minutes) {
return Ok(Err(e));
}
} else {
return Err(DateTimeError::MissingInputField(Some("gmt_offset")));
}
}
IsoMinutes::Optional => {
if gmt_offset.has_minutes() {
if extended_format {
if let Err(e) = sink.write_char(':') {
return Ok(Err(e));
}
}
if let Ok(offset_minutes) =
&TimeZoneFormatter::format_offset_minutes(time_zone)
{
if let Err(e) = sink.write_str(offset_minutes) {
return Ok(Err(e));
}
} else {
return Err(DateTimeError::MissingInputField(Some("gmt_offset")));
}
}
}
}
if let IsoSeconds::Optional = self.seconds {
if gmt_offset.has_seconds() {
if extended_format {
if let Err(e) = sink.write_char(':') {
return Ok(Err(e));
}
}
return TimeZoneFormatter::format_offset_seconds(sink, time_zone);
}
}
return Ok(Ok(()));
};
Err(DateTimeError::MissingInputField(Some("gmt_offset")))
}
}
impl FormatTimeZone for ExemplarCityFormat {
fn format<W: fmt::Write + ?Sized>(
&self,
sink: &mut W,
time_zone: &impl TimeZoneInput,
data_payloads: &TimeZoneDataPayloads,
) -> Result<fmt::Result, DateTimeError> {
// Writes the exemplar city associated with this time zone.
let formatted_exemplar_city = data_payloads
.exemplar_cities
.as_ref()
.map(|p| p.get())
.and_then(|cities| time_zone.time_zone_id().and_then(|id| cities.0.get(&id)));
match formatted_exemplar_city {
Some(ftz) => Ok(sink.write_str(ftz)),
None => {
// Writes the unknown city "Etc/Unknown" for the current locale.
//
// If there is no localized form of "Etc/Unknown" for the current locale,
// returns the "Etc/Unknown" value of the `und` locale as a hard-coded string.
//
// This can be used as a fallback if [`exemplar_city()`](TimeZoneFormatter::exemplar_city())
// is unable to produce a localized form of the time zone's exemplar city in the current locale.
let formatted_unknown_city = data_payloads
.exemplar_cities
.as_ref()
.map(|p| p.get())
.and_then(|cities| cities.0.get(&TimeZoneBcp47Id(tinystr!(8, "unk"))))
.unwrap_or(&Cow::Borrowed("Unknown"));
Ok(sink.write_str(formatted_unknown_city))
}
}
}
}