Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CustomTimeZone is too unstructured #5533

Open
robertbastian opened this issue Sep 12, 2024 · 37 comments
Open

CustomTimeZone is too unstructured #5533

robertbastian opened this issue Sep 12, 2024 · 37 comments
Labels
C-datetime Component: datetime, calendars, time zones C-time-zone Component: Time Zones discuss-priority Discuss at the next ICU4X meeting S-medium Size: Less than a week (larger bug fix or enhancement)

Comments

@robertbastian
Copy link
Member

robertbastian commented Sep 12, 2024

// Current
pub trait TimeZoneInput {
    fn offset(&self) -> Option<UtcOffset>;
    fn time_zone_id(&self) -> Option<TimeZoneBcp47Id>;
    fn metazone_id(&self) -> Option<MetazoneId>;
    fn zone_variant(&self) -> Option<ZoneVariant>;
}

The TimeZoneInput trait combines too much information, and using it as the time zone in ZonedDateTimeFormatter yields an unusable API.

Obervation 1

Datetime libraries deal only with IANA names and offsets:

  • jiff::Timezone: iana(), to_offset(Timestamp)
  • chrono_tz::Tz: name(), offset_from_utc_datetime(DateTime<Utc>) (chrono doesn't have a timestamp type)
  • IXDTF: -06:00, +5:00[America/Vancouver], [America/Vancouver]

The concept of metazones, and zone variants is a CLDR-internal concept.1

Observation 2

The BCP47 ID is superior to the IANA ID. It is fixed width and alias-free. Using it internally is the right choice.

Observation 3

The formatters currently use ExtractedTimeZoneInput and CustomTimeZone interchangeably, as those types have the same fields. This should be cleaned up (it should be ExtractedTimeZoneInput where it is ExtractedDate/TimeInput for the other fields). For the sake of this discussion, please assume that this cleanup happened.

Proposal

I propose changing the TimeZoneInput trait to replace time_zone_id by location, with a different signature, and remove the metazone_id and zone_variant methods.

// Proposed
pub trait TimeZoneInput {
    fn offset(&self) -> Option<UtcOffset>;
    fn location(&self, lookup: Option<&TimezoneIdMapper>) -> Option<TimeZoneBcp47Id>;
}

Passing an optional &TimeZoneIdMapper, which is managed by the formatter (and enabled with a method like with_iana_support[_unstable]) to the location methods removes the need for trait implementers to manage their own TimeZoneIdMapper. Currently implementing TimeZoneInput on, for example, jiff::Timezone is not possible; it would require a newtype that wraps (jiff::Timezone, &TimezoneIdMapper), and a way to construct these from jiff:Timezones, some kind of TimeZoneIdMapperHolder. Yuck. This is a problem for most implementers, see observation 1.

The second change is the removal of metazone_id and zone_variant. These fields need a MetazoneCalculator, and, more importantly, a DateTime to be computed. If they only needed the metazone calculator, we could follow the same approach as with the timezone mapper, but because they need to be computed at a datetime, this is very display-oriented data and not really part of the "identity" of a timezone. In addition to this, the concept of a metazone is not universal, it was invented by CLDR to deduplicate non-location display names. Exposing this directly to users/trait implementers requires more CLDR knowledge than necessary.

Instead, the formatter should load a MetazoneCalculator whenever it loads a non-location style. The metazone calculation can then be done just for the lookup in the non-location display names.

This new trait is implementable by time zone abstractions in the ecosystem (jiff::Timezone, chrono_tz::Tz).

We could then remove the metazone and zone_variant fields from icu::timezone::CustomTimeZone as well. They are already not populated when parsing an IXDTF string, which I consider the main use case of CustomTimeZone, and need to be manually computed from a MetazoneCalculator and a datetime. I also support this change.

Footnotes

  1. TZDB has a DST flag, but this is not generally useful, as Ireland for example sets it in the winter. It just means "this is the other offset for this location", and is more of a zone-calculation implementation detail.

@robertbastian robertbastian added C-datetime Component: datetime, calendars, time zones S-medium Size: Less than a week (larger bug fix or enhancement) discuss-priority Discuss at the next ICU4X meeting C-time-zone Component: Time Zones labels Sep 12, 2024
@robertbastian
Copy link
Member Author

I think I made my arguments in terms of the old API (@sffc it would be really nice if that could be deleted, I still don't understand which code will stay around and which won't). The equivalent to TimeZoneInput in neo is

    /// Marker for resolving the time zone input field.
    type TimeZoneInput: Into<Option<CustomTimeZone>>;

which actually is very tightly coupled to CustomTimeZone. This should be split into its four constituent fields, and then the same argument can be made (remove metazone and variant, inject a zone id mapper into the location computation, probably by using a different trait than Into).

@robertbastian
Copy link
Member Author

#5534 to address observation 3 and decouple CustomTimeZone from neo fields.

robertbastian added a commit that referenced this issue Sep 12, 2024
`CustomTimeZone` is what it says on the lid, `icu::timezone`'s custom
timezone type. It should not be used internally by `icu::datetime`, this
PR relegates it to an input-only type and instead uses the four fields
offset, id, metazone, and zone variant as neo fields.

I further propose removing the metazone and zone variant fields in
#5533.
@robertbastian
Copy link
Member Author

I've mocked this up in #5535

@sffc
Copy link
Member

sffc commented Sep 12, 2024

The philosophy we've been using in datetime input, which has been working well, is that it is the input type's responsibility to transform itself into the inputs the formatter needs. This applies to calendar systems and time zones the same was as all other fields.

A third-party type that doesn't have metazone information baked into it could use MetazoneCalculator inside of its impl GetInput<Metazone>. I want to get to the point where we call that function only if the metazone is actually required for formatting; your PR gets us closer.

A potential counter-argument is that datetime requires WeekCalculator internally. But, it's been a bit of a pain to deal with this extra external data marker, and we have a clean way to decouple metazone, so I'd really prefer to keep it decoupled. Actually, maybe we even want to decouple week numbering.

@robertbastian
Copy link
Member Author

A third-party type that doesn't have metazone information baked into it could use MetazoneCalculator inside of its impl GetInput.

But it can't, because in the impl GetInput<Metazone> there's no MetazoneCalculator available.

@sffc
Copy link
Member

sffc commented Sep 12, 2024

A third-party type that doesn't have metazone information baked into it could use MetazoneCalculator inside of its impl GetInput.

But it can't, because in the impl GetInput<Metazone> there's no MetazoneCalculator available.

The third-party type would use compiled data and MetazoneCalculator::new(), potentially cached in a OnceLock or something. A third-party type that cares about supporting custom data would have a thin wrapper that has a MetazoneCalculator field.

@robertbastian
Copy link
Member Author

What do you think about the pattern I'm suggesting for IANA -> BCP-47 mapping?

@sffc
Copy link
Member

sffc commented Sep 12, 2024

We could then remove the metazone and zone_variant fields from icu::timezone::CustomTimeZone as well. They are already not populated when parsing an IXDTF string, which I consider the main use case of CustomTimeZone, and need to be manually computed from a MetazoneCalculator and a datetime. I also support this change.

Two things about this statement I don't think I agree with:

  1. IXDTF parsing does populate the metazone field, but only when parsing a CustomZonedDateTime since it has the date and time available. It can't populate the metazone field when parsing to a CustomTimeZone.
  2. CustomTimeZone is a temporary type with public struct fields that exists until we land on a proper time zone type. We might finally after several years be getting close to the point of writing a proper time zone type. Whether we keep CustomTimeZone around after we have a proper solution is a question we can answer.

@sffc
Copy link
Member

sffc commented Sep 12, 2024

What do you think about the pattern I'm suggesting for IANA -> BCP-47 mapping?

Similarly to how I don't think datetime should "manage" the MetazoneCalculator, I don't think it should manage a TimeZoneIdMapper.

@Manishearth
Copy link
Member

  • @robertbastian: explains the problem in the issue
  • @sffc ICU4C has a different membrane than ICU4X: it takes in very low level "just a timestamp" dates and does everything needed to calculate. ICU4X cares about data so it takes in more robust dates.
    1. ICU4X should export a ZonedDT type
    2. Jiff should implement the traits that it can implement, and those that it can't it should require going through ICU4X code on the way
  • @robertbastian: I agree we should push that membrane down. But metazone is an ICU4X internal concept. It's a pre-calculation to put it into the input. If Jiff wants to implement the input trait, Jiff needs a wrapper, and that wrapper is going to be dataful and complicated.
  • @sffc: Two things to address. First thing is about jiff: the jiff wrapper we need (whichever way the dep goes), that wrapper has this code in it. Someone writes this wrapper code and it never has to be seen or touched by anyone.
  • @robertbastian: but it's not just the wrapper code. it's the API of the wrapper, whether you wrap the type or create some weird factory type
  • @sffc I'm open to just dropping the trait. It's tricky to implement
  • @robertbastian: Not a solution, you should be able to use custom data
  • @sffc: The wrapper can have a ctor, yes?
  • @robertbastian: And dropping the trait isn't useful since then you'd still have it on the fields
  • @sffc: That's
  • @sffc Addressing the other part of the comment: is metazone internal or a precalculation. Can see some argument that it's internal. You need the datetime to calculate the metazone. Underneath it all we don't know the timestamp, we just know the calendar specific id and year number, we don't know the identity of the datetime. We don't actually have the timestamp. Secondly Erik's formulation of this kept the metazone as a separate piece. One thing a metazone can be used for is for "reverse timezone calculations". Bringing this up to say metazone is useful outside of internal
  • @robertbastian: For datetime it is an internal concept. If you provide a metazone that's inconsistent with the time zone you provide, weird things will happen
  • @Manishearth The reason these trait exist is so they can be implemented by other types. The question should be, is this information that other types typically have? For metazone, is it likely that those other types have that info? If GetField<Metazone> always wraps ICU4X... I think what we need to do is look at other time zone libraries, see what they provide, see what they require... A path forward is that maybe we don't have a trait but have conversions into our types. A one-stop shop to convert things is good.
  • @sffc CustomZonedDateTime::try_from_str, which @nekevss landed 2 weeks ago, resembles a one-stop shop.
  • @robertbastian - and if that didn't go through a format string, I would've used it with jiff. Note that IXDTF includes date, time and maybe offset and maybe IANA id. It doesn't include metazones or zone variants

No conclusion yet.

@sffc
Copy link
Member

sffc commented Sep 12, 2024

My preference, as discussed in #5248, is to replace the input traits with a concrete struct parameterized by R (the field set). The struct can have data-driven conversion functions to/from third-party types. The fields in the struct can depend on R such that they are only computed if the formatter needs them. @robertbastian probably doesn't like the design of the fields depending on the type parameter, but I think it's fine, and this solves pretty much every other problem. Maybe we could make the fields private.

@Manishearth
Copy link
Member

One thing I find unfortunate about the datetime format design is that it requires extracting all of the fieldset information up front, and this applies doubly so if we switch to structs. Some of this information can be expensive to calculate. We've optimized for this, but it would be nice if datetime only fetched fieldsets when needed (so no need for prefetching week-of info when week-of isn't in the symbols).

