Skip to content

Commit

Permalink
Add exemplar cities format (VVV) (#6018)
Browse files Browse the repository at this point in the history
  • Loading branch information
robertbastian authored Jan 21, 2025
1 parent 9fc2b42 commit 78933a6
Show file tree
Hide file tree
Showing 43 changed files with 4,007 additions and 82 deletions.
84 changes: 84 additions & 0 deletions components/datetime/examples/timezone_picker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 ).

use std::collections::BTreeMap;

use icu::calendar::Date;
use icu::datetime::{fieldsets, DateTimeFormatter};
use icu::locale::locale;
use icu::timezone::Time;

fn main() {
let mapper = icu::timezone::TimeZoneIdMapper::new();
let offsets = icu::timezone::ZoneOffsetCalculator::new();

let prefs = locale!("en").into();

let offset_formatter = DateTimeFormatter::try_new(prefs, fieldsets::O::new()).unwrap();
let non_location_formatter = DateTimeFormatter::try_new(prefs, fieldsets::V::new()).unwrap();
let city_formatter = DateTimeFormatter::try_new(prefs, fieldsets::X::new()).unwrap();

let reference_date = (Date::try_new_iso(2025, 1, 1).unwrap(), Time::midnight());

let mut grouped_tzs = BTreeMap::<_, Vec<_>>::new();

for tz in mapper.iter_bcp47() {
if tz.0 == "unk" || tz.starts_with("utc") || tz.0 == "gmt" {
continue;
}

let offsets = offsets
.compute_offsets_from_time_zone(tz, reference_date)
.unwrap();

let tzi = tz
.with_offset(Some(offsets.standard))
.at_time(reference_date);

grouped_tzs
.entry(non_location_formatter.format(&tzi).to_string())
.or_default()
.push((offsets, tzi));
}

let mut list = Vec::new();

for (non_location, zones) in grouped_tzs {
for (offsets, tzi) in &zones {
list.push((
-offsets.standard.to_seconds(),
format!(
"({}{})",
offset_formatter.format(tzi),
if let Some(daylight) = offsets.daylight {
format!(
"/{}",
offset_formatter.format(
&tzi.time_zone_id()
.with_offset(Some(daylight))
.at_time(reference_date)
)
)
} else {
String::new()
}
),
if zones.len() == 1 {
non_location.clone()
} else {
format!("{non_location} - {}", city_formatter.format(tzi))
},
));
}
}

list.sort_by(|a, b| (a.0, &a.2).cmp(&(b.0, &b.2)));

for (_, offset, non_location) in &list {
println!(
"{offset:0$} {non_location}",
list.iter().map(|(_, l, ..)| l.len()).max().unwrap()
);
}
}
1 change: 1 addition & 0 deletions components/datetime/src/combo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ where
type DayPeriodNames = DT::DayPeriodNames;
type ZoneEssentials = Z::ZoneEssentials;
type ZoneLocations = Z::ZoneLocations;
type ZoneExemplars = Z::ZoneExemplars;
type ZoneGenericLong = Z::ZoneGenericLong;
type ZoneGenericShort = Z::ZoneGenericShort;
type ZoneSpecificLong = Z::ZoneSpecificLong;
Expand Down
6 changes: 5 additions & 1 deletion components/datetime/src/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,11 @@ pub enum ZoneFieldSet {
/// “PT”.
Vs(fieldsets::Vs),
/// The location format, as in
/// “Los Angeles time”.
/// “Los Angeles Time”.
L(fieldsets::L),
/// The exemplar city format, as in
/// “Los Angeles.
X(fieldsets::X),
}

/// An enumeration over all possible date+time composite field sets.
Expand Down Expand Up @@ -476,6 +479,7 @@ impl_attrs! {
V,
Vs,
L,
X,
]
}

