tc39 / proposal-intl-duration-format

https://tc39.es/proposal-intl-duration-format
MIT License
165 stars 18 forks source link

Is there a way to just format a duration into a user friendly string with this? #174

Closed danieltroger closed 11 months ago

danieltroger commented 11 months ago

I think a lot of developers will have the following issue: we're tasked with implementing where you can see how long ago something was.

In my current use case I want to say "The last sync occurred 2 minutes and 35 seconds ago". I discovered this API and got delighted because I always prefer using browser native technologies over bloated libraries but unless I'm missing something this API seems to be only for advanced use cases and not for just writing out the difference between two dates?

So I have one Date object, the current Date, and one Date object of the last sync. I want to get the duration between them.

I tried doing new Intl.DurationFormat("en", { style: "short" }).format({milliseconds: +new Date - new Date("2023-11-02T10:28:07.749Z")}); but it gives me "764,387 ms" which is not what the user expects. Is there another API to split up a duration into seconds/hours/days that I haven't found yet?

I think it's very important for adoption that you also consider the simple use cases and not just make something super advanced

sffc commented 11 months ago

Previously discussed here: https://github.com/tc39/proposal-intl-duration-format/issues/29#issuecomment-682110757

@FrankYFTang made a good observation:

Intl.DurationFormat is chartered to format the "amount" of time, not a "particular time in the timeline" ... A duration itself is a "amount of time" , without any reference point of start or end.

https://github.com/tc39/proposal-intl-duration-format/issues/29#issuecomment-760504931

To get the behavior you are looking for, please use Intl.RelativeTimeFormat, which has been stable since 2018.

danieltroger commented 11 months ago

@sffc thanks for the reply but excuse my ignorance, the difference to Intl.RelativeTimeFormat seems to be that it adds "ago"? That fits my use case better, but why is there no way to break down the provided duration into years/weeks/days/minutes/hours/seconds/whatever automatically?

If I do

new Intl.RelativeTimeFormat("en").format(
    Math.round((+new Date("2023-11-02T10:28:07.749Z") - +new Date()) / 1000),
    "second"
);

I get 21,712 seconds ago while I'd expect 6 hours, 1 minute, 52 seconds ago. Is there an API for that?

sffc commented 11 months ago

RelativeTimeFormat has some limitations

  1. It does not yet support Temporal.Duration inputs (since Temporal.Duration is not fully landed yet)
  2. It does not yet support mixed units (minutes+seconds, days+hours, etc)

https://github.com/tc39/ecma402/issues/498 tracks a feature improvement to RelativeTimeFormat to add these features. Please upvote that issue to help it get prioritized. In the meantime, you can use RelativeTimeFormat with a single unit at a time. Thanks!

FrankYFTang commented 11 months ago

It seems a good idea to champion a new proposal for Intl.RelativeTimeFormat v2 to enhance it for such a feature. I can also see additional different use case to use a different reference time for Intl.RelativeTimeFormat , such as "2 days after December 7, 1941" or "5 days, 23 hours, 54 minutes, and 41 seconds after April 11, 1970, 19:13:00 UTC"

danieltroger commented 11 months ago

you can use RelativeTimeFormat with a single unit at a time. Thanks!

I tried this but it doesn't really work, because every time one calls RelativeTimeFormat with a unit one gets "ago" again. So it will output 5 days ago, 10 hours ago, 20 minutes ago, 50 seconds ago whereas one would say 5 days, 10 hours, 20 minutes, 50 seconds ago

Example code:

const secondsInAMinute = 60;
const secondsInAnHour = secondsInAMinute * 60;
const secondsInADay = secondsInAnHour * 24;

