Skip to content
Open
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
1 change: 1 addition & 0 deletions src/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const expectedNamedExports = [
'extractGetAgConfig',
'getAmountWithTax',
'getTaxValue',
'removeTrailingDecimalZeros',
];

/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
formatAmountFromString,
formatPriceUnit,
parseDecimalValue,
removeTrailingDecimalZeros,
toIntegerAmount,
addSeparatorToDineroString,
} from './money/formatters';
Expand Down
123 changes: 123 additions & 0 deletions src/money/formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
formatAmountFromString,
formatPriceUnit,
parseDecimalValue,
removeTrailingDecimalZeros,
toIntegerAmount,
unitDisplayLabels,
} from './formatters';
Expand Down Expand Up @@ -308,3 +309,125 @@ describe('parseDecimalValue', () => {
expect(parseDecimalValue(value)).toEqual(expected);
});
});

describe('removeTrailingDoubleDecimalZeros', () => {
describe('with dot decimal separator', () => {
it.each`
input | expected
${'10.00'} | ${'10'}
${'10.50'} | ${'10.50'}
${'10.0000'} | ${'10'}
${'10.1000'} | ${'10.10'}
${'10.1200'} | ${'10.12'}
${'10.0500'} | ${'10.05'}
${'0.500'} | ${'0.50'}
${'0.00'} | ${'0'}
${'123.4500'} | ${'123.45'}
${'999.9900'} | ${'999.99'}
${'1.000000'} | ${'1'}
`('should remove trailing double zeros from $input to get $expected', ({ input, expected }) => {
expect(removeTrailingDecimalZeros(input)).toBe(expected);
});
});

describe('with comma decimal separator', () => {
it.each`
input | expected
${'10,00'} | ${'10'}
${'10,50'} | ${'10,50'}
${'10,0000'} | ${'10'}
${'10,1000'} | ${'10,10'}
${'10,1200'} | ${'10,12'}
${'10,0500'} | ${'10,05'}
${'0,500'} | ${'0,50'}
${'0,00'} | ${'0'}
${'123,4500'} | ${'123,45'}
${'999,9900'} | ${'999,99'}
${'1,000000'} | ${'1'}
`('should remove trailing double zeros from $input to get $expected', ({ input, expected }) => {
expect(removeTrailingDecimalZeros(input)).toBe(expected);
});
});

describe('with currency symbols and units', () => {
it.each`
input | expected
${'10.00 €'} | ${'10 €'}
${'10.00 €/Stück'} | ${'10 €/Stück'}
${'10.1200 USD'} | ${'10.12 USD'}
${'0.00 €/kWh'} | ${'0 €/kWh'}
${'123.4500 CHF/month'} | ${'123.45 CHF/month'}
${'10,00 €'} | ${'10 €'}
${'10,00€/Stück'} | ${'10€/Stück'}
${'10,1200 USD'} | ${'10,12 USD'}
${'0,00 €/kWh'} | ${'0 €/kWh'}
${'123,4500 CHF/month'} | ${'123,45 CHF/month'}
`('should remove trailing double zeros from $input with suffix to get $expected', ({ input, expected }) => {
expect(removeTrailingDecimalZeros(input)).toBe(expected);
});
});

describe('with complex suffixes', () => {
it.each`
input | expected
${'10.00 €/Stück pro Monat'} | ${'10 €/Stück pro Monat'}
${'15.0000/unit'} | ${'15/unit'}
${'25.1200 per item'} | ${'25.12 per item'}
${'100.00€'} | ${'100€'}
${'50.0000$'} | ${'50$'}
${'10,00 €/Stück pro Monat'} | ${'10 €/Stück pro Monat'}
${'15,0000/unit'} | ${'15/unit'}
${'25,1200 per item'} | ${'25,12 per item'}
${'100,00€'} | ${'100€'}
${'50,0000$'} | ${'50$'}
`('should handle complex suffixes correctly for $input', ({ input, expected }) => {
expect(removeTrailingDecimalZeros(input)).toBe(expected);
});
});

describe('edge cases', () => {
it.each`
input | expected | description
${'10.01'} | ${'10.01'} | ${'should not modify numbers without trailing double zeros'}
${'10.10'} | ${'10.10'} | ${'should not modify single trailing zero'}
${'10'} | ${'10'} | ${'should not modify integers without decimals'}
${'10.'} | ${'10.'} | ${'should not modify numbers ending with decimal separator only'}
${'10.0'} | ${'10'} | ${'should not modify single decimal zero'}
${'abc'} | ${'abc'} | ${'should not modify non-numeric strings'}
${'10.00.00'} | ${'10.00.00'} | ${'should not modify invalid number formats'}
${''} | ${''} | ${'should handle empty strings'}
${'10.000000000'} | ${'10'} | ${'should remove multiple consecutive double zeros'}
${'10,01'} | ${'10,01'} | ${'should not modify numbers without trailing double zeros (comma)'}
${'10,10'} | ${'10,10'} | ${'should not modify single trailing zero (comma)'}
${'10,0'} | ${'10'} | ${'should not modify single decimal zero (comma)'}
`('$description: $input -> $expected', ({ input, expected }) => {
expect(removeTrailingDecimalZeros(input)).toBe(expected);
});
});

describe('preserves whitespace and formatting', () => {
it.each`
input | expected
${'10.00 €'} | ${'10 €'}
${'10.00\t€'} | ${'10\t€'}
${'10.00\n€/unit'} | ${'10\n€/unit'}
${'10,00 €'} | ${'10 €'}
${'10,00\t€'} | ${'10\t€'}
${'10,00\n€/unit'} | ${'10\n€/unit'}
`('should preserve whitespace in $input', ({ input, expected }) => {
expect(removeTrailingDecimalZeros(input)).toBe(expected);
});
});

describe('with minDecimals', () => {
it.each`
input | expected | minDecimals
${'10.1000'} | ${'10.1'} | ${0}
${'10.1000'} | ${'10.10'} | ${2}
${'10.1000'} | ${'10.100'} | ${3}
${'10.1000'} | ${'10.1000'} | ${4}
`('should remove trailing double zeros from $input to get $expected', ({ input, expected, minDecimals }) => {
expect(removeTrailingDecimalZeros(input, minDecimals)).toBe(expected);
});
});
});
46 changes: 46 additions & 0 deletions src/money/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,52 @@ export const unitDisplayLabels = {
kwp: 'kWp',
} as const;

/**
* Removes trailing double-zero groups from the decimal part of a formatted decimal amount string.
*
* - Always preserves any suffixes after the number (whitespace, newlines, currency symbols, unit names, etc.).
* - Removes the entire decimal part if all digits after the separator are zeros.
* - Keeps at least `minDecimals` decimal digits when a fractional part remains after trimming.
* - Handles both dot `.` and comma `,` as decimal separators.
*
* @param decimalAmount - The decimal amount to remove trailing zeros from.
* @param minDecimals - The minimum number of decimal digits to keep.
* @returns The decimal amount with trailing zeros removed.
*/
export const removeTrailingDecimalZeros = (decimalAmount: string, minDecimals = 2): string => {
if (decimalAmount.split(/[.,]/).length > 2) return decimalAmount;

const match = decimalAmount.match(/^(-?\d+)([.,])(\d+)(.*)$/s);
if (!match) return decimalAmount;

const [, intPart, sep, decimals, suffix] = match;

// Remove trailing double-zero pairs
let trimmed = decimals.replace(/(00)+$/, '');
const removedDoubleZeros = decimals !== trimmed;

// If nothing remains, or all remaining decimals are zeros, remove decimal entirely
if (trimmed === '' || /^0+$/.test(trimmed)) {
return intPart + suffix; // preserve suffix (spaces, newlines, currency)
}

// If minDecimals is 0, we can remove trailing single zeros as well
if (minDecimals === 0) {
trimmed = trimmed.replace(/0+$/, '');
// If all decimals were removed, don't include the decimal separator
if (trimmed === '') {
return intPart + suffix;
}
}

// Pad to minDecimals only if we removed double zeros and minDecimals > 0
if (removedDoubleZeros && minDecimals > 0 && trimmed.length < minDecimals) {
trimmed = trimmed.padEnd(minDecimals, '0');
}

return `${intPart}${sep}${trimmed}${suffix}`; // always append suffix
};

/**
* Formats built-in price units into a displayable representation. Eg. kw -> kW
*
Expand Down