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

Options for smallestUnit/largestUnit and hideZeroValued #32

Closed
sffc opened this issue Aug 24, 2020 · 80 comments
Closed

Options for smallestUnit/largestUnit and hideZeroValued #32

sffc opened this issue Aug 24, 2020 · 80 comments
Assignees
Labels
Meeting Discussion Need to be discussed in one of the upcoming meetings Under discussion units
Milestone

Comments

@sffc
Copy link
Collaborator

sffc commented Aug 24, 2020

We discussed several semantics here.

  1. smallestUnit/largestUnit in Temporal.Duration.prototype.round for arithmetic; smallestUnit in Intl.DurationFormat for controlling the display of zero-valued fields.
duration.round({ smallestUnit: "seconds", largestUnit: "hours" }).toLocaleString({ smallestUnit: "seconds" })
  1. smallestUnit/largestUnit in Intl.DurationFormat; implicitly call Duration rounding function. No separate hideZeroValued option.
duration.toLocaleString({ smallestUnit: "seconds", largestUnit: "hours" })
  1. smallestUnit/largestUnit in Intl.DurationFormat; implicitly call Duration rounding function. Include hideZeroValued option.
duration.toLocaleString({ smallestUnit: "seconds", largestUnit: "hours", hideZeroValued: "leading" })
  1. smallestUnit/largestUnit in Temporal.Duration.prototype.round for arithmetic; requiredFields in Intl.DurationFormat for controlling the display of zero-valued fields.
duration.round({ smallestUnit: "seconds", largestUnit: "hours" }).toLocaleString({ requiredFields: ["minutes", "seconds"] })
  1. smallestUnit/largestUnit in Temporal.Duration.prototype.round for arithmetic; Include hideZeroValued option.
duration.round({ smallestUnit: "seconds", largestUnit: "hours" }).toLocaleString({ hideZeroValued: "all" })
  1. smallestUnit/largestUnit in Temporal.Duration.prototype.round for arithmetic; Include hideZeroValued option.
duration.round({ smallestUnit: "seconds", largestUnit: "hours" }).toLocaleString({ hideZeroValued: "all" })

It would be good to have a list of use cases, and how those use cases would look in each of these options.

@younies
Copy link
Member

younies commented Aug 24, 2020

@younies ... TODO: add all the use cases and how this will be affected with each option.

@younies
Copy link
Member

younies commented Aug 27, 2020

For Option 1:
the users could not hide the zero values in the middle or in the beginning or ... etc.

For example: 2 hours, 0 minutes and 31 seconds could not be 2 hours and 31 seconds

For Option 2
the same as option 1

For Option 3
The users have a full control on how the zero-valued should be treated.

@sffc
Copy link
Collaborator Author

sffc commented Aug 28, 2020

Is the hiding of interior zeros the only case that options 1 and 2 are unable to handle?

@sffc
Copy link
Collaborator Author

sffc commented Sep 10, 2020

Can you please post specific code examples of operations that you can do with options 3 and 4 that you can't do with options 1 and 2?

@younies
Copy link
Member

younies commented Jan 8, 2021

Option 5:

duration.round({ smallestUnit: "seconds", largestUnit: "hours" }).toLocaleString({ hideZeroValue: "all" })

@younies
Copy link
Member

younies commented Jan 14, 2021

Can you please post specific code examples of operations that you can do with options 3 and 4 that you can't do with options 1 and 2?

I do not think there is a case that could done by round and could not done by toLocaleString or vs versa. However, I believe that hideZeroValue must be added to toLocaleString() function.

@sffc
Copy link
Collaborator Author

sffc commented Jan 14, 2021

Can you please post examples with code? Show me example duration inputs and outputs, and how to produce each output given each of the four options.

@younies
Copy link
Member

younies commented Jan 14, 2021

Can you please post examples with code? Show me example duration inputs and outputs, and how to produce each output given each of the four options.

Okay, lets say duration = "1 day, 1 hour, 0 minutes and 21 seconds"
And we want to the output to be " 25 hours and 21 seconds", which contains only hours, minutes and seconds. Also, hide zero values.

- Option 1:

duration.round({ smallestUnit: "seconds", largestUnit: "hours" }).toLocaleString({ smallestUnit: "seconds" })

and the output will be

25 hours, 0 minutes and 21 seconds // not acceptable

- Option 2:

duration.toLocaleString({ smallestUnit: "seconds", largestUnit: "hours" })

the output will be

25 hours, 0 minutes and 21 seconds // not acceptable) 

- Option 3:

duration.toLocaleString({ smallestUnit: "seconds", largestUnit: "hours", hideZeroValued: "all" })

the output will be

25 hours and 21 seconds // acceptable

- Option 4:

duration.round({ smallestUnit: "seconds", largestUnit: "hours" })
                             .toLocaleString({ requiredFields: ["hours",  "seconds"] })

the output would be

25 hours and 21 seconds // acceptable

- Option 5:

duration.round({ smallestUnit: "seconds", largestUnit: "hours" }).toLocaleString({ hideZeroValue: "all" })

the output would be

25 hours and 21 minutes // acceptable 

@younies younies added Meeting Discussion Need to be discussed in one of the upcoming meetings and removed Under discussion labels Feb 2, 2021
@justingrant
Copy link

justingrant commented Feb 11, 2021

Regardless of which toLocaleString choice is made from the options above, I'd recommend that Intl.DurationFormat.prototype.format be available as an "escape hatch" where if the caller passes a plain object bag (not a Temporal.Duration instance), by default the API will format exactly the fields passed in-- no more, no less, including zeroes.

Per @sffc, the current plan is for Intl.DurationFormat.prototype.format to be a wrapper around Duration.prototype.toLocaleString. In pseudocode:

format(duration, options) {
  if (!isDuration(duration)) duration = Temporal.Duration.from(duration);
  return duration.toLocaleString(options);
}

My suggestion would be to reverse this flow and make toLocaleString into a wrapper for Intl.DurationFormat.prototype.format. Like this pseudocode:

