my fork of the bluesky client

Fix inconsistent number formatting between mobile and web (#6384)

* Manual truncation & identify factor points for each lang

* Reduce indirection

* Add test

Co-authored-by: khuddite <biliie811028@hotmail.com>

* Handle big numbers, clarify special case

* Clarify the reason

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>

authored by

Khuddite
khuddite
Dan Abramov
and committed by
GitHub
e4284744 4dd62e24

+133 -6
+92
src/view/com/util/numeric/__tests__/format-test.ts
··· 1 + import {describe, expect, it} from '@jest/globals' 2 + 3 + import {APP_LANGUAGES} from '#/locale/languages' 4 + import {formatCount} from '../format' 5 + 6 + const formatCountRound = (locale: string, num: number) => { 7 + const options: Intl.NumberFormatOptions = { 8 + notation: 'compact', 9 + maximumFractionDigits: 1, 10 + } 11 + return new Intl.NumberFormat(locale, options).format(num) 12 + } 13 + 14 + const formatCountTrunc = (locale: string, num: number) => { 15 + const options: Intl.NumberFormatOptions = { 16 + notation: 'compact', 17 + maximumFractionDigits: 1, 18 + // @ts-ignore 19 + roundingMode: 'trunc', 20 + } 21 + return new Intl.NumberFormat(locale, options).format(num) 22 + } 23 + 24 + // prettier-ignore 25 + const testNums = [ 26 + 1, 27 + 5, 28 + 9, 29 + 11, 30 + 55, 31 + 99, 32 + 111, 33 + 555, 34 + 999, 35 + 1111, 36 + 5555, 37 + 9999, 38 + 11111, 39 + 55555, 40 + 99999, 41 + 111111, 42 + 555555, 43 + 999999, 44 + 1111111, 45 + 5555555, 46 + 9999999, 47 + 11111111, 48 + 55555555, 49 + 99999999, 50 + 111111111, 51 + 555555555, 52 + 999999999, 53 + 1111111111, 54 + 5555555555, 55 + 9999999999, 56 + 11111111111, 57 + 55555555555, 58 + 99999999999, 59 + 111111111111, 60 + 555555555555, 61 + 999999999999, 62 + 1111111111111, 63 + 5555555555555, 64 + 9999999999999, 65 + 11111111111111, 66 + 55555555555555, 67 + 99999999999999, 68 + 111111111111111, 69 + 555555555555555, 70 + 999999999999999, 71 + 1111111111111111, 72 + 5555555555555555, 73 + ] 74 + 75 + describe('formatCount', () => { 76 + for (const appLanguage of APP_LANGUAGES) { 77 + const locale = appLanguage.code2 78 + it('truncates for ' + locale, () => { 79 + const mockI8nn = { 80 + locale, 81 + number(num: number) { 82 + return formatCountRound(locale, num) 83 + }, 84 + } 85 + for (const num of testNums) { 86 + const formatManual = formatCount(mockI8nn as any, num) 87 + const formatOriginal = formatCountTrunc(locale, num) 88 + expect(formatManual).toEqual(formatOriginal) 89 + } 90 + }) 91 + } 92 + })
+41 -6
src/view/com/util/numeric/format.ts
··· 1 - import type {I18n} from '@lingui/core' 1 + import {I18n} from '@lingui/core' 2 + 3 + const truncateRounding = (num: number, factors: Array<number>): number => { 4 + for (let i = factors.length - 1; i >= 0; i--) { 5 + let factor = factors[i] 6 + if (num >= 10 ** factor) { 7 + if (factor === 10) { 8 + // CA and ES abruptly jump from "9999,9 M" to "10 mil M" 9 + factor-- 10 + } 11 + const precision = 1 12 + const divisor = 10 ** (factor - precision) 13 + return Math.floor(num / divisor) * divisor 14 + } 15 + } 16 + return num 17 + } 18 + 19 + const koFactors = [3, 4, 8, 12] 20 + const hiFactors = [3, 5, 7, 9, 11, 13] 21 + const esCaFactors = [3, 6, 10, 12] 22 + const itDeFactors = [6, 9, 12] 23 + const jaZhFactors = [4, 8, 12] 24 + const restFactors = [3, 6, 9, 12] 2 25 3 26 export const formatCount = (i18n: I18n, num: number) => { 4 - return i18n.number(num, { 27 + const locale = i18n.locale 28 + let truncatedNum: number 29 + if (locale === 'hi') { 30 + truncatedNum = truncateRounding(num, hiFactors) 31 + } else if (locale === 'ko') { 32 + truncatedNum = truncateRounding(num, koFactors) 33 + } else if (locale === 'es' || locale === 'ca') { 34 + truncatedNum = truncateRounding(num, esCaFactors) 35 + } else if (locale === 'ja' || locale === 'zh-CN' || locale === 'zh-TW') { 36 + truncatedNum = truncateRounding(num, jaZhFactors) 37 + } else if (locale === 'it' || locale === 'de') { 38 + truncatedNum = truncateRounding(num, itDeFactors) 39 + } else { 40 + truncatedNum = truncateRounding(num, restFactors) 41 + } 42 + return i18n.number(truncatedNum, { 5 43 notation: 'compact', 6 44 maximumFractionDigits: 1, 7 - // `1,953` shouldn't be rounded up to 2k, it should be truncated. 8 - // @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet 9 - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode 10 - roundingMode: 'trunc', 45 + // Ideally we'd use roundingMode: 'trunc' but it isn't supported on RN. 11 46 }) 12 47 }