flex-template-web/src/util/currency.js
Kimmo Puputti 22b0aa512b Don't throw error when dealing with other currencies
The app can handle many currencies, and changing the default currency
shouldn't break the Styleguide.
2017-11-28 15:14:29 +02:00

257 lines
8.2 KiB
JavaScript

import { has, trimEnd } from 'lodash';
import Decimal from 'decimal.js';
import { types } from './sdkLoader';
import { subUnitDivisors } from './currencyConfig';
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER
// https://stackoverflow.com/questions/26380364/why-is-number-max-safe-integer-9-007-199-254-740-991-and-not-9-007-199-254-740-9
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -1 * (2 ** 53 - 1);
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 2 ** 53 - 1;
export const isSafeNumber = decimalValue => {
if (!(decimalValue instanceof Decimal)) {
throw new Error('Value must be a Decimal');
}
return decimalValue.gte(MIN_SAFE_INTEGER) && decimalValue.lte(MAX_SAFE_INTEGER);
};
// Get the minor unit divisor for the given currency
export const unitDivisor = currency => {
if (!has(subUnitDivisors, currency)) {
throw new Error(`No minor unit divisor defined for currency: ${currency}`);
}
return subUnitDivisors[currency];
};
////////// Currency manipulation in string format //////////
/**
* Ensures that the given string uses only dots or commas
* e.g. ensureSeparator('9999999,99', false) // => '9999999.99'
*
* @param {String} str - string to be formatted
*
* @return {String} converted string
*/
export const ensureSeparator = (str, useComma = false) => {
if (typeof str !== 'string') {
throw new TypeError('Parameter must be a string');
}
return useComma ? str.replace(/\./g, ',') : str.replace(/,/g, '.');
};
/**
* Ensures that the given string uses only dots
* (e.g. JavaScript floats use dots)
*
* @param {String} str - string to be formatted
*
* @return {String} converted string
*/
export const ensureDotSeparator = str => {
return ensureSeparator(str, false);
};
/**
* Convert string to Decimal object (from Decimal.js math library)
* Handles both dots and commas as decimal separators
*
* @param {String} str - string to be converted
*
* @return {Decimal} numeral value
*/
export const convertToDecimal = str => {
const dotFormattedStr = ensureDotSeparator(str);
return new Decimal(dotFormattedStr);
};
/**
* Converts Decimal value to a string (from Decimal.js math library)
*
* @param {Decimal|Number|String} decimalValue
*
* @param {boolean} useComma - optional.
* Specify if return value should use comma as separator
*
* @return {String} converted value
*/
export const convertDecimalToString = (decimalValue, useComma = false) => {
const d = new Decimal(decimalValue);
return ensureSeparator(d.toString(), useComma);
};
// Divisor can be positive value given as Decimal, Number, or String
const convertDivisorToDecimal = divisor => {
try {
const divisorAsDecimal = new Decimal(divisor);
if (divisorAsDecimal.isNegative()) {
throw new Error(`Parameter (${divisor}) must be a positive number.`);
}
return divisorAsDecimal;
} catch (e) {
throw new Error(`Parameter (${divisor}) must present a number.`, e);
}
};
/**
* Limits value to sub-unit precision: "1.4567" -> "1.45"
* Useful in input fields so this doesn't use rounding.
*
* @param {String} inputString - positive number presentation.
*
* @param {Decimal|Number|String} subUnitDivisor - should be something that can be converted to
* Decimal. (This is a ratio between currency's main unit and sub units.)
*
* @param {boolean} useComma - optional.
* Specify if return value should use comma as separator
*
* @return {String} truncated value
*/
export const truncateToSubUnitPrecision = (inputString, subUnitDivisor, useComma = false) => {
const subUnitDivisorAsDecimal = convertDivisorToDecimal(subUnitDivisor);
// '10,' should be passed through, but that format is not supported as valid number
const trimmed = trimEnd(inputString, useComma ? ',' : '.');
// create another instance and check if value is convertable
const value = convertToDecimal(trimmed, useComma);
if (value.isNegative()) {
throw new Error(`Parameter (${inputString}) must be a positive number.`);
}
// Amount is always counted in subunits
// E.g. $10 => 1000¢
const amount = value.times(subUnitDivisorAsDecimal);
if (!isSafeNumber(amount)) {
throw new Error(
`Cannot represent money minor unit value ${amount.toString()} safely as a number`
);
}
// Amount must be integer
// We don't deal with subunit fragments like 1000.345¢
if (amount.isInteger()) {
// accepted strings: '9', '9,' '9.' '9,99'
const decimalCount2 = value.toFixed(2);
const decimalPrecisionMax2 =
decimalCount2.length >= inputString.length ? inputString : value.toFixed(2);
return ensureSeparator(decimalPrecisionMax2, useComma);
} else {
// truncate strings ('9.999' => '9.99')
const truncated = amount.truncated().dividedBy(subUnitDivisorAsDecimal);
return convertDecimalToString(truncated, useComma);
}
};
////////// Currency - Money helpers //////////
/**
* Converts given value to sub unit value and returns it as a number
*
* @param {Number|String} value
*
* @param {Decimal|Number|String} subUnitDivisor - should be something that can be converted to
* Decimal. (This is a ratio between currency's main unit and sub units.)
*
* @param {boolean} useComma - optional.
* Specify if return value should use comma as separator
*
* @return {number} converted value
*/
export const convertUnitToSubUnit = (value, subUnitDivisor, useComma = false) => {
const subUnitDivisorAsDecimal = convertDivisorToDecimal(subUnitDivisor);
if (!(typeof value === 'string' || typeof value === 'number')) {
throw new TypeError('Value must be either number or string');
}
const val = typeof value === 'string' ? convertToDecimal(value, useComma) : new Decimal(value);
const amount = val.times(subUnitDivisorAsDecimal);
if (!isSafeNumber(amount)) {
throw new Error(
`Cannot represent money minor unit value ${amount.toString()} safely as a number`
);
} else if (amount.isInteger()) {
return amount.toNumber();
} else {
throw new Error(`value must divisible by ${subUnitDivisor}`);
}
};
const isNumber = value => {
return typeof value === 'number' && !isNaN(value);
};
/* eslint-disable no-underscore-dangle */
// Detect if the given value is a goog.math.Long object
// See: https://google.github.io/closure-library/api/goog.math.Long.html
const isGoogleMathLong = value => {
return typeof value === 'object' && isNumber(value.low_) && isNumber(value.high_);
};
/* eslint-enable no-underscore-dangle */
/**
* Convert Money to a number
*
* @param {Money} value
*
* @return {Number} converted value
*/
export const convertMoneyToNumber = value => {
if (!(value instanceof types.Money)) {
throw new Error('Value must be a Money type');
}
const subUnitDivisorAsDecimal = convertDivisorToDecimal(unitDivisor(value.currency));
let amount;
if (isGoogleMathLong(value.amount)) {
// TODO: temporarily also handle goog.math.Long values created by
// the Transit tooling in the Sharetribe JS SDK. This should be
// removed when the value.amount will be a proper Decimal type.
// eslint-disable-next-line no-console
console.warn('goog.math.Long value in money amount:', value.amount, value.amount.toString());
amount = new Decimal(value.amount.toString());
} else {
amount = new Decimal(value.amount);
}
if (!isSafeNumber(amount)) {
throw new Error(
`Cannot represent money minor unit value ${amount.toString()} safely as a number`
);
}
return amount.dividedBy(subUnitDivisorAsDecimal).toNumber();
};
/**
* Format the given money to a string
*
* @param {Object} intl
* @param {Money} value
*
* @return {String} formatted money value
*/
export const formatMoney = (intl, value) => {
if (!(value instanceof types.Money)) {
throw new Error('Value must be a Money type');
}
const valueAsNumber = convertMoneyToNumber(value);
// See: https://github.com/yahoo/react-intl/wiki/API#formatnumber
const numberFormatOptions = {
style: 'currency',
currency: value.currency,
currencyDisplay: 'symbol',
useGrouping: true,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
};
return intl.formatNumber(valueAsNumber, numberFormatOptions);
};