I think the current design is better here since it won't fetch stuff known to be unused at compile time, but I'm not sure if it's perfect. The flipside is that using too many traits too deeply in the impl code will lead to a lot of codesize bloat.

@sffc
Copy link
Member

sffc commented Sep 12, 2024

One thing I find unfortunate about the datetime format design is that it requires extracting all of the fieldset information up front, and this applies doubly so if we switch to structs. Some of this information can be expensive to calculate. We've optimized for this, but it would be nice if datetime only fetched fieldsets when needed (so no need for prefetching week-of info when week-of isn't in the symbols).

But this is exactly what the type parameter lets us do and is exactly what I'm proposing. My parameterized struct suggestion does not change which fields get calculated relative to the GetField design. It just moves it to the other side of the membrane.

@Manishearth
Copy link
Member

But this is exactly what the type parameter lets us do and is exactly what I'm proposing

I think the type parameter has gotten us the significant win of "we don't request week info if we don't need it" but it doesn't extend to not requesting info if not needed by the specific pattern as resolved by data. I think. Which is fine, it's a microoptimization.

My parameterized struct suggestion does not change which fields get calculated relative to the GetField design. It just moves it to the other side of the membrane.

I know, but traits let us lazily fetch if we decide to. Which I guess isn't that important in the neo design since we're already doing minimal field getting?

@sffc
Copy link
Member

sffc commented Sep 12, 2024