Expand Down
42 changes: 42 additions & 0 deletions components/datetime/src/fieldsets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ macro_rules! impl_date_or_calendar_period_marker {
type DayPeriodNames = datetime_marker_helper!(@names/dayperiod,);
type ZoneEssentials = datetime_marker_helper!(@names/zone/essentials,);
type ZoneLocations = datetime_marker_helper!(@names/zone/locations,);
type ZoneExemplars = datetime_marker_helper!(@names/zone/exemplar,);
type ZoneGenericLong = datetime_marker_helper!(@names/zone/generic_long,);
type ZoneGenericShort = datetime_marker_helper!(@names/zone/generic_short,);
type ZoneSpecificLong = datetime_marker_helper!(@names/zone/specific_long,);
Expand Down Expand Up @@ -625,6 +626,7 @@ macro_rules! impl_date_marker {
type DayPeriodNames = datetime_marker_helper!(@names/dayperiod, yes);
type ZoneEssentials = datetime_marker_helper!(@names/zone/essentials,);
type ZoneLocations = datetime_marker_helper!(@names/zone/locations,);
type ZoneExemplars = datetime_marker_helper!(@names/zone/exemplar,);
type ZoneGenericLong = datetime_marker_helper!(@names/zone/generic_long,);
type ZoneGenericShort = datetime_marker_helper!(@names/zone/generic_short,);
type ZoneSpecificLong = datetime_marker_helper!(@names/zone/specific_long,);
Expand Down Expand Up @@ -791,6 +793,7 @@ macro_rules! impl_time_marker {
type DayPeriodNames = datetime_marker_helper!(@names/dayperiod, $($dayperiods_yes)?);
type ZoneEssentials = datetime_marker_helper!(@names/zone/essentials,);
type ZoneLocations = datetime_marker_helper!(@names/zone/locations,);
type ZoneExemplars = datetime_marker_helper!(@names/zone/exemplar,);
type ZoneGenericLong = datetime_marker_helper!(@names/zone/generic_long,);
type ZoneGenericShort = datetime_marker_helper!(@names/zone/generic_short,);
type ZoneSpecificLong = datetime_marker_helper!(@names/zone/specific_long,);
Expand Down Expand Up @@ -840,6 +843,8 @@ macro_rules! impl_zone_marker {
$(zone_essentials = $zone_essentials_yes:ident,)?
// Whether locations formats can occur.
$(zone_locations = $zone_locations_yes:ident,)?
// Whether exemplar city formats can occur.
$(zone_exemplars = $zone_exemplars_yes:ident,)?
// Whether generic long formats can occur.
$(zone_generic_long = $zone_generic_long_yes:ident,)?
// Whether generic short formats can occur.
Expand Down Expand Up @@ -900,6 +905,7 @@ macro_rules! impl_zone_marker {
type DayPeriodNames = datetime_marker_helper!(@names/dayperiod,);
type ZoneEssentials = datetime_marker_helper!(@names/zone/essentials, $($zone_essentials_yes)?);
type ZoneLocations = datetime_marker_helper!(@names/zone/locations, $($zone_locations_yes)?);
type ZoneExemplars = datetime_marker_helper!(@names/zone/exemplars, $($zone_exemplars_yes)?);
type ZoneGenericLong = datetime_marker_helper!(@names/zone/generic_long, $($zone_generic_long_yes)?);
type ZoneGenericShort = datetime_marker_helper!(@names/zone/generic_short, $($zone_generic_short_yes)?);
type ZoneSpecificLong = datetime_marker_helper!(@names/zone/specific_long, $($zone_specific_long_yes)?);
Expand All @@ -913,6 +919,7 @@ macro_rules! impl_zone_marker {
type TimeZoneLocalTimeInput = datetime_marker_helper!(@input/timezone/local_time, $($localtime_input_yes)?);
type EssentialsV1Marker = datetime_marker_helper!(@data/zone/essentials, $($zone_essentials_yes)?);
type LocationsV1Marker = datetime_marker_helper!(@data/zone/locations, $($zone_locations_yes)?);
type ExemplarCitiesV1Marker = datetime_marker_helper!(@data/zone/exemplars, $($zone_exemplars_yes)?);
type GenericLongV1Marker = datetime_marker_helper!(@data/zone/generic_long, $($zone_generic_long_yes)?);
type GenericShortV1Marker = datetime_marker_helper!(@data/zone/generic_short, $($zone_generic_short_yes)?);
type SpecificLongV1Marker = datetime_marker_helper!(@data/zone/specific_long, $($zone_specific_long_yes)?);
Expand Down Expand Up @@ -1564,6 +1571,41 @@ impl_zone_marker!(
input_tzid = yes,
);

impl_zone_marker!(
/// A time zone ID is required to format with this style.
/// For example, a raw [`UtcOffset`] cannot be used here.
///
/// ```compile_fail,E0277
/// use icu::calendar::{DateTime, Iso};
/// use icu::datetime::FixedCalendarDateTimeFormatter;
/// use icu::datetime::fieldsets::X;
/// use icu::timezone::UtcOffset;
/// use tinystr::tinystr;
/// use icu::locale::locale;
/// use writeable::assert_writeable_eq;
///
/// let utc_offset = UtcOffset::try_from_str("-06").unwrap();
///
/// let formatter = FixedCalendarDateTimeFormatter::try_new(
/// locale!("en-US").into(),
/// X::new(),
/// )
/// .unwrap();
///
/// // error[E0277]: the trait bound `UtcOffset: AllInputMarkers<X>` is not satisfied
/// // note: required by a bound in `FixedCalendarDateTimeFormatter::<C, FSet>::format`
/// formatter.format(&utc_offset);
/// ```
X,
description = "time zone in exemplar city format",
length_override = Long,
sample = "Chicago",
field = (fields::TimeZone::Location, fields::FieldLength::Three),
zone_locations = yes,
zone_exemplars = yes,
input_tzid = yes,
);

impl_zone_combo_helpers!(DateFieldSet, DateZone, DateFieldSet);

impl_zone_combo_helpers!(TimeFieldSet, TimeZone, TimeFieldSet);
Expand Down
10 changes: 10 additions & 0 deletions components/datetime/src/format/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,16 @@ where
],
)?
}
(FieldSymbol::TimeZone(fields::TimeZone::Location), FieldLength::Three) => {
perform_timezone_fallback(
w,
input,
datetime_names,
fdf,
field,
&[TimeZoneFormatterUnit::ExemplarCity],
)?
}
(FieldSymbol::TimeZone(fields::TimeZone::LocalizedOffset), l) => perform_timezone_fallback(
w,
input,
Expand Down
50 changes: 50 additions & 0 deletions components/datetime/src/format/time_zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub(super) enum TimeZoneFormatterUnit {
SpecificNonLocation(FieldLength),
GenericLocation,
SpecificLocation,
ExemplarCity,
#[allow(dead_code)]
GenericPartialLocation(FieldLength),
LocalizedOffset(FieldLength),
Expand Down Expand Up @@ -91,6 +92,7 @@ impl FormatTimeZone for TimeZoneFormatterUnit {
Self::SpecificLocation => {
SpecificLocationFormat.format(sink, input, data_payloads, fdf)
}
Self::ExemplarCity => ExemplarCityFormat.format(sink, input, data_payloads, fdf),
Self::GenericPartialLocation(length) => {
GenericPartialLocationFormat(length).format(sink, input, data_payloads, fdf)
}
Expand Down Expand Up @@ -381,6 +383,54 @@ impl FormatTimeZone for SpecificLocationFormat {
}
}

// Los Angeles
struct ExemplarCityFormat;

impl FormatTimeZone for ExemplarCityFormat {
/// Writes the time zone exemplar city format as defined by the UTS-35 spec.
/// e.g. Los Angeles
/// <https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology>
fn format<W: writeable::PartsWrite + ?Sized>(
&self,
sink: &mut W,
input: &ExtractedInput,
data_payloads: TimeZoneDataPayloadsBorrowed,
_fdf: Option<&FixedDecimalFormatter>,
) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
let Some(time_zone_id) = input.time_zone_id else {
return Ok(Err(FormatTimeZoneError::MissingInputField("time_zone_id")));
};
let Some(exemplars) = data_payloads.exemplars else {
return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
};
let Some(exemplars_root) = data_payloads.exemplars_root else {
return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
};
let Some(locations) = data_payloads.locations else {
return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
};
let Some(locations_root) = data_payloads.locations_root else {
return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
};

let Some(location) = exemplars
.exemplars
.get(&time_zone_id)
.or_else(|| exemplars_root.exemplars.get(&time_zone_id))
.or_else(|| locations.locations.get(&time_zone_id))
.or_else(|| locations_root.locations.get(&time_zone_id))
.or_else(|| exemplars.exemplars.get(&TimeZoneBcp47Id::unknown()))
.or_else(|| exemplars_root.exemplars.get(&TimeZoneBcp47Id::unknown()))
else {
return Ok(Err(FormatTimeZoneError::Fallback));
};

location.write_to(sink)?;

Ok(Ok(()))
}
}

// Pacific Time (Los Angeles) / PT (Los Angeles)
struct GenericPartialLocationFormat(FieldLength);

Expand Down
2 changes: 2 additions & 0 deletions components/datetime/src/neo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ where
&<FSet::T as TimeMarkers>::DayPeriodNamesV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::EssentialsV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::LocationsV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::ExemplarCitiesV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::GenericLongV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::GenericShortV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::SpecificLongV1Marker::bind(provider),
Expand Down Expand Up @@ -520,6 +521,7 @@ where
&<FSet::T as TimeMarkers>::DayPeriodNamesV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::EssentialsV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::LocationsV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::ExemplarCitiesV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::GenericLongV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::GenericShortV1Marker::bind(provider),
&<FSet::Z as ZoneMarkers>::SpecificLongV1Marker::bind(provider),
Expand Down
4 changes: 4 additions & 0 deletions components/datetime/src/neo_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ enum FieldSetField {
// ZoneGenericShort = 39,
ZoneGenericLong = 40,
ZoneLocation = 41,
ZoneExemplar = 42,
}

impl FieldSetField {
Expand Down Expand Up @@ -440,6 +441,7 @@ impl FieldSetSerde {
const ZONE_GENERIC: Self = Self::from_fields(&[ZoneGeneric]);
const ZONE_GENERIC_LONG: Self = Self::from_fields(&[ZoneGenericLong]);
const ZONE_LOCATION: Self = Self::from_fields(&[ZoneLocation]);
const ZONE_EXEMPLAR: Self = Self::from_fields(&[ZoneExemplar]);

const fn from_fields(fields: &[FieldSetField]) -> Self {
let mut bit_fields = 0;
Expand Down Expand Up @@ -587,6 +589,7 @@ impl FieldSetSerde {
(ZoneFieldSet::V(v), true) => (Self::ZONE_GENERIC, v.to_raw_options()),
(ZoneFieldSet::Vs(v), true) => (Self::ZONE_GENERIC, v.to_raw_options()),
(ZoneFieldSet::L(v), true) => (Self::ZONE_LOCATION, v.to_raw_options()),
(ZoneFieldSet::X(v), true) => (Self::ZONE_EXEMPLAR, v.to_raw_options()),
// Non-standalone: return the short as default and long as opt-in
(ZoneFieldSet::Z(v), false) => (Self::ZONE_SPECIFIC_LONG, v.to_raw_options()),
(ZoneFieldSet::Zs(v), false) => (Self::ZONE_SPECIFIC, v.to_raw_options()),
Expand All @@ -595,6 +598,7 @@ impl FieldSetSerde {
(ZoneFieldSet::V(v), false) => (Self::ZONE_GENERIC_LONG, v.to_raw_options()),
(ZoneFieldSet::Vs(v), false) => (Self::ZONE_GENERIC, v.to_raw_options()),
(ZoneFieldSet::L(v), false) => (Self::ZONE_LOCATION, v.to_raw_options()),
(ZoneFieldSet::X(v), false) => (Self::ZONE_EXEMPLAR, v.to_raw_options()),
}
}

Expand Down
Loading

0 comments on commit 78933a6

Please sign in to comment.