Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/libs/DistanceRequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,25 @@ function isDistanceAmountWithinLimit(distance: number, rate: number): boolean {
* Normalize odometer text by standardizing locale digits and stripping all
* non-numeric characters except the decimal point. fromLocaleDigit converts
* each locale character to its standard equivalent (e.g. German ',' → '.'
* for decimal, German '.' → ',' for group separator), then we keep only
* digits and the standard decimal point.
* for decimal, German '.' → ',' for group separator), so after conversion
* dots are always decimals and commas are always group separators.
* We then strip everything except digits and the standard decimal point.
*/
function normalizeOdometerText(text: string, fromLocaleDigit: (char: string) => string): string {
const standardized = replaceAllDigits(text, fromLocaleDigit);
return standardized.replaceAll(/[^0-9.]/g, '');
const stripped = standardized.replaceAll(/[^0-9.]/g, '');
// Remove redundant leading zeroes (e.g. "007" → "7", "000" → "0") but
// keep a single zero before a decimal point (e.g. "0.5" stays "0.5").
return stripped.replace(/^0+(?=\d)/, '');
}

/**
* Prepare odometer input text for display by removing non-numeric characters
* (except the decimal point, comma, and space — which serve as group or
* decimal separators depending on locale) and stripping redundant leading zeroes.
*/
function prepareTextForDisplay(text: string): string {
return text.replaceAll(/[^0-9., ]/g, '').replace(/^0+(?=\d)/, '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for allowing space? It's causing weird bug

Screen.Recording.2026-03-19.at.1.40.13.AM.mov

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes some locale uses blank space as group separator(eg. fr).

And for the multiple spaces grouped together I mentioned the slack thread -

Note: I have ignored the cases of multiple group separators grouped together(ie. 12,,3.4) as code would turn ugly and no real gain.

Copy link
Contributor

@ChavdaSachin ChavdaSachin Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further, please refer to the QA tests for better understanding of the locale support.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok not blocker then

}

export default {
Expand All @@ -503,6 +516,7 @@ export default {
getRateForExpenseDisplay,
isDistanceAmountWithinLimit,
normalizeOdometerText,
prepareTextForDisplay,
};

export type {MileageRate};
10 changes: 6 additions & 4 deletions src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,9 @@ function IOURequestStepDistanceOdometer({
if (!isOdometerInputValid(text, startReading)) {
return;
}
setStartReading(text);
startReadingRef.current = text;
const textForDisplay = DistanceRequestUtils.prepareTextForDisplay(text);
setStartReading(textForDisplay);
startReadingRef.current = textForDisplay;
if (formError) {
setFormError('');
}
Expand All @@ -319,8 +320,9 @@ function IOURequestStepDistanceOdometer({
if (!isOdometerInputValid(text, endReading)) {
return;
}
setEndReading(text);
endReadingRef.current = text;
const textForDisplay = DistanceRequestUtils.prepareTextForDisplay(text);
setEndReading(textForDisplay);
endReadingRef.current = textForDisplay;
if (formError) {
setFormError('');
}
Expand Down
102 changes: 101 additions & 1 deletion tests/unit/OdometerNormalizationTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('normalizeOdometerText', () => {
expect(DistanceRequestUtils.normalizeOdometerText('1,5', germanFromLocaleDigit)).toBe('1.5');
});

it("German '1.5' (fifteen, dot is group separator) normalizes to '15'", () => {
it("German '1.5' (dot is group separator, fifteen) normalizes to '15'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('1.5', germanFromLocaleDigit)).toBe('15');
});

Expand Down Expand Up @@ -37,4 +37,104 @@ describe('normalizeOdometerText', () => {
expect(DistanceRequestUtils.normalizeOdometerText('9999999', englishFromLocaleDigit)).toBe('9999999');
});
});

describe('locale-strict decimal handling', () => {
const englishFromLocaleDigit = (char: string) => fromLocaleDigit('en', char);
const germanFromLocaleDigit = (char: string) => fromLocaleDigit('de', char);

it("English '123,4' (comma is group separator, not decimal) normalizes to '1234'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('123,4', englishFromLocaleDigit)).toBe('1234');
});

it("English '1,234.5' (comma as group with dot decimal) normalizes to '1234.5'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('1,234.5', englishFromLocaleDigit)).toBe('1234.5');
});

it("German '1.234,5' (dot-group, comma-decimal) normalizes to '1234.5'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('1.234,5', germanFromLocaleDigit)).toBe('1234.5');
});

it("plain comma with identity function: ',5' normalizes to '5' (comma stripped)", () => {
const identity = (char: string) => char;
expect(DistanceRequestUtils.normalizeOdometerText(',5', identity)).toBe('5');
});
});

describe('leading zeroes', () => {
const identity = (char: string) => char;

it("strips redundant leading zeroes: '000' → '0'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('000', identity)).toBe('0');
});

it("strips leading zeroes before digits: '007' → '7'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('007', identity)).toBe('7');
});

it("keeps single zero before decimal: '0.5' → '0.5'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('0.5', identity)).toBe('0.5');
});

it("normalizes multiple zeroes before decimal: '00.5' → '0.5'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('00.5', identity)).toBe('0.5');
});

it("keeps a single zero: '0' → '0'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('0', identity)).toBe('0');
});

it("does not affect numbers without leading zeroes: '123' → '123'", () => {
expect(DistanceRequestUtils.normalizeOdometerText('123', identity)).toBe('123');
});
});
});

describe('prepareTextForDisplay', () => {
it("strips non-numeric characters except decimal and comma: 'abc123.4def' → '123.4'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('abc123.4def')).toBe('123.4');
});

it("keeps commas for locale display: '1,234.5' → '1,234.5'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('1,234.5')).toBe('1,234.5');
});

it("strips leading zeroes: '007' → '7'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('007')).toBe('7');
});

it("strips redundant leading zeroes: '000' → '0'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('000')).toBe('0');
});

it("keeps single zero before decimal: '0.5' → '0.5'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('0.5')).toBe('0.5');
});

it("normalizes multiple leading zeroes before decimal: '00.5' → '0.5'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('00.5')).toBe('0.5');
});

it("keeps single zero: '0' → '0'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('0')).toBe('0');
});

it("strips letters and symbols: '@#$%123' → '123'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('@#$%123')).toBe('123');
});

it("handles empty string: '' → ''", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('')).toBe('');
});

it("handles valid number unchanged: '12345.6' → '12345.6'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('12345.6')).toBe('12345.6');
});

it("keeps spaces as group separator (French locale): '1 234,5' → '1 234,5'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('1 234,5')).toBe('1 234,5');
});

it("keeps periods as group separator (Italian locale): '1.234,5' → '1.234,5'", () => {
expect(DistanceRequestUtils.prepareTextForDisplay('1.234,5')).toBe('1.234,5');
});
});
Loading