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

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

Open
paulirish opened this issue May 15, 2024 · 9 comments
Open

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

paulirish opened this issue May 15, 2024 · 9 comments

Comments

@paulirish
Copy link

paulirish commented May 15, 2024

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
Copy link
Member

@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
Copy link
Author

paulirish commented May 15, 2024

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
Copy link
Contributor

sffc commented May 15, 2024

+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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Author

@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
Copy link
Contributor

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:

  • There is no BC/BCE era. The year before ISO year 1 is ISO year 0, and the year before that is ISO year -1.
  • The ISO calendar uses a specific week-numbering scheme commonly used in Europe.

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:

  • Learn that Temporal supports (and their code might be used with) many calendars like Chinese, Islamic, or Hebrew.
  • Learn about the ergonomic alternative methods that put ISO at the end of those calls when writing code that only uses the ISO calendar.

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
Copy link
Contributor

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
Copy link
Contributor

sffc commented May 21, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants