Skip to content

Commit b92ede4

Browse files
authored
fix(money-input): avoid rounding errors (#229)
1 parent 6f58ca8 commit b92ede4

File tree

2 files changed

+50
-10
lines changed

2 files changed

+50
-10
lines changed

src/components/inputs/money-input/money-input.js

+23-10
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,24 @@ export const createMoneyValue = (currencyCode, rawAmount) => {
164164
const amountAsNumber = parseRawAmountToNumber(rawAmount);
165165
if (isNaN(amountAsNumber)) return null;
166166

167-
const centAmount = amountAsNumber * 10 ** currency.fractionDigits;
167+
// The cent amount is rounded to the currencie's default number
168+
// of fraction digits for prices with high precision.
169+
//
170+
// Additionally, JavaScript is sometimes incorrect when multiplying floats,
171+
// e.g. 2.49 * 100 -> 249.00000000000003
172+
// While inaccuracy from multiplying floating point numbers is a
173+
// general problem in JS, we can avoid it by cutting off all
174+
// decimals. This is possible since cents is the base unit, so we
175+
// operate on integers anyways
176+
// Also we should the round the value to ensure that we come close
177+
// to the nearest decimal value
178+
// ref: https://github.com/commercetools/merchant-center-frontend/pull/770
179+
const centAmount = Math.trunc(
180+
Math.round(amountAsNumber * 10 ** currency.fractionDigits)
181+
);
182+
168183
const fractionDigitsOfAmount =
169-
// The input will always use a dot as the separator.
184+
// The conversion to a string will always use a dot as the separator.
170185
// That means we don't have to handle a comma.
171186
String(amountAsNumber).indexOf('.') === -1
172187
? 0
@@ -176,9 +191,7 @@ export const createMoneyValue = (currencyCode, rawAmount) => {
176191
return {
177192
type: 'highPrecision',
178193
currencyCode,
179-
// For prices with high precision, the cent amount is rounded to the
180-
// currencies default number of fraction digits
181-
centAmount: parseFloat(centAmount.toFixed(0), 10),
194+
centAmount,
182195
preciseAmount: parseInt(
183196
amountAsNumber * 10 ** fractionDigitsOfAmount,
184197
10
@@ -211,13 +224,13 @@ const formatAmount = (rawAmount, currencyCode, locale) => {
211224

212225
const amount = getAmountAsNumberFromMoneyValue(moneyValue);
213226

227+
const fractionDigits = moneyValue.preciseAmount
228+
? moneyValue.fractionDigits
229+
: currencies[moneyValue.currencyCode].fractionDigits;
230+
214231
return isNaN(amount)
215232
? ''
216-
: amount.toLocaleString(locale, {
217-
minimumFractionDigits: moneyValue.preciseAmount
218-
? moneyValue.fractionDigits
219-
: currencies[moneyValue.currencyCode].fractionDigits,
220-
});
233+
: amount.toLocaleString(locale, { minimumFractionDigits: fractionDigits });
221234
};
222235

223236
const getAmountStyles = props => {

src/components/inputs/money-input/money-input.spec.js

+27
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,33 @@ describe('MoneyInput.convertToMoneyValue', () => {
152152
});
153153
});
154154

155+
describe('when called with a centPrecision price with weird JS rounding', () => {
156+
it('should treat it as a decimal separator', () => {
157+
expect(
158+
MoneyInput.convertToMoneyValue({ currencyCode: 'EUR', amount: '2.49' })
159+
).toEqual({
160+
type: 'centPrecision',
161+
currencyCode: 'EUR',
162+
centAmount: 249,
163+
fractionDigits: 2,
164+
});
165+
166+
// This test ensures that rounding is used instead of just cutting the
167+
// number of. Cutting it of would result in an incorrect 239998.
168+
expect(
169+
MoneyInput.convertToMoneyValue({
170+
currencyCode: 'EUR',
171+
amount: '2399.99',
172+
})
173+
).toEqual({
174+
type: 'centPrecision',
175+
currencyCode: 'EUR',
176+
centAmount: 239999,
177+
fractionDigits: 2,
178+
});
179+
});
180+
});
181+
155182
describe('when called with a high precision price', () => {
156183
it('should return a money value of type "highPrecision"', () => {
157184
expect(

0 commit comments

Comments
 (0)