Skip to content

Commit bd10366

Browse files
committed
Address CodeRabbit feedback
1 parent 24b0a32 commit bd10366

2 files changed

Lines changed: 96 additions & 23 deletions

File tree

‎packages/react-core/src/helpers/__tests__/util.test.ts‎

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,64 @@ test('formatBreakpointMods', () => {
114114
expect(formatBreakpointMods({ default: 'column', lg: 'row' }, styles)).toEqual('pf-m-column pf-m-row-on-lg');
115115
});
116116

117-
test('parseLocalizedDecimal accepts dot and comma decimal separators', () => {
117+
test('parseLocalizedDecimal accepts locale-formatted decimals', () => {
118118
const enFormatter = new Intl.NumberFormat('en');
119119
const esFormatter = new Intl.NumberFormat('es');
120+
const deFormatter = new Intl.NumberFormat('de-DE');
120121

121122
expect(parseLocalizedDecimal('50.2', enFormatter)).toBe(50.2);
122123
expect(parseLocalizedDecimal('50,2', esFormatter)).toBe(50.2);
123-
expect(parseLocalizedDecimal('1.234,56', esFormatter)).toBe(1234.56);
124+
expect(parseLocalizedDecimal('12.345,67', esFormatter)).toBe(12345.67);
124125
expect(parseLocalizedDecimal('1,234.56', enFormatter)).toBe(1234.56);
126+
expect(parseLocalizedDecimal('1.234,56', deFormatter)).toBe(1234.56);
125127
expect(parseLocalizedDecimal('', enFormatter)).toBeNaN();
126128
});
127129

130+
test('parseLocalizedDecimal rejects mismatched separators', () => {
131+
const enFormatter = new Intl.NumberFormat('en-US');
132+
const deFormatter = new Intl.NumberFormat('de-DE');
133+
const deNoGrouping = new Intl.NumberFormat('de-DE', { useGrouping: false });
134+
135+
// Comma used as decimal in en-US would otherwise silently merge to 502
136+
expect(parseLocalizedDecimal('50,2', enFormatter)).toBeNaN();
137+
// Dot used as decimal in de-DE would otherwise silently merge to 502
138+
expect(parseLocalizedDecimal('50.2', deFormatter)).toBeNaN();
139+
// Mixed separators for de-DE
140+
expect(parseLocalizedDecimal('1,234.56', deFormatter)).toBeNaN();
141+
// Without a reported group separator, alternate '.' must not be stripped/merged
142+
expect(parseLocalizedDecimal('1.234,56', deNoGrouping)).toBeNaN();
143+
expect(parseLocalizedDecimal('50,2', deNoGrouping)).toBe(50.2);
144+
});
145+
146+
test('parseLocalizedDecimal accepts locale-specific grouping patterns', () => {
147+
const enINFormatter = new Intl.NumberFormat('en-IN');
148+
const enUSFormatter = new Intl.NumberFormat('en-US');
149+
const esFormatter = new Intl.NumberFormat('es');
150+
151+
expect(parseLocalizedDecimal('12,34,567', enINFormatter)).toBe(1234567);
152+
expect(parseLocalizedDecimal('12,345', enINFormatter)).toBe(12345);
153+
expect(parseLocalizedDecimal('1,234', enINFormatter)).toBe(1234);
154+
expect(parseLocalizedDecimal('1,23,4567', enINFormatter)).toBeNaN();
155+
expect(parseLocalizedDecimal('50,2', enUSFormatter)).toBeNaN();
156+
expect(parseLocalizedDecimal('1.234,56', esFormatter)).toBeNaN();
157+
});
158+
159+
test('parseLocalizedDecimal rejects malformed numeric tokens', () => {
160+
const enFormatter = new Intl.NumberFormat('en-US');
161+
162+
expect(parseLocalizedDecimal('12abc', enFormatter)).toBeNaN();
163+
expect(parseLocalizedDecimal('1,2abc', enFormatter)).toBeNaN();
164+
expect(parseLocalizedDecimal('12-3', enFormatter)).toBeNaN();
165+
expect(parseLocalizedDecimal('--12', enFormatter)).toBeNaN();
166+
expect(parseLocalizedDecimal('+-12', enFormatter)).toBeNaN();
167+
expect(parseLocalizedDecimal('12.3.4', enFormatter)).toBeNaN();
168+
expect(parseLocalizedDecimal('abc', enFormatter)).toBeNaN();
169+
170+
expect(parseLocalizedDecimal('-50.2', enFormatter)).toBe(-50.2);
171+
expect(parseLocalizedDecimal('+12', enFormatter)).toBe(12);
172+
expect(parseLocalizedDecimal('1,234.56', enFormatter)).toBe(1234.56);
173+
});
174+
128175
test('formatLocalizedDecimal uses locale decimal separator', () => {
129176
expect(formatLocalizedDecimal(50.2, new Intl.NumberFormat('en-US'))).toBe('50.2');
130177
expect(formatLocalizedDecimal(50.2, new Intl.NumberFormat('de-DE'))).toBe('50,2');

‎packages/react-core/src/helpers/util.ts‎

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,8 @@ export const getInlineStartProperty = (
592592
};
593593

594594
/**
595-
* Parses a decimal input string, accepting both comma and dot as the decimal separator.
596-
* Given the locale, it discovers the locale's separators and integers using a reference value.
595+
* Parses a decimal input string using the given formatter's locale separators.
596+
* Grouping is removed only when formatToParts reports a group separator.
597597
*
598598
* @param {string} value - The input string to parse
599599
* @param {Intl.NumberFormat} formatter - The Intl.NumberFormat instance to use for formatting
@@ -605,31 +605,57 @@ export const parseLocalizedDecimal = (value: string, formatter: Intl.NumberForma
605605
}
606606

607607
const parts = formatter.formatToParts(12345.6);
608-
const groupSymbol = parts.find((p) => p.type === 'group')?.value || ',';
608+
// Only strip grouping when the formatter actually reports a group separator.
609+
// Never fall back to ',' — that can match comma-decimal locales and eat the decimal.
610+
const groupSymbol = parts.find((p) => p.type === 'group')?.value;
609611
const decimalSymbol = parts.find((p) => p.type === 'decimal')?.value || '.';
612+
const normalizedString = value.trim();
610613

611-
// in case of non-Arabic numerals
612-
const digitParts = formatter.formatToParts(1234567890);
613-
const localDigits = digitParts
614-
.filter((p) => p.type === 'integer')
615-
.map((p) => p.value)
616-
.join('');
617-
let normalizedString = value;
618-
if (localDigits.length === 10 && localDigits !== '1234567890') {
619-
const standardDigits = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
620-
const digitMap = new Map();
621-
[...localDigits].forEach((char, idx) => digitMap.set(char, standardDigits[idx]));
622-
normalizedString = [...value].map((char) => digitMap.get(char) || char).join('');
614+
// Reject '.' / ',' when they are neither this locale's decimal nor its reported group separator
615+
// (e.g. mixed-separator de-DE input like "1,234.56").
616+
for (const sep of ['.', ','] as const) {
617+
if (sep !== decimalSymbol && sep !== groupSymbol && normalizedString.includes(sep)) {
618+
return NaN;
619+
}
623620
}
624621

625-
const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
622+
const decimalIndex = normalizedString.indexOf(decimalSymbol);
623+
if (decimalIndex !== -1 && normalizedString.indexOf(decimalSymbol, decimalIndex + decimalSymbol.length) !== -1) {
624+
return NaN;
625+
}
626+
627+
let intPart = decimalIndex === -1 ? normalizedString : normalizedString.slice(0, decimalIndex);
628+
const fracPart = decimalIndex === -1 ? undefined : normalizedString.slice(decimalIndex + decimalSymbol.length);
629+
630+
let sign = '';
631+
if (intPart.startsWith('-') || intPart.startsWith('+')) {
632+
sign = intPart[0] === '-' ? '-' : '';
633+
intPart = intPart.slice(1);
634+
}
635+
636+
if (fracPart !== undefined && groupSymbol && fracPart.includes(groupSymbol)) {
637+
return NaN;
638+
}
626639

627-
const cleaned = normalizedString
628-
.replace(new RegExp(escapeRegex(groupSymbol), 'g'), '')
629-
.replace(new RegExp(escapeRegex(decimalSymbol), 'g'), '.')
630-
.replace(/[^0-9.-]/g, '');
640+
// If grouping appears, require locale-valid groups so values like en-US "50,2" are invalid
641+
// instead of silently merging into 502, while accepting locales such as en-IN "12,34,567".
642+
if (groupSymbol && intPart.includes(groupSymbol)) {
643+
const digitsOnly = intPart.split(groupSymbol).join('');
644+
if (!/^\d+$/.test(digitsOnly) || formatter.format(Number(digitsOnly)) !== intPart) {
645+
return NaN;
646+
}
647+
intPart = digitsOnly;
648+
}
649+
650+
if (!/^\d*$/.test(intPart) || (fracPart !== undefined && !/^\d*$/.test(fracPart))) {
651+
return NaN;
652+
}
653+
654+
if (intPart === '' && (fracPart === undefined || fracPart === '')) {
655+
return NaN;
656+
}
631657

632-
return parseFloat(cleaned);
658+
return parseFloat(`${sign}${intPart || '0'}${fracPart !== undefined ? `.${fracPart}` : ''}`);
633659
};
634660

635661
/**

0 commit comments

Comments
 (0)