function howLongAgo(date: Date, now: Date): string {
  const intlObject = new Intl.RelativeTimeFormat("en");
  const output: string[] = [];
  let seconds = Math.round((+now - +date) / 1000);

  const days = Math.floor(seconds / secondsInADay);
  seconds %= secondsInADay; // Remainder after subtracting days

  const hours = Math.floor(seconds / secondsInAnHour);
  seconds %= secondsInAnHour; // Remainder after subtracting hours

  const minutes = Math.floor(seconds / secondsInAMinute);
  seconds %= secondsInAMinute; // Remainder after subtracting minutes

  if (days > 0) output.push(intlObject.format(-days, "day"));
  if (hours > 0) output.push(intlObject.format(-hours, "hour"));
  if (minutes > 0) output.push(intlObject.format(-minutes, "minute"));
  if (seconds > 0 || output.length === 0) output.push(intlObject.format(-seconds, "second"));

  return output.join(", ");
}
sffc commented 11 months ago

When I said "you can use RelativeTimeFormat with a single unit at a time", I meant that currently you should just choose which unit you want. It doesn't make sense to glue multiple outputs together (which, by the way, should be using ListFormat, not the code you wrote above).

danieltroger commented 11 months ago

😅 ah yes, let's tell my users they last saved 3,453 minutes ago lol. Really looking forward to in a couple years when this is improved. Thanks for clarifying about ListFormat though, updated my code. Now it says "8 minutes ago and 32 seconds ago" which I guess is more localised so I'll keep it.

Updated code ```ts function formatWithINTL(date: Date, now: Date, locale: string): string { const intlObject = new Intl.RelativeTimeFormat(locale); const listFormatter = new Intl.ListFormat(locale, { style: "long", type: "conjunction" }); const timeComponents: string[] = []; let seconds = Math.round((+now - +date) / 1000); const days = Math.floor(seconds / secondsInADay); seconds %= secondsInADay; // Remainder after subtracting days const hours = Math.floor(seconds / secondsInAnHour); seconds %= secondsInAnHour; // Remainder after subtracting hours const minutes = Math.floor(seconds / secondsInAMinute); seconds %= secondsInAMinute; // Remainder after subtracting minutes if (days > 0) timeComponents.push(intlObject.format(-days, "day")); if (hours > 0) timeComponents.push(intlObject.format(-hours, "hour")); if (minutes > 0) timeComponents.push(intlObject.format(-minutes, "minute")); if (seconds > 0 || timeComponents.length === 0) timeComponents.push(intlObject.format(-seconds, "second")); return listFormatter.format(timeComponents); } ```
sffc commented 9 months ago

I believe the thinking is that relative time format has generally been considered to be an approximation of time, so a single unit is sufficient. You say "X seconds ago" until you get to 60 seconds and then you switch to "X minutes ago" until 60 minutes and then "X hours ago", etc. I believe the thinking was that if you need more precision then you should just format the timestamp directly instead of through RelativeTimeFormat.

danieltroger commented 9 months ago

Hmm, that's an interesting take. Would certainly be a solution, but I feel like especially between "1 hour and 1 minute ago" and "one hour and 59 minutes ago" there's quite a difference that users probably would like to know about, without having to read a full date and time printout

michaelficarra commented 9 months ago

there's quite a difference that users probably would like to know about, without having to read a full date and time printout

You may be right, but that's not how many popular UIs display relative times today. See the dates on the comments on this very site. Same goes with Twitter(-alikes), popular blogging/CMS platforms, email clients, etc. They just display approximations rounded to the largest unit and people seem okay enough with it. Note that it's also common to display the precise timestamp on hover with these UIs.

danieltroger commented 9 months ago

Yeah I'm already showing the precise time on hover but it takes some mental effort to parse. But I think you're right, it's probably enough with just one unit. However, then why does Intl not select the right unit for me?

michaelficarra commented 9 months ago

@danieltroger You have a good point. That's a common need and we should make sure it's easy. You should open an issue for that.

danieltroger commented 9 months ago

@michaelficarra if this issue isn't enough, would this repo be the correct one?

michaelficarra commented 9 months ago

~Yes, this repo would be the correct one,~ and a new issue focused on that more pointed topic would be best, in my opinion.

Oops, I misspoke. The correct repo would be either the relative time format repo or https://github.com/tc39/ecma402.