toLocaleString(options) {
  const { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = this;
  const nonZero = Object.entries({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds })
    .filter(e => e[1]);
  const toDisplay = Object.fromEntries(nonZero.length ? nonZero : { seconds: 0 });
  return new Intl.DurationFormat(options).format(toDisplay);
}
format(duration, options) {
  if (isDuration(duration)) return duration.toLocaleString(options);
  if (typeof(duration) !== 'object') throw new TypeError('Object required');
  // format the provided object bag
}

If this is available, then the uncommon "arbitrary set of fields" case that justifies Option 4 could be handled in userland without needing to offer a requiredFields option. Like this:

const duration = Temporal.Duration.from('PT30M');
const {hours, minutes, seconds} = duration;
new Intl.DurationFormat().format({hours, minutes, seconds});
// => 0 hours, 30 minutes, 0 seconds

const nonZero = Object.entries({hours, minutes, seconds}).filter(e => e[1]);
const toDisplay = Object.fromEntries(nonZero.length ? nonZero : { seconds: 0 });
new Intl.DurationFormat().format(toDisplay);
// => 30 minutes

new Intl.DurationFormat().format({});
// => RangeError - at least one unit required

I have more feedback about the 1-5 choices above, but I'll comment on those separately.

@justingrant
Copy link

justingrant commented Feb 11, 2021

Some bikeshed notes:

Instead of verbose hideZeroValue could something shorter be used, e.g. hideZero or hideZeroes or even zero?

Also, why is it negative (hideXXX) not positive (showXxx)? I don't have a strong opinion either way, just wondering why negative was chosen.

EDIT: If we did pick zero or zeroes, then the options could be something like: 'hide' | 'show' | 'internal' | 'leading' | 'trailing' | 'auto' where auto is a default, e.g. "digital" => internal, "long" => hide

@justingrant
Copy link

justingrant commented Feb 11, 2021

OK, now finally some feedback about 1-5 options: my main concern would be confusion caused by divergent behavior between round and toLocaleString. For example: toLocaleString truncates via largestUnit while round rounds, which could cause confusing behavior when passing a balanced duration (e.g. PT59M => '59 minutes', PT1H => '0 minutes').

For this reason, my current preference would be to have toLocaleString when passed a Temporal.Duration should accept all round options and to behave exactly like round in all cases, but it would also accept an option to control zero display. I think that this is Option 3 above. If the user wants different behavior (e.g. truncation) they can use an object bag.

If the user passes an object bag and provides some options, then we'd have to walk through the cases to understand how they'd intersect. Let's discuss!

@justingrant
Copy link

justingrant commented Feb 11, 2021

Meeting 2021-02-11: Use Option 3. Here's my understanding of exactly what Option 3 is. Below that are a few open issues where I didn't know the answers. Feedback welcome!

  1. Temporal.Duration.prototype.toLocaleString and new Intl.DurationFormat() accept the same options as Temporal.Duration.prototype.round: largestUnit, smallestUnit, relativeTo, roundingIncrement, roundingMode

  2. Before displaying the output, the implementation will call round on the input duration to ensure that all nonzero units are between largestUnit and smallestUnit.

  3. ``Temporal.Duration.prototype.toLocaleStringandnew Intl.DurationFormat()` will also accept additional options:

  1. The default zero-value-display option may vary based on the style. For example, the "long" option may omit internal zeroes while the "digital" option must display internal zeroes.

  2. The defaults for largestUnit and/or smallestUnit will be the largest and/or smallest non-zero unit of the input duration, respectively.

  3. If an ISO string is passed to DurationFormat.prototype.format, it will be converted to a Temporal.Duration, then round-ed (which will be a no-op with default options), and then formatted.

  4. If a plain object (not a Duration) is passed to DurationFormat.prototype.format, then it will be converted to a Temporal.Duration, then round-ed (which will be a no-op with default options), and then formatted. See open issue below re: defaults for smallestUnit and largestUnit in this case.

ISSUES:

A) If the user passes a plain object (not a Duration), should the default for largestUnit/smallestUnit should be set to the largest/smallest field in the object, even if those fields have zero values?

  • Pro: callers are explicitly building a list of fields so we shouldn't second-guess them.
  • Con: it's inconsistent, so df.format({hours, minutes, seconds}) could return different output than df.format(Temporal.Duration.from({hours, minutes, seconds})) which may be unexpected.

B) If all units are zero, then what should be displayed? Here's a few cases to consider:

  • No options (use defaults)
  • largestUnit: 'hours'
  • smallestUnit: 'minutes'
  • style: 'digital'

C) Should round always be called on the input, even if the input has no nonzero units larger than largestUnit?

  • Yes: ensures consistent display of unbalanced durations whether or not there are out-of-range values, e.g. with largestUnit: 'hours' you'd get P1DT120M => "26 hours" and PT120M => "2 hours"
  • No: allows displaying unbalanced durations, e.g. with largestUnit: 'hours' you'd get P1DT120M => "26 hours" and PT120M => "120 minutes"

C) Should round always be called on the input, even if the input has no nonzero units smaller than smallestUnit?

  • Yes: ensures consistent display of unbalanced durations whether or not there are out-of-range values, e.g. with smallestUnit: 'seconds' you'd get PT120.2S => "2 minutes" and PT120.2S => "2 minutes"
  • No: allows displaying unbalanced durations, e.g. with smallestUnit: 'seconds' you'd get PT120.2S => "2 minutes" and PT120S => "120 seconds"

D) Should there be an option to explicitly prevent rounding, in order to allow display of unbalanced durations? This would be an alternative to choosing one behavior in (B) and (C) above.

@younies
Copy link
Member

younies commented Feb 18, 2021

consent on Option 3, the values of hideZeroValue will be determined in this issue #40

@younies younies added consensus We reached a consensus in a discussion meeting, through email or the issue discussion and removed Meeting Discussion Need to be discussed in one of the upcoming meetings labels Feb 18, 2021
@FrankYFTang
Copy link
Collaborator

so in this smallestUnit+largestUnit world, how would the "weeks" be handle?
Let's we have

new Intl.DurationFormat("en", { style: "long", smallestUnit: "days", largestUnit: "years"}).format({
    years: 1,
    weeks: 3,
    days: 2,
});

what will be the output?
"1 years, 3 weeks and 2 days"?

new Intl.DurationFormat("en", { style: "long", smallestUnit: "days", largestUnit: "years"}).format({
    years: 1,
    months: 2,
    days: 2,
});

"1 years, 2 months and 2 days"?

@FrankYFTang
Copy link
Collaborator

Is that true the decision means:

  1. remove the reading of "fields" from options in Intl.DurationFormat ( [ locales [, options ] ] )
  2. remove the output "fields" array in Intl.DurationFormat.prototype.resolvedOptions ()
  3. read "smallestUnit" from Intl.DurationFormat ( [ locales [, options ] ] )
    What is the default value for smallestUnit if it is absent? nanoseconds?
  4. read "largestUnit" from Intl.DurationFormat ( [ locales [, options ] ] )
    What is the default value for largestUnit if it is absent? years?
  5. output smallestUnit and largestUnit in Intl.DurationFormat.prototype.resolvedOptions ()

@ryzokuken
Copy link
Member

@FrankYFTang I see your point. I was wrong to assume that the constructor should only accept a specific set of values: it should accept all subsets as well, including single units. Then, it could use the best-effort algorithm you mentioned to find the appropriate pattern to use based on the actual input. Does that sounds okay to you?

I don't mind the second solution you proposed all that much either, it's inelegant but then so is everything else about "digital" but if we can get consensus around the former, I would much prefer that.

What do you folks think? I feel that we're pretty much at consensus?

@justingrant
Copy link

Could we see some code samples and expected output of what you're proposing?

@ryzokuken
Copy link
Member

// I prefer to use the example of Intl.DurationFormat , because it will help us to understand if you will
// throw exception, when will it be throw, using toLocaleString as example will hide such info.

// Example 3
let df3 = new Intl.DurationFormat('en-US', { style: 'digital', requiredUnits: [ 'hours' ] });
df3.format(new Duration('P1M2DT3H4M')); // 1m 2d 03:04

// Example 4
let df4 = new Intl.DurationFormat('en-US', { style: 'digital', requiredUnits: [ 'seconds' ] });
df4.format(new Duration('P1M2DT3H4M')); // 1m 2d 03:04:00
 
// Example 5
let df5 = new Intl.DurationFormat('en-US', { style: 'digital', requiredUnits: [ 'microseconds' ] });
df5.format(new Duration('P1M2DT3H4M')); // 1m 2d 03:04:00.000000

@FrankYFTang
Copy link
Collaborator

FrankYFTang commented Jul 12, 2021

so... based on the df3, and df4 above
df3.format(new Duration('P1M')); will output "1m 00:00" and
df3.format(new Duration('P1M0DT')); will output "1m 00:00" and
df3.format(new Duration('T1S')); will output "00:00:01", right?
df3.format(new Duration('P0Y0M0DT1S')); will output "00:00:01", right?

and
df4.format(new Duration('P1M')); will output "1m 00:00:00" and
df4.format(new Duration('P0Y1M')); will output "1m 00:00:00" and
df4.format(new Duration('P1M0D')); will output "1m 00:00:00" and
df4.format(new Duration('T1S')); will output "00:01", right?
df4.format(new Duration('T1.000000S')); will output "00:01", right?

and if I have
let df7 = new Intl.DurationFormat('en-US', { style: 'digital', requiredUnits: [ 'minute' ] });
both of the following will output "03:04"?
df7.format(new Duration('T3H4M')); // 03:04
df7.format(new Duration('P0Y0M0DT3H4M')); // 03:04
df7.format(new Duration('T3M4S')); // 03:04
df7.format(new Duration('T3M4.000000000S')); // 03:04

so both A) 3 hours 4 minutes and B) 3 minutes 4 seconds will have the same output.

Remember, once the Duration object is constructed, we can only tell a field is 0 or non zero, and we cannot tell is that field specified in the string or not.

@ryzokuken
Copy link
Member

@FrankYFTang that is true, how do we best deal with this edge case? Or should we just not and assume that the developer should be responsible here?

@FrankYFTang
Copy link
Collaborator

We could write in the spec that whne style is 'digital' if there is only requiredUnits: [ 'minute' ] is specified, it is equivelant to requiredUnits: [ 'minute', 'second' ] , right?
in that way , then

df7.format(new Duration('T3H4M')); // 03:04:00
df7.format(new Duration('P0Y0M0DT3H4M')); // 03:04:00
df7.format(new Duration('T3M4S')); // 03:04
df7.format(new Duration('T3M4.000000000S')); // 03:04

@justingrant
Copy link

We could write in the spec that whne style is 'digital' if there is only requiredUnits: [ 'minute' ] is specified, it is equivelant to requiredUnits: [ 'minute', 'second' ]

Another choice would be to require users to provide enough required units to avoid ambiguity. So requiredUnits: [ 'minute' ] would throw but requiredUnits: [ 'minute', 'second' ] or requiredUnits: [ 'hour', 'minute' ] would succeed.

I don't have an opinion about which solution is better. I'd be fine with either one.

Also, how would requiredUnits: [ 'second' ] behave?

@ryzokuken
Copy link
Member

This is exactly why I had opposed single-unit requiredUnits for "digital" fwiw.

@justingrant
Copy link

@ryzokuken - I agree that single-unit is the problem case (specifically minutes alone) but it seems OK for us to pick one of the two solutions above: either pick a default or throw. As long as the solution is well-documented, either one seems fine to me.

@ryzokuken
Copy link
Member

I don't think there is an "obvious" default here, so I prefer throwing. @FrankYFTang what do you think?

@FrankYFTang
Copy link
Collaborator

how about "00" - "59" for requiredUnits: ['second' ]
how about "00" - "59" for requiredUnits: ['minute' ]
how about "00" - "23" for requiredUnits: ['hour' ]
how about "00:00" - "59:59" for requiredUnits: ['minute' , 'second' ]
how about "00:00" - "23:59" for requiredUnits: ['hour', 'minute' ]
how about "00:00:00" - "23:59:59" for requiredUnits: ['hour', 'minute' , 'second' ]

@justingrant
Copy link

@FrankYFTang, if the input is PT2H30M, what would the output be with requiredUnits: ['minute' ] in your proposal?

If the option were units not requiredUnits then I think Frank's proposal above would be perfect, because the developer expectation would be that the only units in the output are the units in the options. But with requiredUnits the caller's expectation is that the output can (and often will be!) be a superset of the units in the options. I think it may be unexpected to only show one unit of output in this case but not in others.

Also, I've been using DateTimeFormat a lot over the last week and I notice that what we're planning for DurationFormat (a single option to determine which units to show) is different from how DateTimeFormat works (a separate option for each unit). Is this difference intentional? I admit I don't like the DateTimeFormat pattern because it's so verbose, but having them be different is also potentially confusing so I'm not sure if the ergonomic improvement is worth the inconsistency. What do you guys think?

@FrankYFTang
Copy link
Collaborator

@FrankYFTang, if the input is PT2H30M, what would the output be with requiredUnits: ['minute' ] in your proposal?

"02:30"

@justingrant
Copy link

@FrankYFTang, if the input is PT2H30M, what would the output be with requiredUnits: ['minute' ] in your proposal?

"02:30"

IMHO this seems like it'd potentially confuse callers, because PT2H30M would output "02:30" while PT30M would only output "30". Maybe if PT30M output ":30" it might be somewhat clearer. But honestly I'm nervous about how much magic happens under the covers here depending on the input. It makes me wonder (per my comment above) whether using options that are more similar to DateTimeFormat (with one option per unit, and potentially additional options for each unit) might be less ergonomic but clearer for developers. And also more consistent with DateTimeFormat, which I'd expect to be helpful because users who use DurationFormat are highly likely to use DateTimeFormat, and vice versa.

@sffc
Copy link
Collaborator Author

sffc commented Jul 16, 2021

Also, I've been using DateTimeFormat a lot over the last week and I notice that what we're planning for DurationFormat (a single option to determine which units to show) is different from how DateTimeFormat works (a separate option for each unit). Is this difference intentional? I admit I don't like the DateTimeFormat pattern because it's so verbose, but having them be different is also potentially confusing so I'm not sure if the ergonomic improvement is worth the inconsistency. What do you guys think?

Are you suggesting we do something like this?

new Intl.DurationFormat("en", {
    style: "short",
    hour: "numeric",
    minute: "2-digit",
});

@justingrant
Copy link

justingrant commented Jul 16, 2021

Are you suggesting we do something like this?

new Intl.DurationFormat("en", {
    style: "short",
    hour: "numeric",
    minute: "2-digit",
});

Yeah. I haven't thought it through enough yet, but after recently spending a lot of time with DateTimeFormat I think that aligning the two XxxFormat APIs may make sense given the highly-overlapping Venn diagram of users and use cases. Even if the resulting API isn't as ergonomic as we'd like (personally I find DateTimeFormat really verbose) there's value in consistency that may be worth some compromise in ergonomics.

I admit I don't understand the corner cases of DateTimeFormat as well as DurationFormat, so it's possible that aligning the APIs will be more trouble than it's worth. But the model of explicitly specifying which units you want to see is admittedly appealing given the odd behavior (esp. of digital) that we've been discussing here in this issue and elsewhere.

In your snippet above, does style: "short" refer to an overall style and minute: "2-digit" overrides that style?

Some quick thoughts if we want to pursue this:

  • Date units could only be 'long' (3 days) or 'short' (3d).
    • Maybe days could also be 'numeric' like in some countdown clocks, but only if hours and minutes are also included in options, otherwise days would be ambiguous and weird.
  • Time units could be: 'long' (3 hours), 'short' (3h), 'numeric' (3), or '2-digit' (03).
  • Presumably, 'numeric' hours or minutes would be upgraded to '2-digit' if there are 'numeric' units shown to the left.
  • fractionalSecondDigits could behave the same as in DateTimeFormat, except we'd maybe want to have it turned on by default because sub-second values are more common in durations (e.g. stopwatch timing) than in localized time display.
  • style: 'digital could always show minutes & seconds by default, and show larger units only if the duration had nonzero values for hours, days, etc.
    • Presumably days and larger units in digital would use the 'short' style.
    • If users want HH:MM, they can manually specify those units.
  • There's still the question of how to hide or show zeroes. One possibility would be a zeroValues: 'show' | 'showInternal' | 'hide' option:
    • The default would be 'hide'.
    • If units are manually specified, the caller can opt into showing all zeroes or just showing internal ones.
    • If units are not manually specified, then 'show' and 'showInternal' do the same thing: show internal zeroes. If the user wants to show leading or trailing zeroes, then they can manually specify the units they want to show.
  • We could offer an overflow: 'show' | 'showTrailing' | 'showLeading' | 'hide' option which would control what should happen if the user manually specifies units but the duration contains additional nonzero units outside those. Presumably the overflow units would get their styles auto-assigned.

I've just started thinking about this while writing this comment, so the suggestions above may not be workable. But there's value in consistency so it seems worth spending some time on whether aligning these two APIs could make sense.

I apologize for not suggesting this alignment sooner; I admittedly didn't really know the DateTimeFormat API very well until a week ago when I wrote a bunch of PRs using it.

@ryzokuken
Copy link
Member

I'm against this design because of the reasons I'd mentioned earlier: a DateTime is a composite value and it's fine to skip certain parts of it: it makes the output less precise but no less accurate. On the other hard, this is only true for DurationFormat if removing the least significant unit.

@sffc
Copy link
Collaborator Author

sffc commented Jul 20, 2021

(I'm not advocating for this option, but I'm writing out what it could look like)

I think the DateTimeFormat-style API would only replace requiredUnits: ["day", "hour", "minute"] with something more like { day: "short", hour: "numeric", minute: "2-digit" }. All the other decisions in this thread regarding interior zeros and such would remain the same.

Here is what the allowed values would be:

  • year, month, day, millisecond, microsecond, nanosecond: "long", "short", "narrow"
  • hour, minute, second: "long", "short", "narrow", "numeric" (and possibly "2-digit")

I would suggest that we still follow my proposal earlier where we render all nonzero fields from the duration. I see that problem as orthogonal to this one.

@sffc
Copy link
Collaborator Author

sffc commented Jul 30, 2021

@justingrant, @ryzokuken, and I had a call and discussed some more options. Here is something that came out of that discussion:

  • Each field has two options in the options bag; for example, hour and hourDisplay
  • The first field, like hour, takes style options: "long", "short", "narrow" and optionally "2-digit", "numeric"
  • The second field, like hourDisplay, takes "always", "auto", or (maybe) "never"
    • "always" = display the field at all times, even if zero
    • "auto" = display the field if and only if it is nonzero
    • "never" (maybe) = do not display the field, even if it is nonzero

The defaults for these fields could be:

  • Style: another setting in the options bag, style or defaultStyle (behaving as previously proposed)
  • Display: default = "auto", except if the style option for that field is present, in which case "always" becomes the default
    • Could also introduce a setting defaultDisplay. This would allow defaultDisplay: "never" to make a DurationFormat with behavior similar to DateTimeFormat in the sense that fields are dropped unless present in the skeleton.

By decoupling style from display, for each field in the duration, we first evaluate whether to display it, and then if we should display it, we look at what the style should be.

@justingrant
Copy link

I agree with the general framework that @sffc proposes above, mainly because it aligns with the precedent of DateTimeFormat (for easier learning/use) and seems to hand common use-cases (e.g. one option to choose digital vs. short for all units) ergonomically while also handling uncommon use cases like showing internal zero values.

Would it be possible to add a few code examples and expected output for the use cases we know about? Here's a few cases (many of which were challenging in previous iterations of this proposal):

  • P2W3DT12H - No options
  • P4M3DT12H24S - Long style, hide internal zero-valued units
  • P4M3DT12H20S - Short style, show internal zero-valued units
  • P3DT12H17M20.8912S - Digital style, show seconds and smaller units
  • P4M3DT12H17M20.89S - Digital style, hide seconds and smaller units
  • P4M3DT12H17M20.89S - Digital style for time units (hide seconds and smaller units), but use long style for all date units
  • P4M3DT12H17M20.8912S - Digital style, show only 2 sub-second digits
  • P4M3DT12H17M20.8912S - Long style for date units, narrow style for time units
  • PT12H20M30.2345678 in short style with 6 sub-second digits shown
  • PT0.12345678 as "0.1234567 seconds"
  • PT0.12345678 as "123.4567 milliseconds"
  • PT0.12345678 as something like "123 milliseconds 456 microseconds 700 nanoseconds" (BTW, I'm not convinced this is a real use case-- probably can omit it. See note below.)

A bunch of questions:

How will users toggle between showing fractional seconds (which should be the default because it's the most common case, not "3 seconds 456 milliseconds") and the less-common case of wanting to spell out trailing sub-second units? Do we even need to support the latter case? If the rule is that for sub-second units, the largest displayed second-or-smaller unit (either because it's nonzero or because of xxxDisplay)

Related: will we use fractionalSecondDigits like DateTimeFormat? If not, how will users control the number of decimals? If so, how would the user specify 4 digits for milliseconds e.g. "123.4567 milliseconds"?

Which style options are compatible with which fields? For example, I assume that '2-digit' isn't compatible with 'year'. And is this compatibility matrix dependent on locale?

What happens if the user provides a style option for a field which doesn't support that style option? Will it snap to a default value? Is there a hierarchy of fallbacks, e.g. '2-digit' falls back to 'numeric', even if 'short' is the default for that unit in that style. Will it throw?

  • The second field, like hourDisplay, takes "always", "auto", or (maybe) "never"

Love it! Is there precedent for "always"/"never" in other Intl APIs, as opposed to "show"/"hide" or some other naming?

What happens if it's a digital style with nonzero HMS and I choose minuteDisplay: 'never' ? Can styles like digital override user options? Or would this throw?

  • Could also introduce a setting defaultDisplay. This would allow defaultDisplay: "never" to make a DurationFormat with behavior similar to DateTimeFormat in the sense that fields are dropped unless present in the skeleton.

What's a "skeleton"? Is that the user's options like {minute: 'short', hour: 'short'} or the locale's defaults that come from the style option?

Would { defaultDisplay: 'always' } cause every unit to be displayed for PT0S? If not what would it show?

@ryzokuken
Copy link
Member

Thanks @sffc for posting this. Let's get some consensus around this before the meeting on thursday so we can make some progress on this!

I agree that the idea to split into options for each unit is a good one and it avoid some of the inconsistencies we've seen before, but I still think that two options per field gets a bit too much. At the same time, if the display for any unit is "never", then the style value is completely disregarded so I don't think we need that much fine-grained detail anyway.

I propose that we keep one option for each unit (eg: years, days, seconds) which can have the following values:

  • "auto": display if non-zero, style as per style option. (default)
  • "narrow": display if non-zero, "narrow" variant.
  • "short": display if non-zero, "short" variant.
  • "long": display if non-zero, "long" variant.
  • "always": always display, style as per style option.
  • "alwaysNarrow": always display, "narrow" variant.
  • "alwaysShort": always display, "narrow" variant.
  • "alwaysLong": always display, "long" variant.
  • "never": never display.

(and "2-digit", "numeric", "always2-digit" and "alwaysNumeric" for hours, minutes and seconds)

I believe strongly that this solution retains all the positive properties of the solution proposed above, while avoiding some of the ergonomic losses I mentioned and keeping the API simple, concise and the list of resolvedOptions short.

@sffc @justingrant @FrankYFTang what do you folks think? If this sounds fine, I'd post the answer to @justingrant's questions assuming this framework.

@sffc
Copy link
Collaborator Author

sffc commented Aug 2, 2021

Although the idea of combining the two options down to a single string appealed to me at one point, I think two options is better because:

  1. Most consistent with previous ECMA-402 decisions such as NumberFormat: Sign Display Syntax proposal-unified-intl-numberformat#6
  2. Easier to programmatically check the two different values via resolvedOptions

At the same time, if the display for any unit is "never", then the style value is completely disregarded so I don't think we need that much fine-grained detail anyway.

If we're concerned about this, we could resolve it by

  1. Don't allow "never" as an option (the effect can be obtained by using "auto" and manually zeroing the field before passing it to the formatter)
  2. Throw an exception when "never" is combined with an explicit setting to the corresponding style option

@sffc
Copy link
Collaborator Author

sffc commented Aug 2, 2021

Would it be possible to add a few code examples and expected output for the use cases we know about? Here's a few cases (many of which were challenging in previous iterations of this proposal):

Yes. Thanks for the detailed list of test cases. We should fill these in. I'll answer your questions first.

How will users toggle between showing fractional seconds (which should be the default because it's the most common case, not "3 seconds 456 milliseconds") and the less-common case of wanting to spell out trailing sub-second units? Do we even need to support the latter case? If the rule is that for sub-second units, the largest displayed second-or-smaller unit (either because it's nonzero or because of xxxDisplay)

Fractional seconds are being discussed in #64 and the recommendations over there are still current, not influenced by the style/display discussion in this thread.

Related: will we use fractionalSecondDigits like DateTimeFormat? If not, how will users control the number of decimals? If so, how would the user specify 4 digits for milliseconds e.g. "123.4567 milliseconds"?

That is the current proposal in #64.

Which style options are compatible with which fields? For example, I assume that '2-digit' isn't compatible with 'year'. And is this compatibility matrix dependent on locale?

I was thinking that "numeric" and "2-digit" would only be valid for hour, minute, and second (the three "digital" fields).

What happens if the user provides a style option for a field which doesn't support that style option? Will it snap to a default value? Is there a hierarchy of fallbacks, e.g. '2-digit' falls back to 'numeric', even if 'short' is the default for that unit in that style. Will it throw?

The programmer should get an exception if an invalid string is passed to an option, as it is now in all other Intl objects. For example, year: "numeric" would trigger an exception, so long as "numeric" is invalid for "year". I do not believe that additional fallbacking is necessary. We can always satisfy requests for "numeric" and "2-digit", and CLDR provides data for the three length-based styles.

Interesting case: hour: "numeric", minute: "numeric". I believe this should mean that we display 1-digit minutes if hours are hidden, and 2-digit minutes if hours are rendered. However, the resolvedOptions is "numeric" for both.

  • The second field, like hourDisplay, takes "always", "auto", or (maybe) "never"

Love it! Is there precedent for "always"/"never" in other Intl APIs, as opposed to "show"/"hide" or some other naming?

signDisplay.

What happens if it's a digital style with nonzero HMS and I choose minuteDisplay: 'never' ? Can styles like digital override user options? Or would this throw?

Let's come back to this one later. I think we may consider dropping "never" since it has other issues that @ryzokuken pointed out, and the same behavior can be obtained by using "auto" and zeroing out the field before passing it to .format().

  • Could also introduce a setting defaultDisplay. This would allow defaultDisplay: "never" to make a DurationFormat with behavior similar to DateTimeFormat in the sense that fields are dropped unless present in the skeleton.

What's a "skeleton"? Is that the user's options like {minute: 'short', hour: 'short'} or the locale's defaults that come from the style option?

The user's options. The word "skeleton" comes from ICU.

Would { defaultDisplay: 'always' } cause every unit to be displayed for PT0S? If not what would it show?

Yes; so maybe defaultDisplay isn't very useful.

@sffc
Copy link
Collaborator Author

sffc commented Aug 2, 2021

Also:

I still think that two options per field gets a bit too much

I don't see them as two options per field. I see them as a single option per field with an additional sub-setting to customize it. The sub-setting is the same for each field. On other words, what we really have is

{
    day: {
        style: "short",
        display: "always",
    }
}

But since we don't do nested options in ECMA-402, we flatten it with camel casing.

@ryzokuken
Copy link
Member

ryzokuken commented Aug 2, 2021

Okay, then since both of you feel so strongly about two options per unit and since it's not unacceptable to me (more delays, on the other hand are), I'll start to make spec changes in that direction. Let's iron out the details in the meeting tomorrow and let's get this show on the road!

@justingrant
Copy link

@sffc overall I agree with your response above. Thanks!

I was thinking that "numeric" and "2-digit" would only be valid for hour, minute, and second (the three "digital" fields).

One possible exception could be day because I've seen some clocks that show a digital style where the days unit is formatted like an hour or minute, e.g. "3:23:51:23.345678".

Fractional seconds are being discussed in #64 and the recommendations over there are still current, not influenced by the style/display discussion in this thread.

How we control whether milliseconds (and smaller) units seems fairly closely connected to how fractions are displayed. So I think it'd make sense to design both together, because if the proposals in this issue don't work for fractions then it'd be problematic.

Yes; so maybe defaultDisplay isn't very useful.

Yeah, I think I agree. The only use case I can think of for an option like this would be to control behavior of internal zeroes. Should we consider an option for that specific case instead of a more general defaultDisplay? I'd be inclined to say "no"-- if the user wants to show internal-zero units, they can manually set xxxDisplay options to 'always' for the zero units they want to display for any particular duration.

Let's come back to this one later. I think we may consider dropping "never" since it has other issues that @ryzokuken pointed out, and the same behavior can be obtained by using "auto" and zeroing out the field before passing it to .format().

One possibility could be to change the choices for xxxDisplay options from 'always' | 'auto' | 'never' to 'always' | 'auto' | 'internal' (modulo a bikeshed on the name "internal") with the latter option meaning "display zero values if they're between the smallest and largest units already being shown". This would support output like "12 days, 0 hours, and 23 minutes" and "12 days" and "23 minutes" using the same options for all three durations.

@sffc
Copy link
Collaborator Author

sffc commented Aug 3, 2021

2021-08-03 discussion:

  • Do we keep style: "digital" ?
    • @justingrant - It would be useful for ergonomics
    • @ryzokuken - But it's complicated; we should say that you should instead list out hour, minute, and second in the options bag
    • @sffc - If we had style: "digital", I think it would basically imply "short" for all fields except "numeric" for the three digital fields
    • @justingrant - I think it's useful to have a rule that fills in the defaults required for a sensible digital format
    • @ryzokuken - It's prone to error; by requiring clients to write out their option bag, bugs become programmer errors rather than spec errors
    • @sffc - I think we can make reasonable answers to how style: "digital" should behave
    • @justingrant - I think many of the potential problems with style: "digital" are problems we need to solve either way
    • @justingrant - I think we shouldn't allow combinations of options that are invalid
    • @sffc - The API should not lead people down a path where it is easy to make mistakes. This is why I generally feel style: "digital" is still a good idea; we can give reasonable defaults that handle most use cases.
  • How about PT1H1S with digital style?
    • @sffc - I think display "auto" can come with the caveat that the field is displayed if surrounded by two nonzero numeric fields

@justingrant suggested:

style: digital means {years: 'narrow', yearsDisplay: 'auto', months: 'narrow', monthsDisplay: 'auto', days: 'narrow', daysDisplay: 'auto', hours: 'numeric', minutes: 'numeric', minutesDisplay: 'always', seconds: 'numeric', secondsDisplay: 'always', }

@sffc - "numeric" means that the number gets zero-padded if there is another nonzero numeric field preceding it.

@ryzokuken
Copy link
Member

This has been updated in the latest version. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Meeting Discussion Need to be discussed in one of the upcoming meetings Under discussion units
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants