Skip to content

Commit d40e4fa

Browse files
authored
Cover all plural forms in NumberParser (#5134)
* Cover all plural forms in NumberParser
1 parent fa1b469 commit d40e4fa

File tree

2 files changed

+30
-4
lines changed

2 files changed

+30
-4
lines changed

packages/@internationalized/number/src/NumberParser.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,18 @@ class NumberParserImpl {
191191

192192
const nonLiteralParts = new Set(['decimal', 'fraction', 'integer', 'minusSign', 'plusSign', 'group']);
193193

194+
// This list is derived from https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html#comparison and includes
195+
// all unique numbers which we need to check in order to determine all the plural forms for a given locale.
196+
// See: https://github.com/adobe/react-spectrum/pull/5134/files#r1337037855 for used script
197+
const pluralNumbers = [
198+
0, 4, 2, 1, 11, 20, 3, 7, 100, 21, 0.1, 1.1
199+
];
200+
194201
function getSymbols(formatter: Intl.NumberFormat, intlOptions: Intl.ResolvedNumberFormatOptions, originalOptions: Intl.NumberFormatOptions): Symbols {
195202
// Note: some locale's don't add a group symbol until there is a ten thousands place
196203
let allParts = formatter.formatToParts(-10000.111);
197204
let posAllParts = formatter.formatToParts(10000.111);
198-
let singularParts = formatter.formatToParts(1);
205+
let pluralParts = pluralNumbers.map(n => formatter.formatToParts(n));
199206

200207
let minusSign = allParts.find(p => p.type === 'minusSign')?.value ?? '-';
201208
let plusSign = posAllParts.find(p => p.type === 'plusSign')?.value;
@@ -212,9 +219,10 @@ function getSymbols(formatter: Intl.NumberFormat, intlOptions: Intl.ResolvedNumb
212219

213220
// this set is also for a regex, it's all literals that might be in the string we want to eventually parse that
214221
// don't contribute to the numerical value
215-
let pluralLiterals = allParts.filter(p => !nonLiteralParts.has(p.type)).map(p => escapeRegex(p.value));
216-
let singularLiterals = singularParts.filter(p => !nonLiteralParts.has(p.type)).map(p => escapeRegex(p.value));
217-
let sortedLiterals = [...new Set([...singularLiterals, ...pluralLiterals])].sort((a, b) => b.length - a.length);
222+
let allPartsLiterals = allParts.filter(p => !nonLiteralParts.has(p.type)).map(p => escapeRegex(p.value));
223+
let pluralPartsLiterals = pluralParts.flatMap(p => p.filter(p => !nonLiteralParts.has(p.type)).map(p => escapeRegex(p.value)));
224+
let sortedLiterals = [...new Set([...allPartsLiterals, ...pluralPartsLiterals])].sort((a, b) => b.length - a.length);
225+
218226
let literals = sortedLiterals.length === 0 ?
219227
new RegExp('[\\p{White_Space}]', 'gu') :
220228
new RegExp(`${sortedLiterals.join('|')}|[\\p{White_Space}]`, 'gu');

packages/@internationalized/number/test/NumberParser.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ describe('NumberParser', function () {
129129
it('should return NaN for partial units', function () {
130130
expect(new NumberParser('en-US', {style: 'unit', unit: 'inch'}).parse('23.5 i')).toBe(NaN);
131131
});
132+
133+
it('should support plural forms', function () {
134+
expect(new NumberParser('en-US', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('0 years')).toBe(0);
135+
expect(new NumberParser('en-US', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('1 year')).toBe(1);
136+
expect(new NumberParser('en-US', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('2 years')).toBe(2);
137+
expect(new NumberParser('en-US', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('1.1 years')).toBe(1.1);
138+
139+
expect(new NumberParser('pl-PL', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('0 rok')).toBe(0);
140+
expect(new NumberParser('pl-PL', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('1 lat')).toBe(1);
141+
expect(new NumberParser('pl-PL', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('2 lata')).toBe(2);
142+
expect(new NumberParser('pl-PL', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('37 lata')).toBe(37);
143+
expect(new NumberParser('pl-PL', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('1.1 roku')).toBe(1.1);
144+
145+
expect(new NumberParser('fr-FR', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('1 an')).toBe(1);
146+
expect(new NumberParser('fr-FR', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('8 ans')).toBe(8);
147+
expect(new NumberParser('fr-FR', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('1,3 an')).toBe(1.3);
148+
expect(new NumberParser('fr-FR', {style: 'unit', unit: 'year', unitDisplay: 'long'}).parse('2,4 ans')).toBe(2.4);
149+
});
132150
});
133151

134152
describe('percents', function () {

0 commit comments

Comments
 (0)