(Note: I'm accepting Kooilnc's answer since it was correct and shared a helpful library to help format dates in a more predictable way. This is meant to add some more detailed findings about and Date
and localization.)
DateTimeFormat
options work in mysterious ways
The first thing I missed when asking this question is that I misunderstood DateTimeFormat
options. The day
, hour
, minute
and second
options accept the following values (example for the day option):
day
The representation of the day. Possible values are:
"numeric"
(e.g., 1)
"2-digit"
(e.g., 01)
As a result, I assumed that to format minutes and seconds without a leading 0 for minutes, the following would work (snippet copied from the question):
const date = new Date(Date.now());
date.setMinutes(9,9); // so we're sure to catch leading zeros
const timeOptions = {
minute: "numeric", // without leading zero
second: "2-digit", // with leading zero
}
const dateString = date.toLocaleTimeString("en-US", timeOptions)
// ❌ I expect "9:09", I get "09:09"
console.log(dateString);
This does not work, and that's because these properties (day
, hour
, minute
, second
) don't behave consistently, especially when grouped in specific subsets.
For example:
const date = new Date(Date.now());
date.setHours(9,9,9); // so we're sure to catch leading zeros
// consistent options
const minuteAndSecond = {
minute: "numeric",
second: "2-digit",
}
const hourAndMinute = {
hour: "numeric",
minute: "2-digit",
}
// inconsistent formatting (even in the same locale)
console.log(date.toLocaleTimeString("en-US", minuteAndSecond)); // 09:09
console.log(date.toLocaleTimeString("en-US", hourAndMinute)); // 9:09 AM
So as Kooilnc wrote, while this can seem inconsistent, this is just how DateTimeFormat
options work.
A side note about locale
When I stumbled on the similar SO question that I linked to, I was surprised to see that the accepted solution wasn't working for me:
const date = new Date(Date.now());
date.setHours(9,9); // so we're sure to catch leading zeros
const timeOptions = {
hour: "numeric", // without leading zero
minute: "2-digit", // with leading zero
}
const dateString = date.toLocaleTimeString([], timeOptions)
// the question said this would be "9:09 AM", I was getting "09:09"
console.log(dateString);
I had missed a couple important things:
- first, as mentioned above, options for formatting hours and minutes don't behave like options for formatting minutes and seconds. So the other question's solution doesn't apply to my question
- in the other question's case, the reason that I was getting a leading zero was indeed because of locale. The snippet passes
[]
as Date.toLocaleTimeString()
's first parameter, so the browser uses the default locale. I won't go into details here, but the tl;dr is: my browser's locale resolved to en-DE
(not en-US
), therefore I was seeing hours and minutes formatted differently. Iff you're interested, I did a few formatting experiments in a CodeSandbox.
So: locale is an important factor for formatting hours and minutes, but it didn't affect my case of formatting minutes and seconds (although it might have, with yet other locales).
In closing (where do we go from here)
I've personally decided to not remove the leading zero from the formatted time after all. Attempting to force a localized time string to look a certain way seems at odds with the concept of localization itself.
My recommendation to others would be to treat the output of Date.toLocaleDateTime()
like a random string (i.e. don't try to predict its format) and to trust the browser to format it in a way that makes the most sense for users. If you absolutely need to format a Date
in a specific way, you're probably better off using something like Date.toISOString()
(which outputs a predictable ISO format), and transforming the result at will.