diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index fa98cf32ee39..d9b2c7137e43 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -128,6 +128,7 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string * @param currency The currency associated with the rate * @param translate Translate function * @param toLocaleDigit Function to convert to localized digit + * @param useShortFormUnit If true, the unit will be returned in short form (e.g., "mi", "km"). * @returns A string that displays the rate used for expense calculation */ function getRateForDisplay( @@ -137,6 +138,7 @@ function getRateForDisplay( translate: LocaleContextProps['translate'], toLocaleDigit: LocaleContextProps['toLocaleDigit'], isOffline?: boolean, + useShortFormUnit?: boolean, ): string { if (isOffline && !rate) { return translate('iou.defaultRate'); @@ -146,11 +148,11 @@ function getRateForDisplay( } const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); - const formattedRate = PolicyUtils.getUnitRateValue(toLocaleDigit, {rate}); + const formattedRate = PolicyUtils.getUnitRateValue(toLocaleDigit, {rate}, useShortFormUnit); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const currencySymbol = CurrencyUtils.getCurrencySymbol(currency) || `${currency} `; - return `${currencySymbol}${formattedRate} / ${singularDistanceUnit}`; + return `${currencySymbol}${formattedRate} / ${useShortFormUnit ? unit : singularDistanceUnit}`; } /** @@ -159,14 +161,26 @@ function getRateForDisplay( * @param unit Unit that should be used to display the distance * @param rate Expensable amount allowed per unit * @param translate Translate function + * @param useShortFormUnit If true, the unit will be returned in short form (e.g., "mi", "km"). * @returns A string that describes the distance traveled */ -function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit | undefined, rate: number | undefined, translate: LocaleContextProps['translate']): string { +function getDistanceForDisplay( + hasRoute: boolean, + distanceInMeters: number, + unit: Unit | undefined, + rate: number | undefined, + translate: LocaleContextProps['translate'], + useShortFormUnit?: boolean, +): string { if (!hasRoute || !rate || !unit || !distanceInMeters) { return translate('iou.fieldPending'); } const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); + if (useShortFormUnit) { + return `${distanceInUnits} ${unit}`; + } + const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; @@ -197,8 +211,8 @@ function getDistanceMerchant( return translate('iou.fieldPending'); } - const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate); - const ratePerUnit = getRateForDisplay(unit, rate, currency, translate, toLocaleDigit); + const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate, true); + const ratePerUnit = getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, undefined, true); return `${distanceInUnits} @ ${ratePerUnit}`; } diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 3f3a2a96a1e1..d1a35587a501 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -137,16 +137,23 @@ function getCustomUnitRate(policy: OnyxEntry, customUnitRateID: string): return distanceUnit?.rates[customUnitRateID]; } -function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => string): string { +function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => string, withDecimals?: boolean): string { const numValue = getNumericValue(value, toLocaleDigit); if (Number.isNaN(numValue)) { return ''; } + + if (withDecimals) { + const decimalPart = numValue.toString().split('.')[1]; + const fixedDecimalPoints = decimalPart.length > 2 && !decimalPart.endsWith('0') ? 3 : 2; + return Number(numValue).toFixed(fixedDecimalPoints).toString().replace('.', toLocaleDigit('.')); + } + return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.toString().length); } -function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate?: Rate) { - return getRateDisplayValue((customUnitRate?.rate ?? 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit); +function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate?: Rate, withDecimals?: boolean) { + return getRateDisplayValue((customUnitRate?.rate ?? 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit, withDecimals); } /** @@ -993,6 +1000,7 @@ export { getTagLists, getTaxByID, getUnitRateValue, + getRateDisplayValue, goBackFromInvalidPolicy, hasAccountingConnections, hasSyncError, diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts new file mode 100644 index 000000000000..8178bb99e877 --- /dev/null +++ b/tests/unit/PolicyUtilsTest.ts @@ -0,0 +1,48 @@ +import * as PolicyUtils from '@libs/PolicyUtils'; + +function toLocaleDigitMock(dot: string): string { + return dot; +} + +describe('PolicyUtils', () => { + describe('getRateDisplayValue', () => { + it('should return an empty string for NaN', () => { + const rate = PolicyUtils.getRateDisplayValue('invalid' as unknown as number, toLocaleDigitMock); + expect(rate).toEqual(''); + }); + + describe('withDecimals = false', () => { + it('should return integer value as is', () => { + const rate = PolicyUtils.getRateDisplayValue(100, toLocaleDigitMock); + expect(rate).toEqual('100'); + }); + + it('should return non-integer value as is', () => { + const rate = PolicyUtils.getRateDisplayValue(10.5, toLocaleDigitMock); + expect(rate).toEqual('10.5'); + }); + }); + + describe('withDecimals = true', () => { + it('should return integer value with 2 trailing zeros', () => { + const rate = PolicyUtils.getRateDisplayValue(10, toLocaleDigitMock, true); + expect(rate).toEqual('10.00'); + }); + + it('should return non-integer value with up to 2 trailing zeros', () => { + const rate = PolicyUtils.getRateDisplayValue(10.5, toLocaleDigitMock, true); + expect(rate).toEqual('10.50'); + }); + + it('should return non-integer value with 3 decimals as is', () => { + const rate = PolicyUtils.getRateDisplayValue(10.531, toLocaleDigitMock, true); + expect(rate).toEqual('10.531'); + }); + + it('should return non-integer value with 3+ decimals cut to 3', () => { + const rate = PolicyUtils.getRateDisplayValue(10.531345, toLocaleDigitMock, true); + expect(rate).toEqual('10.531'); + }); + }); + }); +});