facebook / hermes

A JavaScript engine optimized for running React Native.
https://hermesengine.dev/
MIT License
9.9k stars 640 forks source link

Intl.DateTimeFormat does not format 2-digit hours correctly #1537

Open MrPancakes39 opened 1 month ago

MrPancakes39 commented 1 month ago

Bug Description

When using the Intl.DateTimeFormat API in JavaScript to format dates and times, there is an inconsistency in the behavior when the hour12 option is set to true or when the provided locale uses a 12-hour clock by default, in combination with the hour option set to "2-digit".

Note: This bug seems to effect all locales but I only tested the ones below:

Steps To Reproduce

  1. Use the Intl.DateTimeFormat API to format a date.
  2. Set the hour option to "2-digit".
  3. Set the hour12 option to true or provide a locale that uses a 12-hour clock by default (e.g., "en-US").
  4. Observe that single-digit hours are formatted without leading zeros.

Run the following code example:

const hour1 = new Intl.DateTimeFormat("en-US", {
  hour: "2-digit",
}).format(new Date("2024-10-30 19:00"));

console.log(hour1);

// OR

const hour2 = new Intl.DateTimeFormat("fr-FR", {
  hour: "2-digit",
  hour12: true,
}).format(new Date("2024-10-30 19:00"));

console.log(hour2);

Assert that it prints 7 PM instead of 07 PM.

The Expected Behavior

When the hour option is set to "2-digit", it is expected that the formatted hour will always have two digits, including a leading zero for single-digit hours (e.g., "01", "02", ..., "12").

Actual Behavior

When hour12 is set to true or the provided locale uses a 12-hour clock by default, the "2-digit" option for hour is ignored. The formatted hour is displayed with a single digit for single-digit hours (e.g., "1", "2", ..., "12") without a leading zero.

This bug only affects the 12-hour clock format. When using a 24-hour clock format, the "2-digit" option works as expected, displaying hours with leading zeros (e.g., "00", "01", ..., "23").

Temporary Example Workaround

function workaround(date: Date) {
  const locale = "en-US";

  const parts = new Intl.DateTimeFormat(locale, {
    hour: "2-digit",
    minute: "2-digit",
    hour12: true,
  }).formatToParts(date);

  const hourPart = parts.find((part) => part.type === "hour")!;
  const hourIn24Format = new Intl.DateTimeFormat(locale, {
    hour: "2-digit",
    hour12: false,
  })
    .formatToParts(date)
    .find((p) => p.type === "hour")!.value;
  if (hourPart.value !== hourIn24Format) {
    const tmp = new Date(date);
    tmp.setHours(tmp.getHours() % 12);
    const hour24 = new Intl.DateTimeFormat(locale, {
      hour: "2-digit",
      hour12: false,
    }).format(tmp);
    hourPart.value = hour24;
  }

  return parts.map((part) => part.value).join("");
}

const hour1 = new Intl.DateTimeFormat("en-US", {
  hour: "2-digit",
  minute: "2-digit",
  hour12: true,
}).format(new Date("2024-10-30 19:00"));
const hour2 = workaround(new Date("2024-10-30 19:00"));

console.log(hour1, hour2); // prints 7:00 PM 07:00 PM

Note: The above isn't simply padding the string with zeros because that only works in latin languages.

Output of npx react-native info:

Note: The output below is from the project I am currently working on using expo. I did created a blank new project with "react-native": "^0.75.4" (latest version ATTOW) and the issue persists.

System:
  OS: Linux 6.9 Pop!_OS 22.04 LTS
  CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  Memory: 2.24 GB / 15.49 GB
  Shell:
    version: 3.3.1
    path: /usr/bin/fish
Binaries:
  Node:
    version: 20.15.0
    path: ~/.nvm/versions/node/v20.15.0/bin/node
  Yarn:
    version: 1.22.22
    path: ~/.nvm/versions/node/v20.15.0/bin/yarn
  npm:
    version: 10.8.1
    path: ~/.nvm/versions/node/v20.15.0/bin/npm
  Watchman:
    version: 4.9.0
    path: /usr/bin/watchman
SDKs:
  Android SDK: Not Found
IDEs:
  Android Studio: AI-241.15989.150.2411.11792637
Languages:
  Java:
    version: 17.0.12
    path: /usr/bin/javac
  Ruby:
    version: 3.0.2
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.74.5
    wanted: 0.74.5
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: Not found
  newArchEnabled: false
lavenzg commented 2 weeks ago

I can reproduce it on MacOS, on which it's caused by incorrectly used platform APIs. Probably a different reason for Linux. We'll look into it.