tc39 / ecma402

Status, process, and documents for ECMA 402
https://tc39.es/ecma402/
Other
524 stars 102 forks source link

Preferred DateTimeFormat invocation to get YYYY-MM-DD formatting #891

Open paulirish opened 1 month ago

paulirish commented 1 month ago

For a developer-centric audience, I want dates formatted in YYYY-MM-DD. (But, I don't want ISO/UTC times.)

There are multiple ways to do it, but the safer and less-risky options are pretty verbose. :/


There are a handful of locales that where CLDR defines that formatting as preferred.. eg: en-CA, fr-CA, lt-LT, sv-SE

new Date().toLocaleDateString('en-CA') // '2024-05-15'

While short, it feels inappropriate to use a specific locale only because it happens to match YYYY-MM-DD. Is there much of a risk of this changing?

The more verbose option is formatToParts:

const formatter = new Intl.DateTimeFormat('en-US',{
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
});
const parts = formatter.formatToParts(date);
obj = Object.fromEntries(Array.from(parts.values()).map(v => ([v.type, v.value])));
`${obj.year}-${obj.month}-${obj.day}` // '2024-05-15'

It gets the job done (and probably won't break even if CLDR changes some fundamental en-US things), but it's not not so elegant.

And there's plenty of more abbreviated options like these, but... they do rely on the current CLDR. (So it's probably fine for the next 20 years… who knows past that. :)

const dateParts = new Date().toLocaleDateString('en-US', {
  year: 'numeric', month: '2-digit', day: '2-digit',
}).split('/');
dateParts.unshift(dateParts.pop());
dateParts.join('-'); // '2024-05-15'

What's best practice?

I'm definitely not requesting a feature to specify YYYY-MM-DD.

I'm curious how i18n experts would solve this very basic, but recurring, question. :)

rxaviers commented 1 month ago

@paulirish what's your use case? Are you looking for a "human text" (based on locale) or simply an ISO date-only format? For an ISO date-only format, I think you'd be interested on https://tc39.es/proposal-temporal/docs/plaindate.html#toString

paulirish commented 1 month ago

I have ISO timestamps and I want to format them for technical/developer humans. ;)

If the user is in PDT (as I am)...

2024-05-16T01:30:00.000Z ==> 2024-05-15

sffc commented 1 month ago

+1 on using Temporal once available.

There are also some other open issues/proposals that could make this functionality available:

Stable Formatting Proposal: https://github.com/tc39/proposal-stable-formatting

I separately want the und locale to produce ISO-8601 formats, but both of the following issues need to be fixed first:

paulirish commented 1 month ago

2024-05-16T01:30:00.000Z ==> 2024-05-15

Looks like PlainDate explicitly doesn't handle timezone adjustments.

The closest I can get with Temporal is this:

const opts = new Intl.DateTimeFormat().resolvedOptions();
Temporal.Instant.from('2024-05-16T01:30:00.000Z').toZonedDateTime(opts).toString().replace(/T.*/, '') // '2024-05-15'

Is that about right?

justingrant commented 1 month ago

Here's a Temporal-only solution. Was this what you're looking for?

Temporal.Instant.from('2024-05-16T01:30:00.000Z')
  .toZonedDateTimeISO(Temporal.Now.timeZoneId())
  .toPlainDate();
// => 2024-05-15
paulirish commented 1 month ago

@justingrant yeah that's lovely. Thank you

Shane, I'll keep an eye on the und threads. That would be such a lovely convenience.

justingrant commented 1 month ago

For anyone reading this issue in the future wondering why there's an ISO suffix on the toZonedDateTimeISO and plainDateISO functions above, it's referring to the ISO 8601 calendar (or "ISO calendar" for short). This calendar is the same as Proleptic Gregorian calendar except:

The intent of adding that ISO suffix in a few places in the Temporal API (Instant#toZonedDateTimeISO, and the zonedDateTime, plainDate, plainDateTime, and plainTime functions of Temporal.Now) was to help developers to learn that Temporal supports non-Gregorian calendars by ensuring that they encounter a speed bump of a calendarId parameter whenever they use the more-intuitive ISO-less variants of those APIs.

By requiring developers to look up the method in the docs to understand this calendarId parameter, the hope was that developers would:

This API naming decision was difficult. It was the most contentious argument in Temporal's 7+ year history. The two positions could be summarized as:

  1. Almost all people in the world rely on non-Gregorian calendars for some use cases, for example the dates of holidays like Easter, Lunar New Year, or Ramadan. We want to encourage developers to support all use cases of all people worldwide, and to write code that works for all calendars. Calendar localization should be treated no differently from language or currency localization where every language or currency is treated equally. Therefore, and every API that accepts a date input should also require a calendarId.
  2. Non-Gregorian calendars determine religious holiday dates and support other cases like Japanese government documents, but across the universe of software use cases their use is exceedingly rare. The Gregorian calendar is the primary civic calendar in almost every country on Earth. Even countries like Saudi Arabia or Israel that make heavy use of non-Gregorian calendars, it's common to present Gregorian dates side-by-side with non-Gregorian dates. This ensures that developers worldwide are familiar with the Gregorian calendar. Therefore, to simplify developer ergonomics Temporal should treat the ISO calendar as a default, and should offer intuitive API pairs like Temporal.Instant#toZonedDateTime() for the ISO calendar and Temporal.Instant#toZonedDateTimeInCalendar() for non-ISO calendars.

The current API is a compromise between these two positions: ergonomic, ISO-only APIs are available, but they require learning about and using an ISO suffix.

gibson042 commented 1 month ago

I have ISO timestamps and I want to format them for technical/developer humans. ;)

If the user is in PDT (as I am)...

2024-05-16T01:30:00.000Z ==> 2024-05-15

Before Temporal, there's the obvious but verbose

const zeroPad = (val, len) => (val + "").padStart(len, "0");
const toLocalDateString = date =>
  `${date.getFullYear()}-${zeroPad(date.getMonth() + 1, 2)}-${zeroPad(date.getDate(), 2)}`;

or the more concise and clever

const toLocalDateString = date => 
  new Date(date - date.getTimezoneOffset() * 60_000).toISOString().replace(/T.*/, "");
sffc commented 1 month ago

The intent of adding that ISO suffix in a few places in the Temporal API (Instant#toZonedDateTimeISO, and the zonedDateTime, plainDate, plainDateTime, and plainTime functions of Temporal.Now) was to help developers to learn that Temporal supports non-Gregorian calendars by ensuring that they encounter a speed bump of a calendarId parameter whenever they use the more-intuitive ISO-less variants of those APIs.

As the primary stakeholder who advocated for it, this doesn't capture my position. The intent was that every call site should be explicit when introducing a calendar system, because implicit calendars can be a source of i18n bugs. Many users have a non-Gregorian system like Buddhist, Hebrew, or Islamic in their e-calendar, and the calendar parameter is required in order to make operations such as "add 1 month" behave correctly. It wasn't about educating developers of the existence of non-Gregorian calendars; it was about preventing i18n bugs.