unicode-org / icu4x

Solving i18n for client-side and resource-constrained environments.
https://icu4x.unicode.org
Other
1.33k stars 173 forks source link

Neo date formatter: Options<R> vs R::Options and same for .format #5269

Open sffc opened 1 month ago

sffc commented 1 month ago

Currently in #5248 I implemented

/// Options bag for datetime formatting.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct NeoOptions<R: DateTimeMarkers> {
    /// The desired length of the formatted string,
    /// if required for the chosen field set.
    ///
    /// See [`NeoSkeletonLength`].
    pub length: R::LengthOption,
}

where R::LengthOption is set according to the field set. It is usually NeoSkeletonLength but it is a zero-sized type for field sets that don't need the length.

I intend to add at least two more options:

  1. R::HourCycleOption for runtime hour cycle choices
  2. R::EraDisplayOption for runtime era display choices

Another approach would be to instead create separate options bags for each combinations of possible options. For example:

#[non_exhaustive]
pub struct DateWithYearOptions {
    pub length: Length,
    pub era_display: EraDisplay,
}

#[non_exhaustive]
pub struct DateWithoutYearOptions {
    pub length: Length,
}

#[non_exhaustive]
pub struct TimeOptions {
    pub length: Length,
    pub hour_cycle: HourCycle,
}

// note: same as DateWithoutYearOptions; could be combined
#[non_exhaustive]
pub struct ZoneWithRuntimeLengthOptions {
    pub length: Length,
}

#[non_exhaustive]
pub struct ZoneWithConstLengthOptions {
}

#[non_exhaustive]
pub struct DateTimeWithYearOptions {
    pub length: Length,
    pub era_display: EraDisplay,
    pub hour_cycle: HourCycle,
}

and then R::Options would choose the correct struct.

Pros and cons:


I also wanted to raise the possibility of applying a similar treatment to the format<I>(&self, input: &I) function. Currently we have a trait that looks like this:

pub trait AllInputMarkers<R: DateTimeMarkers>:
    NeoGetField<<R::D as DateInputMarkers>::YearInput>
    + NeoGetField<<R::D as DateInputMarkers>::MonthInput>
    + NeoGetField<<R::D as DateInputMarkers>::DayOfMonthInput>
    + NeoGetField<<R::D as DateInputMarkers>::DayOfWeekInput>
    + NeoGetField<<R::D as DateInputMarkers>::DayOfYearInput>
    + NeoGetField<<R::D as DateInputMarkers>::AnyCalendarKindInput>
    + NeoGetField<<R::T as TimeMarkers>::HourInput>
    + NeoGetField<<R::T as TimeMarkers>::MinuteInput>
    + NeoGetField<<R::T as TimeMarkers>::SecondInput>
    + NeoGetField<<R::T as TimeMarkers>::NanoSecondInput>
    + NeoGetField<<R::Z as ZoneMarkers>::TimeZoneInput>
where
    R::D: DateInputMarkers,
    R::T: TimeMarkers,
    R::Z: ZoneMarkers,
{
}

which is blanked-implemented on all T satisfying its bounds. We could instead have

#[non_exhaustive]
pub struct Input<R> {
    pub year: R::YearInput,
    pub month: R::MonthInput,
    // ...
}

Pros and cons:

Thoughts?

@Manishearth @robertbastian @zbraniecki

Manishearth commented 1 month ago

Discussed with Shane yesterday. In general I'm in favor of having the intermediate type with the user calling .into() or whatever, or having a single trait.

Both of these add some indirection, but they're more easily documented over a pile of traits. IMO functions like format() should spend ink documenting how they are used, without having to go too deep into "how to make the traits work". Having an intermediate type or trait gets us over the finish line with a dedicated place to describe and untangle the trait mess.

Between having an intermediate type or trait, I find that having a type tends to be less messy, and probably better for monomorphization.

sffc commented 1 month ago

In addition to hour cycle and era display, I also need an option for fractional second digits, unless that gets modeled as part of the field set.

sffc commented 1 month ago

Would like comments from @robertbastian and @zbraniecki

robertbastian commented 1 month ago

I'm in favour of R::Options for usability.

sffc commented 3 weeks ago

Discussion on constructors, also with implications on NeoOptions:

use icu::datetime::fieldsets::YMD;
use icu::datetime::DateComponents;

icu::datetime::Formatter::<YMD>::try_new(locale, options)
icu::datetime::Formatter::try_new_with_components(locale, DateComponents::YearMonth, options)

// Zibi prefers:
icu::datetime::Formatter::try_new(locale, YMD, options)

// Shane: It can also be written as:
icu::datetime::Formatter::<YMD>::try_new(locale, YMD, options)

// Robert: This would give gnarly errors
icu::datetime::Formatter::<YMDT>::try_new(locale, YMD, options)

// Robert: This actually works if the type can be inferred, i.e. it's declared in a field or argument
let formatter: Formatter<YMD> =
    icu::datetime::Formatter::try_new(locale, option)

// Shane: can we keep the old signature around?
icu::datetime::Formatter::<YMD>::try_new_bikeshed(locale, options)

// Shane: If we go with NeoOptions<R>, we could make it so that you write something like
icu::datetime::Formatter::try_new(locale, YMD.with_length(Length::Long) /* NeoOptions<YMD> */)

Constructor signature proposal:

LGTM: @Manishearth @sffc

New proposal:

LGTM: @Manishearth @sffc

Need input from @robertbastian @zbraniecki

Manishearth commented 3 weeks ago

The main thing that we discussed in the meeting after people left was the "else" block there, which proposes a way to decouple the ctor discussion from the options discussion, however it constrains the options discussion by giving DateOptions (etc) a generic in the "we have five options types" model

sffc commented 3 weeks ago

I want to explore the field set being the options type.

We would get call sites like this:

icu::datetime::Formatter::try_new(locale, YMD::with_length(Length::Medium))

The function signature is

impl Formatter<FSet> {
    pub fn try_new(Preferences, FSet)
}

And the field set would be defined like this (most likely expanded from a macro)

#[non_exhaustive]
pub struct YMD {
    pub length: Length,
    pub era_display: EraDisplay,
}

For runtime components, it would be

#[non_exhaustive]
pub struct DateSkeleton {
    pub field_set: DateFieldSet,
    pub length: Length,
    pub era_display: EraDisplay,
}

Assuming it works, I can agree to this.

robertbastian commented 3 weeks ago

This would produce good documentation, so I'm onboard.