oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
74.13k stars 2.76k forks source link

Inconsistent Runtime Output for Intl.NumberFormat #6193

Open ImBIOS opened 1 year ago

ImBIOS commented 1 year ago

What version of Bun is running?

1.0.15

What platform is your computer?

Darwin 23.1.0 arm64 arm

What steps can reproduce the bug?

/**
 * Convert a number to its Indonesian Rupiah (IDR) currency format.
 * The function returns a string that represents the number formatted in IDR.
 *
 * @param {number} number - The numeric value to be formatted as IDR.
 * @returns {string} - The formatted string in IDR currency format.
 */
export function toRupiah(number: number): string {
  // Check for invalid input
  if (typeof number !== 'number' || Number.isNaN(number)) {
    throw new Error('Invalid input: The input should be a valid number.')
  }

  // Configure the Intl.NumberFormat object for IDR currency formatting
  const options: Intl.NumberFormatOptions = {
    style: 'currency',
    currency: 'IDR',
    minimumFractionDigits: 0,
  }

  // Create a new Intl.NumberFormat instance and format the number
  const formatter = new Intl.NumberFormat('id-ID', options)
  return formatter.format(number)
}
  1. Run above script with input 1000 in MacOS 14 the result => Rp1.000
  2. Run above script with input 1000 in ubuntu-latest GitHub Action environment the result => -Rp 1.000

For ease, you can automate the test using below script:

import { describe, expect, it, test } from 'bun:test'

import {
  toRupiah,
} from '@/utils/number'