I think the type parameter has gotten us the significant win of "we don't request week info if we don't need it" but it doesn't extend to not requesting info if not needed by the specific pattern as resolved by data. I think. Which is fine, it's a microoptimization.

The current behavior, in both 1.x and 2.0 datetime formatter, is that the fields are eagerly evaluated. There was a bug from a while back to make them lazy, but I think the type parameter solves it more cleanly.

I know, but traits let us lazily fetch if we decide to. Which I guess isn't that important in the neo design since we're already doing minimal field getting?

This is accurate.

@robertbastian
Copy link
Member Author

I will wait until the legacy datetime API is deleted before commenting here.

@robertbastian robertbastian changed the title TimeZoneInput is too unstructured CustomTimeZone is too unstructured Sep 16, 2024
@sffc
Copy link
Member

sffc commented Sep 19, 2024

#5533

struct TimeZone(TimeZoneBcp47Id, Option<UtcOffset>);
struct ResolvedTimeZone(TimeZone, MetazoneId, Option<ZoneVariant>); // Formattable

struct OffsetDateTime(DateTime, UtcOffset); // Formattable, IXDTF-parseable (errors when there's no offset)

struct ZonedDateTime(DateTime, TimeZone); // IXDTF-parseable (errors when there's no IANA)
struct ResolvedZonedDateTime(ZonedDateTime, MetazoneId, Option<ZoneVariant>); // Formattable
  • @sffc You can have timezone ids that don't have metzones because CLDR doesn't have them.
  • @robertbastian You can't parse the IANA then already to get the timezone id, and every other system deals in IANA ids
  • @sffc You're right that BCP47 ids are kept in sync with metazone ids because they are provided in CLDR.
  • @sffc Another big downside of doing this is that it will increase the format function surface by 40%. I don't see the motivation here. The model works. I do support that distinction you provided of have a raw type and a formattable type.
  • @robertbastian That distinction is flawed because resolution will take your None timezone ID and make a None metazone ID out of it
  • @sffc A time zone calculator could take a BCP 47 id and a offset. Or maybe you can take a offset alone. I see the point about data loading (with a None value), and we can resolve the issues through API design. But the concerns about fallbacking -- I don't understand because they come from well defined algorithms in CLDR.
  • @robertbastian If we knew from the type system which part of the fallback we needed, we didn't have to load all data
  • @sffc That's not true. Not all exemplar cities have a mapping to time zones.
  • @robertbastian Having a struct where every field can be None is giving too much allowance for input values that would cause errors, such as every field being None
  • @sffc I would be okay with making GMT offset required in a Formattable types to constrain the possibility of inputs that result in formatting errors
  • @nekevss If the time zone is Europe/Zurich and the data is available, then use the tz name. If not, then fallback to the offset (referring to the offset being required in the structure and the name optional)
  • @robertbastian CLDR doesn't have Zurich in its English display names because it can be derived from the IANA name.
    If you want "Zürich", then you need to include a display name.
    The exemplar cities will never fall back to an offset, so we should not require the offset.
  • @sffc I'm looking up in UTS 35 what it says about fallback.
  • @nekevss This would need to work on Windows, too.
  • @robertbastian I'm just saying is that our exemplar city struct is sufficient for all time zones.
  • @sffc We would need 3 types if we were to go in this direction, because the trait impls would be different:
    1. Offset only
    2. IANA only
    3. IANA and Offset
  • @robertbastian We would not need the type that has both if there is already IANA.
  • @sffc If we didn't have the combined type, then Formatter<HourMinuteUtc> and derivations of that would not support formatting the IANA-only type. It also wouldn't work for Formatter<NeoComponents> (the one that takes a runtime field set) because that could resolve to either Utc format or Location format.
  • @robertbastian Okay, in that case you need both IANA and Offset. But then would they both be optional?
  • @sffc So then we would need all of the following (all formattable), which would need to be constructed with the input types listed:
    • OffsetZone (UtcOffset)
    • LocationTimeZone (TimeZoneBcp47Id, MetazoneId, Option<ZoneVariant>)
    • TimeZone (UtcOffset, TimeZoneBcp47Id, MetazoneId, ZoneVariant)
    • OffsetZonedDateTime (Date, Time, UtcOffset)
    • LocationZonedDateTime (Date, Time, TimeZoneBcp47Id, MetazoneId, Option<ZoneVariant>)
    • ZonedDateTime (Date, Time, UtcOffset, TimeZoneBcp47Id, MetazoneId, ZoneVariant)
  • @sffc But, also, how do we support specific time zone formatting? Specific non-location format, e.g. CEST, falls back to a UTC format, not a location format. So actually for formatting individual time zones we would also need the sixth type.
  • @robertbastian Generic partial location format is mentioned here: https://cldr.unicode.org/translation/time-zones-and-city-names
  • @sffc Yes, but it's not part of the fallback algorithm defined here: https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Goals
  • @robertbastian could put the difference in a generic
  • @sffc I would be okay with TimeZone<T> and ZonedDateTime<T>. Then T defines the reckoning of the time zone, but acknowledges that these are all just time zones. It also make it so that we don't need as many format functions.

Conclusion: Needs more work.

@sffc
Copy link
Member

sffc commented Sep 19, 2024

Does this look right?

Time Zone Type Input Fields Resolved Fields Supported Formatting Styles
Offset-Only UtcOffset UtcOffset GMT-Offset
Location-Only TimeZoneBcp47Id TimeZoneBcp47Id, MetazoneId Location, Generic, Partial
Full UtcOffset, TimeZoneBcp47Id UtcOffset, TimeZoneBcp47Id, MetazoneId, ZoneVariant GMT-Offset, Location, Generic, Specific, Partial

As a reminder, the time zone formatting styles are:

Style Example
GMT-Offset GMT-7
Location Los Angeles Time
Generic Pacific Time
Specific Pacific Daylight Time
Partial Pacific Time (Los Angeles)

(the "partial" style is not supported yet)

I think we could do this as so:

/// NOT formattable
pub struct LocationTimeZone {
    pub bcp47_id: TimeZoneBcp47Id,
}
/// NOT formattable
pub struct OffsetOnlyTimeZone {
    pub utc_offset: UtcOffset,
}
/// NOT formattable
pub struct LocationAndOffsetTimeZone {
    pub utc_offset: UtcOffset,
    pub bcp47_id: TimeZoneBcp47Id,
}

pub trait TimeZoneLike {
    // this is mostly a marker trait with no methods
}
impl TimeZoneLike for LocationTimeZone {}
impl TimeZoneLike for OffsetOnlyTimeZone {}
impl TimeZoneLike for LocationAndOffsetTimeZone {}

/// Formattable
pub struct ResolvedTimeZone<T: TimeZoneLike> {
    // either fields determined by T or optional fields; some subset of:
    utc_offset: Option<UtcOffset>,
    bcp47_id: Option<TimeZoneBcp47Id>,
    metazone_id: Option<MetazoneId>,
    zone_variant: Option<ZoneVariant>,
}

/// Formattable
pub struct ZonedDateTime<A: AsCalendar, T: TimeZoneLike> {
    pub date: Date<A>,
    pub time: Time,
    pub zone: ResolvedTimeZone<T>,
}

@sffc
Copy link
Member

sffc commented Sep 19, 2024

@nordzilla @justingrant @leftmostcat for feedback on my above post about time zone data model design.

@nordzilla
Copy link
Member

nordzilla commented Sep 20, 2024

I just read through this whole thread and I definitely support this new design.

This feels much more focused on ICU4X end-user API ergonomics, as it should be. My old design mentioned in the top comment, with everything being optional, was more driven by me reading through UTS-35 Part 4 and trying to ensure that we could support all the cases without imposing unnecessary requirements on any of the cases.

It looks like we have the proper calculators/mappings to support @sffc's proposed data structs. I think the functionality split among having an offset, a location, or both is a good differentiator for users.

There's been a lot of activity around ICU4X time zones recently. I feel both humbled and grateful.

Thank you all for your continued hard work on this project.

@justingrant
Copy link

Thanks @sffc for including me. I'm pleased to see the careful thought that you're putting into this design.

I'll add a few comments and questions based on my (admittedly limited) understanding of ICU4X design and (better, but still not perfect) understanding of the use cases for time zones in computing, based on our work designing Temporal's ZonedDateTime type. Please feel free to correct me if I have wrong assumptions below!

pub struct LocationAndOffsetTimeZone 

What are the cases where a caller would supply both? Is this only needed for the formatting case where the time zone name is different depending on DST or non-DST?

Also, what happens if the offset is not valid for this location?

/// NOT formattable
pub struct LocationTimeZone {
    pub bcp47_id: TimeZoneBcp47Id,
}
/// NOT formattable
pub struct OffsetOnlyTimeZone {
    pub utc_offset: UtcOffset,
}
/// NOT formattable
pub struct LocationAndOffsetTimeZone {
    pub utc_offset: UtcOffset,
    pub bcp47_id: TimeZoneBcp47Id,
}

Are the three structs above intended to be mainly used as inputs to methods that return a ResolvedTimeZone? Or is it expected that these structs will be used on their own without resolving them first? If the latter, what are their use cases?

I'm asking because having 4 different structs with the same *TimeZone suffix feels like it may be confusing for users to know which to use. If one of them is more "primary", then maybe making the naming more opinionated may help guide users to that one. And/or it may be clearer to have more differentiation between the inputs and the resolved type.

pub trait TimeZoneLike {
    // not sure yet what methods this needs
}
/// Formattable
pub struct ResolvedTimeZone<T: TimeZoneLike> {
    // this can have private, optional fields, or fields determined by T
}

Would it be possible to provide a bit more context about what fields and methods TimeZoneLike and ResolvedTimeZone will have? I don't need deep detail, but more info would be great so I can understand better how these are intended to used.

pub struct LocationTimeZone {
    pub bcp47_id: TimeZoneBcp47Id,
}

How will users transform what they'll usually start with (an IANA name) to one of these structs? And how will a programmer convert back to IANA?

Speaking of conversion, one of the promises that Temporal makes is that an input IANA zone is not canonicalized. If I create a ZDT with Asia/Calcutta as input, then I get Asia/Calcutta back when I ask the ZDT for its time zone ID, even though the canonical IANA name is Asia/Kolkata. If an ICU4X ZDT always canonicalizes IANA IDs to BCP-47 IDs, then how are you expecting that callers like Temporal implementations handle the need to retain the original case-normalized IANA ID?

/// Formattable
pub struct ZonedDateTime<A: AsCalendar, T: TimeZoneLike> {
    pub date: Date<A>,
    pub time: Time,
    pub zone: ResolvedTimeZone<T>,
}

Will this type have methods that allow arithmetic (e.g. add one month) and other means of creating derived values (e.g. the same time, but on Jan 1 2025) ? Or will creating derived values only be supported for location-based time zones which are DST-safe when creating derived values?

If the former, then I worry that the proposed design is not opinionated enough to dissuade developers from making a very common timezone-related programming error: assuming that if you derive values from an offset-only ZDT, then the results will be accurate for the same location that the original timestamp was measured. (Even though the developer doesn't actually know the location, only the offset!).

In Temporal, we handle this issue by allowing ZDTs to be created with either location-based or offset-only zones, but we don't allow parsing ISO 8601/RFC 3339 strings into a ZonedDateTime. A user who wants a ZDT with an offset-only zone needs to jump through more hoops, by first parsing into an Instant and then providing a time zone to convert to a ZDT. This added hurdle makes it less likely that a programmer will accidentally run into the footgun of deriving values from an offset-only ZDT.

Another option that may be more appropriate for ICU4X could be to create three types:

  • OffsetDateTime - Lacks any methods to create derived values.
  • ZonedDateTime - Requires a location time zone to create.
  • DstUnsafeZonedDateTime - Accepts both kinds of time zone inputs, but with a longer and scarier name to discourage misuse.

Another, simpler option could be to rename OffsetOnlyTImeZone to something scarier and longer to dissuade its use, e.g. DstUnsafeOffsetOnlyTimeZone.

Another option could be to make sure that any method that can create a ZDT using an offset-only time zone has a scary name, e.g. .create_dst_unsafe_ZonedDateTime().

@sffc sffc added this to the ICU4X 2.0 ⟨P1⟩ milestone Sep 24, 2024
@sffc
Copy link
Member

sffc commented Sep 24, 2024

@justingrant

Thanks as usual for your thoughtful reply! A few things:

What are the cases where a caller would supply both? Is this only needed for the formatting case where the time zone name is different depending on DST or non-DST?

So a key piece of this proposal, which I realize I didn't make super clear, is what set of data we have available. The proposal is that we have enough data to map from a location-based (IANA) time zone and an offset to all types of display names, without needing the TZDB at runtime. This allows us to offload the TZDB to some other library. So, yes, the caller needs to supply both if they want all formatting styles available. If they have only one but not the other, a subset of formatting styles will be available, listed in the table in #5533 (comment).

Also, what happens if the offset is not valid for this location?

There is no CLDR time zone style that falls back between an offset-based format and a location-based format. The "specific" style uses both pieces of data, and it will fall back to an offset-based format if there is no match for the given offset and location.

Are the three structs above intended to be mainly used as inputs to methods that return a ResolvedTimeZone?

Yes.

We should reconsider how things are named; for now, I'm just trying to get the data models right. Maybe we don't need these structs at all, if they are always just provided as arguments to constructors that return ResolvedTimeZone. But, they need to exist in the type system, so I thought we might as well use them as input structs.

Would it be possible to provide a bit more context about what fields and methods TimeZoneLike and ResolvedTimeZone will have?

Sure, I edited the post above. The trait is mostly used in the type system so that the formatting functions can raise compile errors if a time zone with the wrong subset of resolved fields is passed to an incompatible formatter.

How will users transform what they'll usually start with (an IANA name) to one of these structs? And how will a programmer convert back to IANA?

We already have TimeZoneIdMapper for this. We could definitely make more helper functions. It also handles canonicalization. I think I had you review it at some point a while ago.

If clients need to retain the original IANA IDs, they can… do so externally. It's just a single string. I don't think there's much ICU4X could do internally to make that any more efficient.

Will this type have methods that allow arithmetic (e.g. add one month) and other means of creating derived values (e.g. the same time, but on Jan 1 2025) ? Or will creating derived values only be supported for location-based time zones which are DST-safe when creating derived values?

It was not my intention to support arithmetic in ZonedDateTime; it exists only for formatting. We defer to other crates like jiff and chrono for arithmetic. If we did add support, it would of course follow the Temporal spec.

We do have a separate plain DateTime type that supports arithmetic without time zones, but it is designed primarily to implement calendar-specific arithmetic logic for Temporal. Most Rust ecosystem users shouldn't be using ICU4X types for general-purpose ISO-8601-calendar arithmetic.

In Temporal, we handle this issue by allowing ZDTs to be created with either location-based or offset-only zones, but we don't allow parsing ISO 8601/RFC 3339 strings into a ZonedDateTime.

That's a good point and I didn't think of that before. I think this new model forces us to at least have a different parse function for each of the three time zone types. We can name them like:

  • ZonedDateTime::try_from_str for the full strings (mixed offset and location)
  • ZonedDateTime::try_from_offset_only_str for an offset-only time zone
  • ZonedDateTime::try_from_location_only_str for a location-only time zone

And the parse functions will fail if the time zone contained in the string does not match the function signature's expectation.

Another option that may be more appropriate for ICU4X could be to create three types

Based on the decision in the formatting API design (#5269 (comment)), I prefer having one type with a type parameter in order to reduce the number of functions we need to write on the formatters.


A design with a similar data model but a more user-centric design might be:

// Marker types (can't be constructed)
pub enum Location {}
pub enum OffsetOnly {}
pub enum LocationAndOffset {}

// Trait for the marker types
pub trait TimeZoneLike {}
impl TimeZoneLike for Location {}
impl TimeZoneLike for OffsetOnly {}
impl TimeZoneLike for LocationAndOffset {}

/// Formattable
pub struct TimeZone<T: TimeZoneLike> {
    utc_offset: Option<UtcOffset>,
    bcp47_id: Option<TimeZoneBcp47Id>,
    metazone_id: Option<MetazoneId>,
    zone_variant: Option<ZoneVariant>,
}

impl<T: TimeZoneLike> TimeZone<T> {
    /// Creates a new time zone based on a UTC offset.
    pub fn new_offset_only(offset: UtcOffset) -> Self { ... }
}

pub struct TimeZoneFactory {
    data: DataPayload<MetazoneAndZoneVariantData>
}

impl TimeZoneFactory {
    /// Creates a new time zone based on a location and UTC offset.
    /// Most clients should use this time zone constructor if possible.
    pub fn get_time_zone(offset: UtcOffset, bcp47_id: TimeZoneBcp47id) -> TimeZone<LocationAndOffset> { ... }

    /// Creates a new time zone based on a location only.
    pub fn get_location_only_time_zone(bcp47_id: TimeZoneBcp47Id) -> TimeZone<LocationOnly> { ... }
}

/// Formattable
pub struct ZonedDateTime<A: AsCalendar, T: TimeZoneLike> {
    pub date: Date<A>,
    pub time: Time,
    pub zone: TimeZone<T>,
}

@justingrant
Copy link

@sffc thanks so much for your thorough explanations above. It's much clearer to me now what you're trying to accomplish, and overall this seems like a good direction.

It was not my intention to support arithmetic in ZonedDateTime; it exists only for formatting. We defer to other crates like jiff and chrono for arithmetic. If we did add support, it would of course follow the Temporal spec.

If this remains a formatting-only type, I'd very strongly suggest renaming it to something like ZonedDateTimeFormatter or something like that to make its role super-clear. Otherwise, any developer who's familiar with ECMAScript might assume that the ICU4X ZDT performs the same jobs as the Temporal ZDT, which would be a mistaken assumption.

If clients need to retain the original IANA IDs, they can… do so externally. It's just a single string. I don't think there's much ICU4X could do internally to make that any more efficient.

In Rust where optimization is paramount, storing as a variable-length string would be a bad decision given that the (case-normalized) strings can be represented as a 10-bit unsigned integer index into a list of ~600 IANA IDs that will, at current rates of expansion, not grow above 10 bits for 100+ years.

IMO there should be some way for callers to turn an IANA ID into a 10-bit (16-bit is probably fine) value and then to reconstitute that value back into a string. Asking every IANA-using caller to do that on their own seems wasteful, esp. given that callers won't have the full list of strings without calling ICU4X to get it. Should ICU4X help with this string=>index=>string transformation for IANA names? Or is there a better way to handle this problem?

@sffc
Copy link
Member

sffc commented Sep 27, 2024

If this remains a formatting-only type, I'd very strongly suggest renaming it to something like ZonedDateTimeFormatter or something like that to make its role super-clear. Otherwise, any developer who's familiar with ECMAScript might assume that the ICU4X ZDT performs the same jobs as the Temporal ZDT, which would be a mistaken assumption.

Hmm, I like calling it ZonedDateTime, but FormattableZonedDateTime could work. However, if the type doesn't have arithmetic functions, it's not like users could accidentally perform arithmetic with this type, so I'm not sure if I agree with the premise of changing the name.

IMO there should be some way for callers to turn an IANA ID into a 10-bit (16-bit is probably fine) value and then to reconstitute that value back into a string. Asking every IANA-using caller to do that on their own seems wasteful, esp. given that callers won't have the full list of strings without calling ICU4X to get it. Should ICU4X help with this string=>index=>string transformation for IANA names? Or is there a better way to handle this problem?

Made a separate issue for this feature request: #5610

@justingrant
Copy link

Hmm, I like calling it ZonedDateTime, but FormattableZonedDateTime could work. However, if the type doesn't have arithmetic functions, it's not like users could accidentally perform arithmetic with this type, so I'm not sure if I agree with the premise of changing the name.

I think the case you want to avoid is someone Googling for Rust ZonedDateTime and ending up at this type when they're looking for a Rust equivalent to ZonedDateTime in JS or Java. So using a different name would be very helpful to guide users to find the actual Rust equivalent of JS/Java ZDT.

FormattableZonedDateTime

Out of curiosity why FormattableZonedDateTime and not ZonedDateTimeFormatter (if it has methods) or ZonedDateTimeFormatterInput (if it's just data)?

To me the former name connotes "A superset of ZDT that is also formattable" (because "formattable" is an adjective that modifies the noun "ZonedDateTime") while the latter connotes "A formatter that uses ZDT as its data model" (because "ZonedDateTime" is the adjective-ish thng that modifies the noun "Formatter"). I assume that the latter is closer to your intent with this type?

Made a separate issue for this feature request: #5610

Thanks! I'll comment over there.

@sffc
Copy link
Member

sffc commented Oct 3, 2024

Hmm, I like calling it ZonedDateTime, but FormattableZonedDateTime could work. However, if the type doesn't have arithmetic functions, it's not like users could accidentally perform arithmetic with this type, so I'm not sure if I agree with the premise of changing the name.

I think the case you want to avoid is someone Googling for Rust ZonedDateTime and ending up at this type when they're looking for a Rust equivalent to ZonedDateTime in JS or Java. So using a different name would be very helpful to guide users to find the actual Rust equivalent of JS/Java ZDT.

I feel like good docs can solve this problem, recommending a different crate if you need arithmetic. And ICU4X's ZonedDateTime is not wrong; it just doesn't support that one piece of functionality.

@justingrant
Copy link

I feel like good docs can solve this problem, recommending a different crate if you need arithmetic. And ICU4X's ZonedDateTime is not wrong; it just doesn't support that one piece of functionality.

My strong opinion is that we should not use ZonedDateTime for a type that only provides a partial (formatting only) implementation of ZDT's features in JS and Java. It seems like unnecessary work for the entire ecosystem to ask any developer who's looking for an equivalent of JS's/Java's ZDT in Rust to read the docs and manually redirect to another crate, or to puzzle at their Google results wondering which of several ZonedDateTime results is the one that is closest to JS's/Java's ZDT.

Also, for developers who are looking for i18n, having something in the type name that's clearly formatting-related will also help with discovery.

I guess I don't understand the reason why we'd want to call it ZonedDateTime. The only advantage I can think of is to increase traffic to the ICU4C ZDT-formatting type to help drive developer awareness that time-zone-aware formatting is possible in Rust. But I think we get almost all of that benefit from a type called ZonedDateTimeFormatter, without the downsides. What am I missing?

@sffc
Copy link
Member

sffc commented Oct 4, 2024

Well, we have already been shipping Date, Time, and DateTime since 2021. ZonedDateTime is simply the natural extension of that. Users of ICU4X will probably be looking for a type with that name, I assume.

I'm not aware of another high-level Rust ecosystem type that plans to support all of Temporal ZonedDateTime. There is temporal_rs, but it is intended as a low-level crate. Users who don't need non-Gregorian calendars can use the existing jiff::Zoned or chrono::DateTime.

Maybe eventually ICU4X ZonedDateTime will support everything; that was originally my idea, but now that we've figured out ways to decouple the type from the TZDB, it seems nice to keep it that way.

A "formatter" is something that owns formatting data and maps "formattable inputs" to "formatted outputs". ZonedDateTime is not a formatter because it doesn't own formatting data. It is a formattable input to a formatter.

@sffc
Copy link
Member

sffc commented Oct 4, 2024

Just to highlight this in case it's not obvious: everything we're talking about is namespaced.

  • icu::calendar::Date
  • icu::calendar::Time
  • icu::calendar::DateTime
  • icu::timezone::ZonedDateTime

So there's no ambiguity that you are using ICU4X's "ZonedDateTime", which is self-evidently the i18n-forward type. Even if you put the imports at the top of your file, you are still being explicit when you import the type.

One direction I'm not necessarily opposed to is emphasizing the namespace in the type name: IntlZonedDateTime, for instance. That's mostly a novel concept for us, though.

@sffc
Copy link
Member

sffc commented Oct 4, 2024

My strong opinion is that we should not use ZonedDateTime for a type that only provides a partial (formatting only) implementation of ZDT's features in JS and Java.

ICU4X's ZonedDateTime supports more than just formatting; it also supports IXDTF and calendar conversions. It's only "add 1 month" type operations that it doesn't support yet, and that's extremely obvious: there is no method for it.

@sffc
Copy link
Member

sffc commented Oct 4, 2024

Just to list the bikeshed names in one place:

  • ZonedDateTime (currently Shane's preference, opposed by Justin)
  • ZonedDateTimeFormatter (currently Justin's preference, opposed by Shane)
  • FormattableZonedDateTime
  • IntlZonedDateTime
  • ZonedDateTimeInfo
  • DateTimeZone
  • Date_Time_Zone

@justingrant
Copy link

it also supports IXDTF and calendar conversions

Other than arithmetic, are there any other gaps between Temporal.ZonedDateTime and ICU4X's corresponding type? Don't need every method, mostly just trying to understand the use cases covered and not covered.

Maybe eventually ICU4X ZonedDateTime will support everything; that was originally my idea, but now that we've figured out ways to decouple the type from the TZDB, it seems nice to keep it that way.

Could you explain this a bit? Why is it nice? I'm not suggesting this is a bad choice, just trying to understand better.

Also, for callers who want to implement arithmetic for non-Gregorian calendars, is there a Rust crate that does this today? If not, what's the plan for arithmetic for these calendars?

In Temporal, we handle this issue by allowing ZDTs to be created with either location-based or offset-only zones, but we don't allow parsing ISO 8601/RFC 3339 strings into a ZonedDateTime.

That's a good point and I didn't think of that before. I think this new model forces us to at least have a different parse function for each of the three time zone types. We can name them like:

  • ZonedDateTime::try_from_str for the full strings (mixed offset and location)
  • ZonedDateTime::try_from_offset_only_str for an offset-only time zone
  • ZonedDateTime::try_from_location_only_str for a location-only time zone

What would be the output of these parsing methods if parsing succeeds? A ZDT instance?

  • ZonedDateTime::try_from_offset_only_str for an offset-only time zone

This is the one I'd be a bit skeptical about, because wouldn't it effectively foreclose a future ability to add arithmetic cleanly?

Another choice could be to create a separate OffsetDateTime type (which is what Java does) which IMO makes the data model much more obvious and discoverable.

This also might make it clearer to callers that there's a difference between a time zone (something that can be used to derive other ZDTs and localize into a wide range of formats, including the most familiar formats for most end users like "Eastern Standard Time") and an offset (something that can be used to parse an Instant string into its constituent parts and localize into limited set of formats that exclude familiar formats that most end-users prefer)

IMO one of the best decisions we made in Temporal was to align type naming with data model. Would it be good to do this in ICU4X too? Or are traits the idiomatic way to do this in Rust?

  • ZonedDateTime::try_from_location_only_str for a location-only time zone

Unlike for offsets where it's not possible to determine the location time zone from an offset, it *is* possible to determine an offset from a location-only time zone. (Using a disambiguation strategy, either hardcoded like Date uses or an optional disambiguator like Temporal uses.). Would ICU4X provide a way to calculate the offset ergonomically?

@robertbastian
Copy link
Member Author

robertbastian commented Oct 11, 2024

Another model:

struct TimeZone(TimeZoneBcp47Id); 
// "[Europe/Zurich]" -> TimeZone("chzrh")
// "[Future/Zone]" -> TimeZone("unk")

struct CalculatedTimeZone(TimeZoneBcp47Id, UtcOffset); 
// "+01:00[Europe/Zurich]" -> CalculatedTimeZone("chzrh", +1)
// "+01:00[Future/Zone]" -> CalculatedTimeZone("unk", +1)
// "+01:00" -> CalculatedTimeZone("unk", +1)

struct Metazone(Option<MetazoneId>, TimeZone);
// TimeZone("chzrh") + timestamp --MetazoneCalculator--> Metazone(Some("cet"), TimeZone("chzrh"))
// TimeZone("unk") + timestamp --MetazoneCalculator-> Metazone(None, TimeZone("unk"))

struct CalculatedMetazone(Option<MetazoneId>, ZoneVariant, CalculatedTimeZone);
// CalculatedTimeZone("chzrh", +1) + timestamp --MetazoneCalculator--> CalculatedMetazone(Some("cet"), Standard, TimeZone("chzrh"))
// CalculatedTimeZone("unk", +1) + timestamp --MetazoneCalculator--> CalculatedMetazone(None, Standard, TimeZone("unk"))

The tzdb can be used to go from TimeZone + timestamp to CalculatedTimeZone, although it's not something we're currently planning on supporting in ICU4X.

@robertbastian
Copy link
Member Author

Hmm actually the ZoneVariant is returned by tzdb calculations (e.g. https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html#method.to_offset), so maybe it should be part of the CalculatedTimeZone. The IXDTF parser would then require the ZoneOffsetPeriodV1 data.

@sffc
Copy link
Member

sffc commented Oct 12, 2024

Of the 4 fields, all are ones the user may or may not have by the time they get to ICU4X, with some more likely than others. Some example input sets:

  • Offset only
  • Time zone only
  • Time zone and offset (eg IXDTF)
  • Time zone, offset, and variant (eg when TZDB is used)
  • Metazone only (less common; ok to not support in the MVP but I'd like a pathway to add it)
  • Metazone and variant (same exceptions as above)

Any anything that says "time zone" could also be an unknown future time zone.

The output sets, after processing with CLDR data and timestamp, are

  • Offset only
  • Time zone and metazone
  • Everything
  • Metazone only (see note above)
  • Metazone and variant (see note above)

(Even ignoring the metazone inputs, it seems that there are enough weird combinations that the simplistic model that CustomTimeZone currently supports might be just fine? Maybe with some improvements to ergonomics and making it harder for fields to drift out of sync.)

@robertbastian
Copy link
Member Author

Time Zone Focus Meeting

Key Questions:

  1. Current state of ICU4X time zone
    • IXDTF parsing
    • IANA <-> BCP47 mapping
    • Formatting (almost full UTS-35)
      • 2024-07-02T12:02:00+0700[Asia/Tokyo] -> 12:02 Japan Time
  2. Current state of Rust ecosystem
    • Nothing in std, not even timestamp type
    • chrono, time: older ecosystem crates, types used by many Rust crates
    • jiff: more recent crate, better design imho (@robertbastian)
    • temporal-rs: tries to be a strict implementation of temporal, unlike jiff
  3. Does ICU4X offer timezone calculations?
    • ZonedDateTime <-> timestamp
    • ZonedDateTime + Duration -> ZonedDateTime
    • are we trying to be an alternative to chrono, jiff, etc.?
  4. Can ICU4X ZonedDateTime support Temporal arithmetic?
    • Bikeshedding
  5. Time zone data model: what are the input fields and the calculated fields for formatting?
  6. Are metazones exported to clients or are they fully internal?
  7. Etc/Unknown vs None time zone

References:

Does ICU4X offer timezone calculations?

Pros and cons of including TZDB:

  • Pro:
    • Easiest for clients, probably
    • ICU4X data provider integration
  • Con:
    • A lot of maintenance burden
    • Scope creep
    • Risks clients including multiple TZDB from different crates
    • Reimplementing the wheel
    • Time zone calculations and formatting happen in different parts of an app

Discussion:

  • @justingrant discusses the clients of the different crates in the ecosystem (icu vs jiff)
  • @justingrant points out that it would be nice to avoid crates duplicating work, because this stuff is complicated
  • @robertbastian This doesn't close the door to us adding more in the future.

@sffc's Proposal:

  • ICU4X: Calendrical calculations (ISO to calendar and back), calendar (PlainDate) arithmetic, formatting (time+date+zone). This is the status quo.
  • temporal_rs: everything not included in ICU4X that Temporal requires. Depends on ICU4X. Could depend on Jiff.
  • jiff: whatever it wants to do, Rust ecosystem focus. No other dependencies. Mostly an independent project, but glue code / ergonomic wrappers can be provided.

Approval: @robertbastian, @sffc, @nekevss @justingrant @Manishearth

Can ICU4X ZonedDateTime support Temporal arithmetic?

All agree, not in ICU4X for now. temporal_rs can do it.

Bikeshedding of ZonedDateTime type

  • @sffc - should our types be called FormattableZonedDateTime to point out that it doesn't support arithmetic?
  • @robertbastian All our types support arithmetic just not ZonedDateTime
  • @Manishearth It seems OK that our ZonedDateTime type is just missing the advanced API
  • @justingrant It's not clear for Rust developers if they find a ZonedDateTime type that it's an incomplete type.
  • @Manishearth - this assumes users are explicitly looking for temporal types, a lot of other languages are using the ZonedDateTime name and offer different functionality. I'm not opposed to ICU in the future supporting calculations on this type, and for now we can link to temporal.rs.
  • @sffc Is it OK that we are using Date, Time, and DateTime instead of PlainDate, PlainTime, and PlainDateTime?
  • @Manishearth I see these as being stacked on top of each other.
  • @sffc I agree; and if we focus on our narrow scope, and not being a one-stop shop for Rust developers needing datetime types, I think the bar is different and we don't need to focus as much on that contituency.
  • @robertbastian Or we could just not have a DateTime type and have format_date_time take a Date and Time as separate arguments? Our DateTime type doesn't really add any value other than being an input time to the formatter. Could even do that for ZonedDateTime, ie. date, time, and time zone in formatting.

Conclusion: OK to keep the names as they are in ICU4X. Create an issue to remove the DateTime type.

Time zone data model: what are the input fields and the calculated fields for formatting?

Status quo: CustomTimeZone is a struct you build with the information you know. Four fields in CustomTimeZone:

  • offset = Option<UtcOffset>
  • time_zone_id = Option<TimeZoneBcp47Id>
  • metazone_id = Option<MetazoneId>
  • zone_variant = Option<ZoneVariant>

@robertbastian Proposal:

One single type, TimeZone

  • time_zone_id = Option<TimeZoneBcp47Id>
  • offset = Option<UtcOffset>
  • zone_variant = Option<ZoneVariant>

Or make the time zone ID non-optional and use Etc/Unknown

  • time_zone_id = TimeZoneBcp47Id
  • offset = Option<UtcOffset>
  • zone_variant = Option<ZoneVariant>

@sffc: I'm happy with a single type, but maybe make an enum of the 3 valid cases:

enum TimeZoneInput {
    OffsetOnly(UtcOffset),
    LocationOnly(TimeZoneBcp47Id, Option<ZoneVariant>),
    Full(UtcOffset, TimeZoneBcp47Id, Option<ZoneVariant>),
}

@justingrant: This seems OK but just needs clear docs about how you get from the common input types to these structs.

@sffc
Copy link
Member

sffc commented Oct 15, 2024

Some more discussion with @sffc and @robertbastian

pub struct TimeZoneInfo {
    /// An ID or Etc/Unknown
    pub id: TimeZoneBcp47Id,
    /// The offset from UTC, if known
    pub offset: Option<UtcOffset>,
    /// The daylight/standard variant, if known
    pub variant: Option<ZoneVariant>,
    /// The local datetime for name lookup calculations, if known
    pub local_time: Option<(Date<Iso>, Time)>,
}

pub(crate) struct ResolvedTimeZone {
    /// From input
    pub id: TimeZoneBcp47Id,
    /// From input
    pub offset: Option<UtcOffset>,
    /// From input, or calculated if not present
    pub variant: ZoneVariant,
    /// Calculated (None if there is no metazone)
    pub metazone_id: Option<MetazoneId>,
} // actually not needed if we go from TimeZoneInfo to ExtractedInput directly

pub(crate) struct ExtractedInput {
    // ...
    /// None if input type doesn't have time zone
    pub id: Option<TimeZoneBcp47Id>,
    /// None if input type doesn't have time zone
    /// Some(None) if there is a time zone but offset is not known
    pub offset: Option<Option<UtcOffset>>,
    /// None if input type has neither the variant nor the 3-tuple time zone, local time, and offset
    pub variant: Option<ZoneVariant>,
    /// None if input type doesn't have time zone or local time
    /// Some(None) if there is a time zone but the time zone has no metazone at the local time
    pub metazone_id: Option<Option<MetazoneId>>,
}

Error cases:

  • ResolvedTimeZone with GenericNonLocation:
    • Required Fields: ID, Metazone
    • Missing Name: OK, fall back
    • Missing Metazone Input: OK, fall back
  • ResolvedTimeZone with SpecificNonLocation:
    • Required Fields: ID, Metazone, Variant
    • Missing Name: OK, fall back
    • Missing Metazone Input: OK, fall back
    • (Variant is non-optional)
  • ResolvedTimeZone with SpecificLocation:
    • Required Fields: ID, Variant
    • Missing Name: OK, fall back
    • (Variant and ID are non-optional)
  • ResolvedTimeZone with Location or BCP-47:
    • Required Fields: ID
    • Missing Name: OK, fall back
  • ResolvedTimeZone with Offset or ISO:
    • Required Fields: Offset
    • Missing Offset: Error case, render GMT+?

@robertbastian: the fields required should be for the whole fallback chain, because otherwise you get data-driven exceptions

@robertbastian: Location and Offset don't need a ResolvedTimeZone; they could accept TimeZoneInfo. The other three formats should check for missing inputs if we do this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-datetime Component: datetime, calendars, time zones C-time-zone Component: Time Zones discuss-priority Discuss at the next ICU4X meeting S-medium Size: Less than a week (larger bug fix or enhancement)
Projects
None yet
Development

No branches or pull requests

5 participants