describe('Number utility functions', () => {
  describe('toRupiah()', () => {
    describe.each([
      [1000, 'Rp 1.000'],
      [15000, 'Rp 15.000'],
      [1234567890, 'Rp 1.234.567.890'],
    ])('toRupiah() - %i should format to %s', (input, expected) => {
      it(`formats ${input} into Indonesian Rupiah as ${expected}`, () => {
        expect(toRupiah(input)).toEqual(expected)
      })
    })

    describe.each([
      [0, 'Rp 0'],
      [-1000, '-Rp 1.000'],
      [-15000, '-Rp 15.000'],
      [-1234567890, '-Rp 1.234.567.890'],
    ])(
      'toRupiah() - %i should handle zero and negative correctly as %s',
      (input, expected) => {
        it(`handles ${input} correctly`, () => {
          expect(toRupiah(input)).toEqual(expected)
        })
      },
    )

    it('throws an error for invalid input', () => {
      expect(() => toRupiah(NaN)).toThrow(
        'Invalid input: The input should be a valid number.',
      )
      // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
      expect(() => toRupiah(undefined as any)).toThrow(
        'Invalid input: The input should be a valid number.',
      )
    })
  })
  // ...

What is the expected behavior?

The result should be consistent so it will pass in macOS and GitHubAction ubuntu-latest.

What do you see instead?

Inconsistent output

Additional information

Minimum reproducable repo: https://github.com/ImBIOS/bun-to-rupiah-repro


  System:
    OS: macOS 14.0
    CPU: (8) arm64 Apple M2
    Memory: 238.50 MB / 8.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 16.20.2 - ~/.proto/bin/node
    npm: 8.19.4 - ~/.proto/bin/npm
    pnpm: 8.7.6 - ~/.proto/bin/pnpm
  Languages:
    Bash: 3.2.57 - /bin/bash
    Go: 1.21.0 - /usr/local/go/bin/go
    Java: 17.0.8.1 - /usr/bin/javac
    Perl: 5.30.3 - /usr/bin/perl
    PHP: 8.2.10 - /Users/ImBIOS/Library/Application Support/Herd/bin/php
    Protoc: 3.21.9 - /usr/local/bin/protoc
    Python: 3.11.5 - /opt/homebrew/opt/python@3.11/libexec/bin/python
    Python3: 3.11.5 - /opt/homebrew/bin/python3
    Ruby: 2.6.10 - /usr/bin/ruby
    Rust: 1.70.0 - /Users/ImBIOS/.cargo/bin/rustc
  npmPackages:
    @chakra-ui/anatomy: ^2.2.1 => 2.2.1 
    @chakra-ui/cli: ^2.4.1 => 2.4.1 
    @chakra-ui/icons: ^2.1.1 => 2.1.1 
    @chakra-ui/next-js: ^2.1.5 => 2.1.5 
    @chakra-ui/react: ^2.8.1 => 2.8.1 
    @chakra-ui/stepper: ^2.3.1 => 2.3.1 
    @chakra-ui/styled-system: ^2.9.1 => 2.9.1 
    @chakra-ui/theme-tools: ^2.1.1 => 2.1.1 
    @choc-ui/chakra-autocomplete: ^5.2.7 => 5.2.7 
    @emotion/react: ^11.11.1 => 11.11.1 
    @emotion/styled: ^11.11.0 => 11.11.0 
    @fontsource/roboto: ^5.0.8 => 5.0.8 
    @happy-dom/global-registrator: ^12.1.6 => 12.2.1 
    @imbios/datepicker: ^1.0.0 => 1.0.0 
    @next/bundle-analyzer: ^13.5.3 => 13.5.3 
    @next/env: ^13.5.2 => 13.5.3 
    @playwright/test: ^1.38.1 => 1.38.1 
    @sentry/nextjs: ^7.70.0 => 7.72.0 
    @testing-library/jest-dom: ^6.1.3 => 6.1.3 
    @testing-library/react: ^14.0.0 => 14.0.0 
    @testing-library/user-event: ^14.5.1 => 14.5.1 
    @types/jest: ^29.5.5 => 29.5.5 
    @types/jsonwebtoken: ^9.0.3 => 9.0.3 
    @types/lodash: ^4.14.199 => 4.14.199 
    @types/node: ^20.6.4 => 20.7.1 
    @types/react: ^18.2.22 => 18.2.23 
    @types/react-dom: ^18.2.7 => 18.2.8 
    @types/react-input-mask: ^3.0.2 => 3.0.3 
    @typescript-eslint/eslint-plugin: ^6.7.2 => 6.7.3 
    @typescript-eslint/parser: ^6.7.2 => 6.7.3 
    bun-types: ^1.0.3 => 1.0.3 
    chakra-react-select: ^4.7.2 => 4.7.2 
    date-fns: ^2.30.0 => 2.30.0 
    date-fns-tz: ^2.0.0 => 2.0.0 
    eslint: ^8.50.0 => 8.50.0 
    eslint-config-airbnb: ^19.0.4 => 19.0.4 
    eslint-config-airbnb-typescript: ^17.1.0 => 17.1.0 
    eslint-config-next: 13.5.2 => 13.5.2 
    eslint-config-prettier: ^9.0.0 => 9.0.0 
    eslint-plugin-import: ^2.28.1 => 2.28.1 
    eslint-plugin-import-helpers: ^1.3.1 => 1.3.1 
    eslint-plugin-jsx-a11y: ^6.7.1 => 6.7.1 
    eslint-plugin-react: ^7.33.2 => 7.33.2 
    eslint-plugin-simple-import-sort: ^10.0.0 => 10.0.0 
    framer-motion: ^10.16.4 => 10.16.4 
    husky: ^8.0.3 => 8.0.3 
    i18n-num-in-words: ^1.0.0 => 1.0.0 
    is-ci: ^3.0.1 => 3.0.1 
    jest: ^29.7.0 => 29.7.0 
    jest-environment-jsdom: ^29.7.0 => 29.7.0 
    jsonwebtoken: ^9.0.2 => 9.0.2 
    jspdf: ^2.5.1 => 2.5.1 
    lint-staged: ^14.0.1 => 14.0.1 
    lodash: ^4.17.21 => 4.17.21 
    next: 13.5.2 => 13.5.2 
    next-auth: ^4.23.1 => 4.23.1 
    prettier: ^3.0.3 => 3.0.3 
    react: 18.2.0 => 18.2.0 
    react-dom: 18.2.0 => 18.2.0 
    react-hook-form: ^7.46.2 => 7.46.2 
    react-input-mask: ^2.0.4 => 2.0.4 
    react-nanny: ^2.15.0 => 2.15.0 
    sharp: ^0.32.6 => 0.32.6 
    typescript: ^5.2.2 => 5.2.2 
csaunier commented 1 year ago

Isn't it related to the Unicode Common Locale Data Repository (CLDR) (https://cldr.unicode.org/) that provide a versioned package of translation used by Intl api ?

Different instance of browser / runtime / env that are versioned may also not used the same version of CLDR, that is also versioned.

I meet this issue with SSR, server version was different from browser version, but if fall into some case where react allow disabling error hydratation : https://react.dev/reference/react-dom/client/hydrateRoot#suppressing-unavoidable-hydration-mismatch-errors

Not sure how it can be fixed in your case

ImBIOS commented 1 year ago

When I'm using jest, the test is consistent in local and GitHub Action. But, when using bun:test, the test result is not consistent, the result in local is different from GitHub Action result.

ImBIOS commented 11 months ago

UPDATE: Added repro repo

ImBIOS commented 11 months ago

I proposed test here:

gp-javier commented 6 months ago

I have the same problem with this method:

function formatMoneyByLocale(
  amount: number | string,
  currency: string,
  locale: string
) {
  if (!isLocaleSupported(locale)) {
    throw new Error(`Locale "${locale}" is not supported`);
  }

  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    useGrouping: true,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(Number(amount) / 10 ** currenciesMap[currency].decimals);
}

When running this test with vitest:

  it('is formatting correctly for es-ES locale and EUR currency', () => {
    const result = formatMoneyByLocale(123456, 'EUR', 'es-ES');
    expect(result).toMatch(/1\.234,56\s€/);
  });

In macOS: ✓ is formatting correctly for es-ES locale and EUR currency

In github action: AssertionError: expected '1234,56 €' to match /1.234,56\s€/

Seems to be related with bun, because running the tests with npm in github-action is success Any solution for this?