From e7e9a19e6eb8e4ff8903867a60c1457a8d241d0c Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Sun, 4 Dec 2022 12:08:06 +0100 Subject: [PATCH] feat: provide better support for non-decimal currencies (#309) This PR changes the way Dinero.js does formatting, providing better support for non-decimal currencies and large integers. Many changes are going in this PR, as this touches several concepts of the library. ## Currencies now accept multi-bases Currencies can have multiple bases. It's the case for the pre-decimal pound sterling, the _livre tournois_ of the French Old Regime, or the Harry Potter wizarding currency of Great Britain. Users creating custom currencies (e.g., for games) may also want to create such currencies. Until now, Dinero.js allowed you to support such currencies by providing the number of units of the smallest subdivision to the biggest (e.g., 240 pence in a pound) as the `base`. It meant that [`toUnit` could only return a double representation of the amount](https://v2.dinerojs.com/docs/api/formatting/to-unit), and that [you'd have to write a lot of logic to properly format it](https://v2.dinerojs.com/docs/guides/formatting-non-decimal-currencies#handling-currencies-with-multiple-subdivisions). Accepting multiple-bases lets users express this complexity and enables better formatting in the new `toUnits` function. ## `toUnit` is replaced with `toUnits` The `toUnit` function currently returns a double, which is problematic at several levels: - IEEE 754 doubles are unreliable, which is why Dinero.js avoids manipulating them altogether. - It makes no sense to ever return a non-decimal currency in a decimal representation. - Large integers can't be accurately casted to `number`s, so this operation can't work well with them. The decimal representation (e.g., 10.5) is prevalent in systems using a decimal currency, but this is only a human representation, and only matters when you're displaying an amount on your site or app. Programming-wise, this representation isn't particularly helpful, and it's even limiting. This PR removes `toUnit` and replaces it with `toUnits`, which returns each subdivision as an integer (a `number`, a `bigint`, a `Big`, [depending on what you use as the amount type](https://v2.dinerojs.com/docs/guides/using-different-amount-types)), in an array. This accounts for decimal and non-decimal currencies, without forcing the "human" representation of one over the other. Thanks to multi-bases, the function handles the heavy lifting of distributing an amount into the right amount of each of its subdivision (e.g., 267 pence is 1 pound, 2 shillings and 3 pence, `toUnits` returns `[1, 2, 3]`). Since `toUnits` is now used by `toFormat` under the hood (instead of `toUnit`), it makes formatting even easier. ## `toFormat` is replaced with `toDecimal` Until now, `toFormat` was the catch-all formatting function for any formatting need. It was also "only" a wrapper around `toUnit`, exposing the right information in a transformer function for users to format and display objects. An intermediary design was to expose both the data from `toUnits` and a `decimal` representation in `toFormat`. But ultimately, there's no point in having a single function that exposes everything: it's computationally more expensive and it does too many things. Instead, this PR introduces `toDecimal`, a formatter function that returns a stringified decimal representation. This is more reliable because of the string format which prevents the system from converting it to an inaccurate binary representation. Another benefit is that the string representation retains the exponent, meaning that $10.50, which used to be returned as `10.5` will now be returned as `"10.50"`. If users need this as a double, despite the potential lack of accuracy (e.g., to manipulate it or use it with the Intl API), they can easily cast it with `Number` or `parseFloat`, but that's their responsibility. `toDecimal` is only meant to be used with decimal, single-base objects. For multi-base or non-decimal needs, `toUnits` is recommended. ## `toUnits` and `toDecimal` take a transformer as its second parameter ```ts declare function f(d: Dinero, t: Transformer): TOutput; ``` This makes it easier for users to customize the output based on the exposed `value`: - A tuple in `toUnits` - A stringified double in `toDecimal` ```js const formatted = toDecimal(d, ({ value, currency }) => `${currency.code} ${value}`); // Instead of const decimal = toDecimal(d); const formatted = `${toSnapshot(d).currency.code} ${decimal}`; ``` ## PR notes **BREAKING CHANGE:** the `toUnit` and the `toFormat` functions were removed. fix #294 Co-authored-by: John Hooks --- bundlesize.config.json | 10 +- examples/cart-react/src/utils/format.js | 12 +- examples/cart-vue/src/utils/format.js | 12 +- examples/pricing-react/src/utils/format.js | 8 +- examples/starter/main.js | 6 +- .../etc/calculator-bigint.api.md | 9 +- .../src/api/__tests__/toNumber.test.ts | 7 - packages/calculator-bigint/src/api/index.ts | 1 - .../calculator-bigint/src/api/toNumber.ts | 12 - packages/calculator-bigint/src/calculator.ts | 2 - .../etc/calculator-number.api.md | 9 +- .../src/api/__tests__/toNumber.test.ts | 7 - packages/calculator-number/src/api/index.ts | 1 - .../calculator-number/src/api/toNumber.ts | 10 - packages/calculator-number/src/calculator.ts | 2 - packages/core/etc/core.api.md | 236 ++-- packages/core/src/api/hasSubUnits.ts | 6 +- packages/core/src/api/haveSameCurrency.ts | 8 +- packages/core/src/api/index.ts | 4 +- packages/core/src/api/toDecimal.ts | 62 + packages/core/src/api/toFormat.ts | 21 - packages/core/src/api/toUnit.ts | 23 - packages/core/src/api/toUnits.ts | 38 + packages/core/src/api/transformScale.ts | 29 +- packages/core/src/api/trimScale.ts | 7 +- packages/core/src/checks/messages.ts | 1 + .../core/src/divide/__tests__/down.test.ts | 46 + .../divide/__tests__/halfAwayFromZero.test.ts | 46 + .../src/divide/__tests__/halfDown.test.ts | 46 + .../src/divide/__tests__/halfEven.test.ts | 52 + .../core/src/divide/__tests__/halfOdd.test.ts | 52 + .../divide/__tests__/halfTowardsZero.test.ts | 46 + .../core/src/divide/__tests__/halfUp.test.ts | 46 + packages/core/src/divide/__tests__/up.test.ts | 46 + packages/core/src/divide/down.ts | 16 + packages/core/src/divide/halfAwayFromZero.ts | 23 + packages/core/src/divide/halfDown.ts | 14 + packages/core/src/divide/halfEven.ts | 17 + packages/core/src/divide/halfOdd.ts | 17 + packages/core/src/divide/halfTowardsZero.ts | 23 + packages/core/src/divide/halfUp.ts | 26 + packages/core/src/{round => divide}/index.ts | 0 packages/core/src/divide/up.ts | 16 + packages/core/src/helpers/createDinero.ts | 8 +- packages/core/src/index.ts | 2 +- .../core/src/round/__tests__/down.test.ts | 22 - .../round/__tests__/halfAwayFromZero.test.ts | 22 - .../core/src/round/__tests__/halfDown.test.ts | 22 - .../core/src/round/__tests__/halfEven.test.ts | 25 - .../core/src/round/__tests__/halfOdd.test.ts | 25 - .../round/__tests__/halfTowardsZero.test.ts | 22 - .../core/src/round/__tests__/halfUp.test.ts | 22 - packages/core/src/round/__tests__/up.test.ts | 22 - packages/core/src/round/down.ts | 12 - packages/core/src/round/halfAwayFromZero.ts | 15 - packages/core/src/round/halfDown.ts | 13 - packages/core/src/round/halfEven.ts | 19 - packages/core/src/round/halfOdd.ts | 19 - packages/core/src/round/halfTowardsZero.ts | 15 - packages/core/src/round/halfUp.ts | 12 - packages/core/src/round/up.ts | 12 - packages/core/src/types/Calculator.ts | 3 +- packages/core/src/types/Dinero.ts | 3 +- packages/core/src/types/DivideOperation.ts | 7 + packages/core/src/types/Formatter.ts | 7 +- packages/core/src/types/RoundingMode.ts | 1 - packages/core/src/types/RoundingOptions.ts | 6 - packages/core/src/types/TransformOperation.ts | 3 - packages/core/src/types/Transformer.ts | 13 +- packages/core/src/types/index.ts | 4 +- .../core/src/utils/__tests__/absolute.test.ts | 20 + .../src/utils/__tests__/computeBase.test.ts | 14 + .../src/utils/__tests__/getDivisors.test.ts | 17 + .../core/src/utils/__tests__/isArray.test.ts | 10 + .../core/src/utils/__tests__/isEven.test.ts | 12 +- .../core/src/utils/__tests__/isHalf.test.ts | 12 +- .../core/src/utils/__tests__/sign.test.ts | 20 + packages/core/src/utils/absolute.ts | 24 + packages/core/src/utils/computeBase.ts | 13 + packages/core/src/utils/getDivisors.ts | 13 + packages/core/src/utils/index.ts | 5 + packages/core/src/utils/isArray.ts | 5 + packages/core/src/utils/isEven.ts | 21 +- packages/core/src/utils/isHalf.ts | 23 +- packages/core/src/utils/sign.ts | 21 + packages/currencies/etc/currencies.api.md | 2 +- packages/currencies/src/types/Currency.ts | 2 +- packages/dinero.js/etc/dinero.js.api.md | 112 +- packages/dinero.js/package.json | 3 + .../src/api/__tests__/hasSubUnits.test.ts | 181 ++- .../api/__tests__/haveSameCurrency.test.ts | 65 ++ .../src/api/__tests__/toDecimal.test.ts | 222 ++++ .../src/api/__tests__/toFormat.test.ts | 68 -- .../src/api/__tests__/toUnit.test.ts | 38 - .../src/api/__tests__/toUnits.test.ts | 227 ++++ .../src/api/__tests__/transformScale.test.ts | 1016 +++++++++++++++-- packages/dinero.js/src/api/index.ts | 4 +- packages/dinero.js/src/api/toDecimal.ts | 28 + packages/dinero.js/src/api/toFormat.ts | 21 - packages/dinero.js/src/api/toSnapshot.ts | 3 +- packages/dinero.js/src/api/toUnit.ts | 22 - packages/dinero.js/src/api/toUnits.ts | 30 + packages/dinero.js/src/api/transformScale.ts | 5 +- packages/dinero.js/src/index.ts | 2 +- test/utils/castToBigintCurrency.ts | 4 +- test/utils/castToBigjsCurrency.ts | 4 +- test/utils/createBigjsDinero.ts | 1 - .../docs/api/conversions/transform-scale.mdx | 25 +- .../data/docs/api/formatting/to-decimal.mdx | 61 + .../data/docs/api/formatting/to-format.mdx | 86 -- website/data/docs/api/formatting/to-unit.mdx | 76 -- website/data/docs/api/formatting/to-units.mdx | 86 ++ website/data/docs/core-concepts/amount.mdx | 6 +- website/data/docs/core-concepts/currency.mdx | 6 +- .../data/docs/core-concepts/formatting.mdx | 59 +- .../docs/getting-started/compatibility.mdx | 2 +- .../data/docs/getting-started/quick-start.mdx | 15 +- .../docs/getting-started/upgrade-guide.mdx | 46 +- .../formatting-in-a-multilingual-site.mdx | 12 +- .../formatting-non-decimal-currencies.mdx | 89 +- .../integrating-with-payment-services.mdx | 6 +- .../guides/using-different-amount-types.mdx | 1 - website/data/docs/index.mdx | 8 +- website/data/sidebar.ts | 4 +- website/next.config.js | 10 + yarn.lock | 6 +- 126 files changed, 3142 insertions(+), 1183 deletions(-) delete mode 100644 packages/calculator-bigint/src/api/__tests__/toNumber.test.ts delete mode 100644 packages/calculator-bigint/src/api/toNumber.ts delete mode 100644 packages/calculator-number/src/api/__tests__/toNumber.test.ts delete mode 100644 packages/calculator-number/src/api/toNumber.ts create mode 100644 packages/core/src/api/toDecimal.ts delete mode 100644 packages/core/src/api/toFormat.ts delete mode 100644 packages/core/src/api/toUnit.ts create mode 100644 packages/core/src/api/toUnits.ts create mode 100644 packages/core/src/divide/__tests__/down.test.ts create mode 100644 packages/core/src/divide/__tests__/halfAwayFromZero.test.ts create mode 100644 packages/core/src/divide/__tests__/halfDown.test.ts create mode 100644 packages/core/src/divide/__tests__/halfEven.test.ts create mode 100644 packages/core/src/divide/__tests__/halfOdd.test.ts create mode 100644 packages/core/src/divide/__tests__/halfTowardsZero.test.ts create mode 100644 packages/core/src/divide/__tests__/halfUp.test.ts create mode 100644 packages/core/src/divide/__tests__/up.test.ts create mode 100644 packages/core/src/divide/down.ts create mode 100644 packages/core/src/divide/halfAwayFromZero.ts create mode 100644 packages/core/src/divide/halfDown.ts create mode 100644 packages/core/src/divide/halfEven.ts create mode 100644 packages/core/src/divide/halfOdd.ts create mode 100644 packages/core/src/divide/halfTowardsZero.ts create mode 100644 packages/core/src/divide/halfUp.ts rename packages/core/src/{round => divide}/index.ts (100%) create mode 100644 packages/core/src/divide/up.ts delete mode 100644 packages/core/src/round/__tests__/down.test.ts delete mode 100644 packages/core/src/round/__tests__/halfAwayFromZero.test.ts delete mode 100644 packages/core/src/round/__tests__/halfDown.test.ts delete mode 100644 packages/core/src/round/__tests__/halfEven.test.ts delete mode 100644 packages/core/src/round/__tests__/halfOdd.test.ts delete mode 100644 packages/core/src/round/__tests__/halfTowardsZero.test.ts delete mode 100644 packages/core/src/round/__tests__/halfUp.test.ts delete mode 100644 packages/core/src/round/__tests__/up.test.ts delete mode 100644 packages/core/src/round/down.ts delete mode 100644 packages/core/src/round/halfAwayFromZero.ts delete mode 100644 packages/core/src/round/halfDown.ts delete mode 100644 packages/core/src/round/halfEven.ts delete mode 100644 packages/core/src/round/halfOdd.ts delete mode 100644 packages/core/src/round/halfTowardsZero.ts delete mode 100644 packages/core/src/round/halfUp.ts delete mode 100644 packages/core/src/round/up.ts create mode 100644 packages/core/src/types/DivideOperation.ts delete mode 100644 packages/core/src/types/RoundingMode.ts delete mode 100644 packages/core/src/types/RoundingOptions.ts delete mode 100644 packages/core/src/types/TransformOperation.ts create mode 100644 packages/core/src/utils/__tests__/absolute.test.ts create mode 100644 packages/core/src/utils/__tests__/computeBase.test.ts create mode 100644 packages/core/src/utils/__tests__/getDivisors.test.ts create mode 100644 packages/core/src/utils/__tests__/isArray.test.ts create mode 100644 packages/core/src/utils/__tests__/sign.test.ts create mode 100644 packages/core/src/utils/absolute.ts create mode 100644 packages/core/src/utils/computeBase.ts create mode 100644 packages/core/src/utils/getDivisors.ts create mode 100644 packages/core/src/utils/isArray.ts create mode 100644 packages/core/src/utils/sign.ts create mode 100644 packages/dinero.js/src/api/__tests__/toDecimal.test.ts delete mode 100644 packages/dinero.js/src/api/__tests__/toFormat.test.ts delete mode 100644 packages/dinero.js/src/api/__tests__/toUnit.test.ts create mode 100644 packages/dinero.js/src/api/__tests__/toUnits.test.ts create mode 100644 packages/dinero.js/src/api/toDecimal.ts delete mode 100644 packages/dinero.js/src/api/toFormat.ts delete mode 100644 packages/dinero.js/src/api/toUnit.ts create mode 100644 packages/dinero.js/src/api/toUnits.ts create mode 100644 website/data/docs/api/formatting/to-decimal.mdx delete mode 100644 website/data/docs/api/formatting/to-format.mdx delete mode 100644 website/data/docs/api/formatting/to-unit.mdx create mode 100644 website/data/docs/api/formatting/to-units.mdx diff --git a/bundlesize.config.json b/bundlesize.config.json index 0ee85e0d0..afea224fc 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -2,23 +2,23 @@ "files": [ { "path": "packages/calculator-bigint/dist/umd/index.production.js", - "maxSize": "680 B" + "maxSize": "625 B" }, { "path": "packages/calculator-number/dist/umd/index.production.js", - "maxSize": "680 B" + "maxSize": "650 B" }, { "path": "packages/core/dist/umd/index.production.js", - "maxSize": "3.55 KB" + "maxSize": "4 KB" }, { "path": "packages/currencies/dist/umd/index.production.js", - "maxSize": "1.65 KB" + "maxSize": "1.5 KB" }, { "path": "packages/dinero.js/dist/umd/index.production.js", - "maxSize": "3.9 KB" + "maxSize": "4.75 KB" } ] } diff --git a/examples/cart-react/src/utils/format.js b/examples/cart-react/src/utils/format.js index 73d9cb2a9..a72d75a27 100644 --- a/examples/cart-react/src/utils/format.js +++ b/examples/cart-react/src/utils/format.js @@ -1,22 +1,22 @@ -import { toFormat, toSnapshot } from 'dinero.js'; +import { toDecimal, toSnapshot } from 'dinero.js'; function intlFormat(locale, options = {}) { return function formatter(dineroObject) { - function transformer({ amount, currency }) { - return amount.toLocaleString(locale, { + function transformer({ value, currency }) { + return Number(value).toLocaleString(locale, { ...options, style: 'currency', currency: currency.code, }); } - return toFormat(dineroObject, transformer); + return toDecimal(dineroObject, transformer); }; } function formatDefault(dineroObject) { - return toFormat(dineroObject, ({ amount, currency }) => { - return `${currency.code} ${amount.toFixed(currency.exponent)}`; + return toDecimal(dineroObject, ({ value, currency }) => { + return `${currency.code} ${Number(value).toFixed(currency.exponent)}`; }); } diff --git a/examples/cart-vue/src/utils/format.js b/examples/cart-vue/src/utils/format.js index 73d9cb2a9..a72d75a27 100644 --- a/examples/cart-vue/src/utils/format.js +++ b/examples/cart-vue/src/utils/format.js @@ -1,22 +1,22 @@ -import { toFormat, toSnapshot } from 'dinero.js'; +import { toDecimal, toSnapshot } from 'dinero.js'; function intlFormat(locale, options = {}) { return function formatter(dineroObject) { - function transformer({ amount, currency }) { - return amount.toLocaleString(locale, { + function transformer({ value, currency }) { + return Number(value).toLocaleString(locale, { ...options, style: 'currency', currency: currency.code, }); } - return toFormat(dineroObject, transformer); + return toDecimal(dineroObject, transformer); }; } function formatDefault(dineroObject) { - return toFormat(dineroObject, ({ amount, currency }) => { - return `${currency.code} ${amount.toFixed(currency.exponent)}`; + return toDecimal(dineroObject, ({ value, currency }) => { + return `${currency.code} ${Number(value).toFixed(currency.exponent)}`; }); } diff --git a/examples/pricing-react/src/utils/format.js b/examples/pricing-react/src/utils/format.js index bd0997e67..3b00ae7c3 100644 --- a/examples/pricing-react/src/utils/format.js +++ b/examples/pricing-react/src/utils/format.js @@ -1,11 +1,11 @@ -import { hasSubUnits, toFormat, toSnapshot } from 'dinero.js'; +import { hasSubUnits, toDecimal, toSnapshot } from 'dinero.js'; export function format(dineroObject) { - function transformer({ amount, currency }) { + function transformer({ value, currency }) { const { scale } = toSnapshot(dineroObject); const minimumFractionDigits = hasSubUnits(dineroObject) ? scale : 0; - return amount.toLocaleString('en-US', { + return Number(value).toLocaleString('en-US', { style: 'currency', currency: currency.code, maximumFractionDigits: scale, @@ -13,5 +13,5 @@ export function format(dineroObject) { }); } - return toFormat(dineroObject, transformer); + return toDecimal(dineroObject, transformer); } diff --git a/examples/starter/main.js b/examples/starter/main.js index 7e57be865..1a8b3ca08 100644 --- a/examples/starter/main.js +++ b/examples/starter/main.js @@ -1,10 +1,10 @@ /* eslint-disable functional/no-expression-statement, no-console */ import { USD } from '@dinero.js/currencies'; -import { dinero, toFormat, toSnapshot } from 'dinero.js'; +import { dinero, toDecimal, toSnapshot } from 'dinero.js'; -const transformer = (props) => `${props.currency.code} ${props.amount}`; +const transformer = ({ value, currency }) => `${currency.code} ${value}`; const d = dinero({ amount: 999, currency: USD }); console.log('Snapshot:', toSnapshot(d)); -console.log('Formatted:', toFormat(d, transformer)); +console.log('Formatted:', toDecimal(d, transformer)); diff --git a/packages/calculator-bigint/etc/calculator-bigint.api.md b/packages/calculator-bigint/etc/calculator-bigint.api.md index d204b48d6..b1206e367 100644 --- a/packages/calculator-bigint/etc/calculator-bigint.api.md +++ b/packages/calculator-bigint/etc/calculator-bigint.api.md @@ -6,7 +6,6 @@ import { BinaryOperation } from '@dinero.js/core'; import { ComparisonOperator } from '@dinero.js/core'; -import { TransformOperation } from '@dinero.js/core'; import { UnaryOperation } from '@dinero.js/core'; // @public @@ -15,7 +14,9 @@ export const add: BinaryOperation; // @public (undocumented) export const calculator: { add: BinaryOperation; - compare: BinaryOperation; + compare: BinaryOperation; decrement: UnaryOperation; increment: UnaryOperation; integerDivide: BinaryOperation; @@ -23,7 +24,6 @@ export const calculator: { multiply: BinaryOperation; power: BinaryOperation; subtract: BinaryOperation; - toNumber: TransformOperation; zero: typeof zero; }; @@ -51,9 +51,6 @@ export const power: BinaryOperation; // @public export const subtract: BinaryOperation; -// @public -export const toNumber: TransformOperation; - // @public export function zero(): bigint; diff --git a/packages/calculator-bigint/src/api/__tests__/toNumber.test.ts b/packages/calculator-bigint/src/api/__tests__/toNumber.test.ts deleted file mode 100644 index e77a9c48c..000000000 --- a/packages/calculator-bigint/src/api/__tests__/toNumber.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { toNumber } from '../toNumber'; - -describe('toNumber', () => { - it('returns the input', () => { - expect(toNumber(2n)).toBe(2); - }); -}); diff --git a/packages/calculator-bigint/src/api/index.ts b/packages/calculator-bigint/src/api/index.ts index 9a838f62e..4fc321bcf 100644 --- a/packages/calculator-bigint/src/api/index.ts +++ b/packages/calculator-bigint/src/api/index.ts @@ -7,5 +7,4 @@ export * from './modulo'; export * from './multiply'; export * from './power'; export * from './subtract'; -export * from './toNumber'; export * from './zero'; diff --git a/packages/calculator-bigint/src/api/toNumber.ts b/packages/calculator-bigint/src/api/toNumber.ts deleted file mode 100644 index 7682b3ab3..000000000 --- a/packages/calculator-bigint/src/api/toNumber.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TransformOperation } from '@dinero.js/core'; - -/** - * Transforms an value to a number. - * - * @param input - The value to transform. - * - * @returns The transformed value. - */ -export const toNumber: TransformOperation = (input) => { - return Number(input); -}; diff --git a/packages/calculator-bigint/src/calculator.ts b/packages/calculator-bigint/src/calculator.ts index 6001e1937..2d99301c2 100644 --- a/packages/calculator-bigint/src/calculator.ts +++ b/packages/calculator-bigint/src/calculator.ts @@ -8,7 +8,6 @@ import { multiply, power, subtract, - toNumber, zero, } from './api'; @@ -22,6 +21,5 @@ export const calculator = { multiply, power, subtract, - toNumber, zero, }; diff --git a/packages/calculator-number/etc/calculator-number.api.md b/packages/calculator-number/etc/calculator-number.api.md index 10e626e06..ec21e7a1d 100644 --- a/packages/calculator-number/etc/calculator-number.api.md +++ b/packages/calculator-number/etc/calculator-number.api.md @@ -6,7 +6,6 @@ import { BinaryOperation } from '@dinero.js/core'; import { ComparisonOperator } from '@dinero.js/core'; -import { TransformOperation } from '@dinero.js/core'; import { UnaryOperation } from '@dinero.js/core'; // @public @@ -15,7 +14,9 @@ export const add: BinaryOperation; // @public (undocumented) export const calculator: { add: BinaryOperation; - compare: BinaryOperation; + compare: BinaryOperation; decrement: UnaryOperation; increment: UnaryOperation; integerDivide: BinaryOperation; @@ -23,7 +24,6 @@ export const calculator: { multiply: BinaryOperation; power: BinaryOperation; subtract: BinaryOperation; - toNumber: TransformOperation; zero: typeof zero; }; @@ -51,9 +51,6 @@ export const power: BinaryOperation; // @public export const subtract: BinaryOperation; -// @public -export const toNumber: TransformOperation; - // @public export function zero(): number; diff --git a/packages/calculator-number/src/api/__tests__/toNumber.test.ts b/packages/calculator-number/src/api/__tests__/toNumber.test.ts deleted file mode 100644 index 5be811c2e..000000000 --- a/packages/calculator-number/src/api/__tests__/toNumber.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { toNumber } from '../toNumber'; - -describe('toNumber', () => { - it('returns the input', () => { - expect(toNumber(2)).toBe(2); - }); -}); diff --git a/packages/calculator-number/src/api/index.ts b/packages/calculator-number/src/api/index.ts index 9a838f62e..4fc321bcf 100644 --- a/packages/calculator-number/src/api/index.ts +++ b/packages/calculator-number/src/api/index.ts @@ -7,5 +7,4 @@ export * from './modulo'; export * from './multiply'; export * from './power'; export * from './subtract'; -export * from './toNumber'; export * from './zero'; diff --git a/packages/calculator-number/src/api/toNumber.ts b/packages/calculator-number/src/api/toNumber.ts deleted file mode 100644 index 9a46620ef..000000000 --- a/packages/calculator-number/src/api/toNumber.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TransformOperation } from '@dinero.js/core'; - -/** - * Transforms an value to a number. - * - * @param input - The value to transform. - * - * @returns The transformed value. - */ -export const toNumber: TransformOperation = (input) => input; diff --git a/packages/calculator-number/src/calculator.ts b/packages/calculator-number/src/calculator.ts index 6001e1937..2d99301c2 100644 --- a/packages/calculator-number/src/calculator.ts +++ b/packages/calculator-number/src/calculator.ts @@ -8,7 +8,6 @@ import { multiply, power, subtract, - toNumber, zero, } from './api'; @@ -22,6 +21,5 @@ export const calculator = { multiply, power, subtract, - toNumber, zero, }; diff --git a/packages/core/etc/core.api.md b/packages/core/etc/core.api.md index ed802b976..b36db24e8 100644 --- a/packages/core/etc/core.api.md +++ b/packages/core/etc/core.api.md @@ -22,7 +22,10 @@ ratios: ReadonlyArray | TAmount> export function assert(condition: boolean, message: string): void; // @public (undocumented) -export type BinaryOperation = (a: TInput, b: TInput) => TOutput; +export type BinaryOperation = ( +a: TInput, +b: TInput +) => TOutput; // @public (undocumented) export type Calculator = { @@ -35,7 +38,6 @@ export type Calculator = { readonly multiply: BinaryOperation; readonly power: BinaryOperation; readonly subtract: BinaryOperation; - readonly toNumber: TransformOperation; readonly zero: () => TInput; }; @@ -52,11 +54,17 @@ export enum ComparisonOperator { // (undocumented) GT = 1, // (undocumented) - LT = -1 + LT = -1, } // @public (undocumented) -export function convert(calculator: Calculator): (dineroObject: Dinero, newCurrency: Currency, rates: Rates) => Dinero; +export function convert( +calculator: Calculator +): ( +dineroObject: Dinero, +newCurrency: Currency, +rates: Rates +) => Dinero; // @public (undocumented) export type ConvertParams = readonly [ @@ -66,23 +74,37 @@ rates: Rates ]; // @public (undocumented) -export function createDinero({ calculator, onCreate, }: CreateDineroOptions): ({ amount, currency: { code, base, exponent }, scale, }: DineroOptions) => Dinero; +export function createDinero({ + calculator, + onCreate, + formatter, +}: CreateDineroOptions): ({ + amount, + currency: { code, base, exponent }, + scale, +}: DineroOptions) => Dinero; // @public (undocumented) export type CreateDineroOptions = { readonly calculator: Calculator; + readonly formatter?: Formatter; readonly onCreate?: (options: DineroOptions) => void; }; // @public (undocumented) export type Dinero = { readonly calculator: Calculator; + readonly formatter: Formatter; readonly create: (options: DineroOptions) => Dinero; readonly toJSON: () => DineroSnapshot; }; // @public (undocumented) -export type DineroFactory = ({ amount, currency, scale, }: DineroOptions) => Dinero; +export type DineroFactory = ({ + amount, + currency, + scale, +}: DineroOptions) => Dinero; // @public (undocumented) export type DineroOptions = { @@ -98,11 +120,20 @@ export type DineroSnapshot = { readonly scale: TAmount; }; -// @public -export const down: RoundingMode; +// @public (undocumented) +export type DivideOperation = ( +amount: TAmount, +factor: TAmount, +calculator: Calculator +) => TAmount; // @public (undocumented) -export function equal(calculator: Calculator): (dineroObject: Dinero, comparator: Dinero) => boolean; +export const down: DivideOperation; + +// @public (undocumented) +export function equal( +calculator: Calculator +): (dineroObject: Dinero, comparator: Dinero) => boolean; // @public (undocumented) export type EqualParams = readonly [ @@ -111,7 +142,10 @@ comparator: Dinero ]; // @public (undocumented) -export type Formatter = (dineroObject: Dinero) => string; +export type Formatter = { + readonly toNumber: (value?: TAmount) => number; + readonly toString: (value?: TAmount) => string; +}; // @public (undocumented) export type GreaterThanOrEqualParams = readonly [ @@ -125,26 +159,28 @@ dineroObject: Dinero, comparator: Dinero ]; -// @public -export const halfAwayFromZero: RoundingMode; +// @public (undocumented) +export const halfAwayFromZero: DivideOperation; -// @public -export const halfDown: RoundingMode; +// @public (undocumented) +export const halfDown: DivideOperation; -// @public -export const halfEven: RoundingMode; +// @public (undocumented) +export const halfEven: DivideOperation; -// @public -export const halfOdd: RoundingMode; +// @public (undocumented) +export const halfOdd: DivideOperation; -// @public -export const halfTowardsZero: RoundingMode; +// @public (undocumented) +export const halfTowardsZero: DivideOperation; -// @public -export const halfUp: RoundingMode; +// @public (undocumented) +export const halfUp: DivideOperation; // @public (undocumented) -export function hasSubUnits(calculator: Calculator): (dineroObject: Dinero) => boolean; +export function hasSubUnits( +calculator: Calculator +): (dineroObject: Dinero) => boolean; // @public (undocumented) export type HasSubUnitsParams = readonly [ @@ -152,7 +188,9 @@ dineroObject: Dinero ]; // @public (undocumented) -export function haveSameAmount(calculator: Calculator): (dineroObjects: readonly Dinero[]) => boolean; +export function haveSameAmount( +calculator: Calculator +): (dineroObjects: readonly Dinero[]) => boolean; // @public (undocumented) export type HaveSameAmountParams = readonly [ @@ -160,19 +198,23 @@ dineroObjects: ReadonlyArray> ]; // @public (undocumented) -export function haveSameCurrency(dineroObjects: ReadonlyArray>): boolean; +export function haveSameCurrency( +dineroObjects: ReadonlyArray> +): boolean; // @public (undocumented) -export const INVALID_AMOUNT_MESSAGE = "Amount is invalid."; +export const INVALID_AMOUNT_MESSAGE = 'Amount is invalid.'; // @public (undocumented) -export const INVALID_RATIOS_MESSAGE = "Ratios are invalid."; +export const INVALID_RATIOS_MESSAGE = 'Ratios are invalid.'; // @public (undocumented) -export const INVALID_SCALE_MESSAGE = "Scale is invalid."; +export const INVALID_SCALE_MESSAGE = 'Scale is invalid.'; // @public (undocumented) -export function isNegative(calculator: Calculator): (dineroObject: Dinero) => boolean; +export function isNegative( +calculator: Calculator +): (dineroObject: Dinero) => boolean; // @public (undocumented) export type IsNegativeParams = readonly [ @@ -180,7 +222,9 @@ dineroObject: Dinero ]; // @public (undocumented) -export function isPositive(calculator: Calculator): (dineroObject: Dinero) => boolean; +export function isPositive( +calculator: Calculator +): (dineroObject: Dinero) => boolean; // @public (undocumented) export type IsPositiveParams = readonly [ @@ -188,10 +232,14 @@ dineroObject: Dinero ]; // @public (undocumented) -export function isZero(calculator: Calculator): (dineroObject: Dinero) => boolean; +export function isZero( +calculator: Calculator +): (dineroObject: Dinero) => boolean; // @public (undocumented) -export type IsZeroParams = readonly [dineroObject: Dinero]; +export type IsZeroParams = readonly [ +dineroObject: Dinero +]; // @public (undocumented) export type LessThanOrEqualParams = readonly [ @@ -216,7 +264,12 @@ dineroObjects: ReadonlyArray> ]; // @public (undocumented) -export function multiply(calculator: Calculator): (multiplicand: Dinero, multiplier: TAmount | ScaledAmount) => Dinero; +export function multiply( +calculator: Calculator +): ( +multiplicand: Dinero, +multiplier: TAmount | ScaledAmount +) => Dinero; // @public (undocumented) export type MultiplyParams = readonly [ @@ -225,7 +278,12 @@ multiplier: ScaledAmount | TAmount ]; // @public (undocumented) -export function normalizeScale(calculator: Calculator): (dineroObjects: readonly Dinero[]) => Dinero[]; +export const NON_DECIMAL_CURRENCY_MESSAGE = 'Currency is not decimal.'; + +// @public (undocumented) +export function normalizeScale( +calculator: Calculator +): (dineroObjects: readonly Dinero[]) => Dinero[]; // @public (undocumented) export type NormalizeScaleParams = readonly [ @@ -239,43 +297,60 @@ export type Rate = ScaledAmount | TAmount; export type Rates = Record>; // @public (undocumented) -export type RoundingMode = (value: number) => number; - -// @public (undocumented) -export type RoundingOptions = { - readonly digits?: TAmount; - readonly round?: RoundingMode; -}; - -// @public (undocumented) -export function safeAdd(calculator: Calculator): (augend: Dinero, addend: Dinero) => Dinero; +export function safeAdd( +calculator: Calculator +): (augend: Dinero, addend: Dinero) => Dinero; // @public (undocumented) -export function safeAllocate(calculator: Calculator): (dineroObject: Dinero, ratios: readonly (TAmount | ScaledAmount)[]) => Dinero[]; +export function safeAllocate( +calculator: Calculator +): ( +dineroObject: Dinero, +ratios: readonly (TAmount | ScaledAmount)[] +) => Dinero[]; // @public (undocumented) -export function safeCompare(calculator: Calculator): (dineroObject: Dinero, comparator: Dinero) => ComparisonOperator; +export function safeCompare( +calculator: Calculator +): ( +dineroObject: Dinero, +comparator: Dinero +) => ComparisonOperator; // @public (undocumented) -export function safeGreaterThan(calculator: Calculator): (dineroObject: Dinero, comparator: Dinero) => boolean; +export function safeGreaterThan( +calculator: Calculator +): (dineroObject: Dinero, comparator: Dinero) => boolean; // @public (undocumented) -export function safeGreaterThanOrEqual(calculator: Calculator): (dineroObject: Dinero, comparator: Dinero) => boolean; +export function safeGreaterThanOrEqual( +calculator: Calculator +): (dineroObject: Dinero, comparator: Dinero) => boolean; // @public (undocumented) -export function safeLessThan(calculator: Calculator): (dineroObject: Dinero, comparator: Dinero) => boolean; +export function safeLessThan( +calculator: Calculator +): (dineroObject: Dinero, comparator: Dinero) => boolean; // @public (undocumented) -export function safeLessThanOrEqual(calculator: Calculator): (dineroObject: Dinero, comparator: Dinero) => boolean; +export function safeLessThanOrEqual( +calculator: Calculator +): (dineroObject: Dinero, comparator: Dinero) => boolean; // @public (undocumented) -export function safeMaximum(calculator: Calculator): (dineroObjects: readonly Dinero[]) => Dinero; +export function safeMaximum( +calculator: Calculator +): (dineroObjects: readonly Dinero[]) => Dinero; // @public (undocumented) -export function safeMinimum(calculator: Calculator): (dineroObjects: readonly Dinero[]) => Dinero; +export function safeMinimum( +calculator: Calculator +): (dineroObjects: readonly Dinero[]) => Dinero; // @public (undocumented) -export function safeSubtract(calculator: Calculator): (minuend: Dinero, subtrahend: Dinero) => Dinero; +export function safeSubtract( +calculator: Calculator +): (minuend: Dinero, subtrahend: Dinero) => Dinero; // @public (undocumented) export type ScaledAmount = { @@ -290,66 +365,77 @@ subtrahend: Dinero ]; // @public (undocumented) -export function toFormat(calculator: Calculator): (dineroObject: Dinero, transformer: Transformer_2) => string; +export function toDecimal(calculator: Calculator): (dineroObject: Dinero, transformer?: Transformer_2 | undefined) => string | TOutput; // @public (undocumented) -export type ToFormatParams = readonly [ +export type ToDecimalParams = readonly [ dineroObject: Dinero, -transformer: Transformer_2 +transformer?: Transformer_2 ]; // @public (undocumented) export function toSnapshot(dineroObject: Dinero): DineroSnapshot; // @public (undocumented) -export function toUnit(calculator: Calculator): (dineroObject: Dinero, options?: RoundingOptions | undefined) => number; +export function toUnits(calculator: Calculator): (dineroObject: Dinero, transformer?: Transformer_2 | undefined) => TOutput | readonly TAmount[]; // @public (undocumented) -export type ToUnitParams = readonly [ +export type ToUnitsParams = readonly [ dineroObject: Dinero, -options?: RoundingOptions +transformer?: Transformer_2 ]; // @public (undocumented) -type Transformer_2 = (options: TransformerOptions) => string; +type Transformer_2 = (options: TransformerOptions) => TOutput; export { Transformer_2 as Transformer } // @public (undocumented) -export type TransformerOptions = { - readonly amount: number; +export type TransformerOptions = { + readonly value: TValue; readonly currency: Currency; - readonly dineroObject: Dinero; }; // @public (undocumented) -export type TransformOperation = (input: TInput) => TOutput; - -// @public (undocumented) -export function transformScale(calculator: Calculator): (dineroObject: Dinero, newScale: TAmount) => Dinero; +export function transformScale( +calculator: Calculator +): ( +dineroObject: Dinero, +newScale: TAmount, +divide?: DivideOperation | undefined +) => Dinero; // @public (undocumented) export type TransformScaleParams = readonly [ dineroObject: Dinero, -newScale: TAmount +newScale: TAmount, +divide?: DivideOperation ]; // @public (undocumented) -export function trimScale(calculator: Calculator): (dineroObject: Dinero) => Dinero; +export function trimScale( +calculator: Calculator +): (dineroObject: Dinero) => Dinero; // @public (undocumented) -export type TrimScaleParams = readonly [dineroObject: Dinero]; +export type TrimScaleParams = readonly [ +dineroObject: Dinero +]; // @public (undocumented) -export type UnaryOperation = (value: TInput) => TOutput; +export type UnaryOperation = ( +value: TInput +) => TOutput; // @public (undocumented) -export const UNEQUAL_CURRENCIES_MESSAGE = "Objects must have the same currency."; +export const UNEQUAL_CURRENCIES_MESSAGE = +'Objects must have the same currency.'; // @public (undocumented) -export const UNEQUAL_SCALES_MESSAGE = "Objects must have the same scale."; +export const UNEQUAL_SCALES_MESSAGE = +'Objects must have the same scale.'; -// @public -export const up: RoundingMode; +// @public (undocumented) +export const up: DivideOperation; // (No @packageDocumentation comment for this package) diff --git a/packages/core/src/api/hasSubUnits.ts b/packages/core/src/api/hasSubUnits.ts index ea115c741..ced147368 100644 --- a/packages/core/src/api/hasSubUnits.ts +++ b/packages/core/src/api/hasSubUnits.ts @@ -1,5 +1,5 @@ import type { Calculator, Dinero } from '../types'; -import { equal } from '../utils'; +import { computeBase, equal } from '../utils'; export type HasSubUnitsParams = readonly [ dineroObject: Dinero @@ -7,12 +7,14 @@ export type HasSubUnitsParams = readonly [ export function hasSubUnits(calculator: Calculator) { const equalFn = equal(calculator); + const computeBaseFn = computeBase(calculator); return function _hasSubUnits(...[dineroObject]: HasSubUnitsParams) { const { amount, currency, scale } = dineroObject.toJSON(); + const base = computeBaseFn(currency.base); return !equalFn( - calculator.modulo(amount, calculator.power(currency.base, scale)), + calculator.modulo(amount, calculator.power(base, scale)), calculator.zero() ); }; diff --git a/packages/core/src/api/haveSameCurrency.ts b/packages/core/src/api/haveSameCurrency.ts index 6e5e9b1ea..920135cd7 100644 --- a/packages/core/src/api/haveSameCurrency.ts +++ b/packages/core/src/api/haveSameCurrency.ts @@ -1,19 +1,23 @@ import type { Dinero } from '../types'; -import { equal } from '../utils'; +import { computeBase, equal } from '../utils'; export function haveSameCurrency( dineroObjects: ReadonlyArray> ) { const [firstDinero, ...otherDineros] = dineroObjects; + const computeBaseFn = computeBase(firstDinero.calculator); + const { currency: comparator } = firstDinero.toJSON(); const equalFn = equal(firstDinero.calculator); + const comparatorBase = computeBaseFn(comparator.base); return otherDineros.every((d) => { const { currency: subject } = d.toJSON(); + const subjectBase = computeBaseFn(subject.base); return ( subject.code === comparator.code && - equalFn(subject.base, comparator.base) && + equalFn(subjectBase, comparatorBase) && equalFn(subject.exponent, comparator.exponent) ); }); diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index debb6de4d..1f7fb0e39 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -18,8 +18,8 @@ export * from './minimum'; export * from './multiply'; export * from './normalizeScale'; export * from './subtract'; -export * from './toFormat'; +export * from './toDecimal'; export * from './toSnapshot'; -export * from './toUnit'; +export * from './toUnits'; export * from './transformScale'; export * from './trimScale'; diff --git a/packages/core/src/api/toDecimal.ts b/packages/core/src/api/toDecimal.ts new file mode 100644 index 000000000..68113312a --- /dev/null +++ b/packages/core/src/api/toDecimal.ts @@ -0,0 +1,62 @@ +import { NON_DECIMAL_CURRENCY_MESSAGE } from '../checks'; +import { assert } from '../helpers'; +import type { Calculator, Dinero, Formatter, Transformer } from '../types'; +import { computeBase, equal, isArray } from '../utils'; + +import { toUnits } from './toUnits'; + +export type ToDecimalParams = readonly [ + dineroObject: Dinero, + transformer?: Transformer +]; + +export function toDecimal(calculator: Calculator) { + const toUnitsFn = toUnits(calculator); + const computeBaseFn = computeBase(calculator); + const equalFn = equal(calculator); + + return function toDecimalFn( + ...[dineroObject, transformer]: ToDecimalParams + ) { + const { currency, scale } = dineroObject.toJSON(); + + const base = computeBaseFn(currency.base); + const zero = calculator.zero(); + const ten = new Array(10).fill(null).reduce(calculator.increment, zero); + + const isMultiBase = isArray(currency.base); + const isBaseTen = equalFn(calculator.modulo(base, ten), zero); + const isDecimal = !isMultiBase && isBaseTen; + + // eslint-disable-next-line functional/no-expression-statement + assert(isDecimal, NON_DECIMAL_CURRENCY_MESSAGE); + + const units = toUnitsFn(dineroObject); + + const getDecimalFn = getDecimal(dineroObject.formatter); + const value = getDecimalFn(units, scale); + + if (!transformer) { + return value; + } + + return transformer({ value, currency }); + }; +} + +function getDecimal(formatter: Formatter) { + return (units: readonly TAmount[], scale: TAmount) => { + return units + .map((unit, index) => { + const isLast = units.length - 1 === index; + const unitAsString = formatter.toString(unit); + + if (isLast) { + return unitAsString.padStart(formatter.toNumber(scale), '0'); + } + + return unitAsString; + }) + .join('.'); + }; +} diff --git a/packages/core/src/api/toFormat.ts b/packages/core/src/api/toFormat.ts deleted file mode 100644 index 498ff4153..000000000 --- a/packages/core/src/api/toFormat.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Calculator, Dinero, Transformer } from '../types'; - -import { toUnit } from './toUnit'; - -export type ToFormatParams = readonly [ - dineroObject: Dinero, - transformer: Transformer -]; - -export function toFormat(calculator: Calculator) { - const toUnitFn = toUnit(calculator); - - return function toFormatFn( - ...[dineroObject, transformer]: ToFormatParams - ) { - const { currency, scale } = dineroObject.toJSON(); - const amount = toUnitFn(dineroObject, { digits: scale }); - - return transformer({ amount, currency, dineroObject }); - }; -} diff --git a/packages/core/src/api/toUnit.ts b/packages/core/src/api/toUnit.ts deleted file mode 100644 index 7410a756a..000000000 --- a/packages/core/src/api/toUnit.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Calculator, Dinero, RoundingOptions } from '../types'; - -export type ToUnitParams = readonly [ - dineroObject: Dinero, - options?: RoundingOptions -]; - -export function toUnit(calculator: Calculator) { - return function toUnitFn(...[dineroObject, options]: ToUnitParams) { - const round = options?.round || identity; - const { amount, currency, scale } = dineroObject.toJSON(); - const { power, toNumber } = calculator; - - const toUnitFactor = toNumber(power(currency.base, scale)); - const factor = toNumber(power(currency.base, options?.digits ?? scale)); - - return round((toNumber(amount) / toUnitFactor) * factor) / factor; - }; -} - -function identity(value: TValue) { - return value; -} diff --git a/packages/core/src/api/toUnits.ts b/packages/core/src/api/toUnits.ts new file mode 100644 index 000000000..8c6c83f22 --- /dev/null +++ b/packages/core/src/api/toUnits.ts @@ -0,0 +1,38 @@ +import type { Calculator, Dinero, Transformer } from '../types'; +import { isArray, getDivisors } from '../utils'; + +export type ToUnitsParams = readonly [ + dineroObject: Dinero, + transformer?: Transformer +]; + +export function toUnits(calculator: Calculator) { + const getDivisorsFn = getDivisors(calculator); + + return function toUnitsFn( + ...[dineroObject, transformer]: ToUnitsParams + ) { + const { amount, currency, scale } = dineroObject.toJSON(); + const { power, integerDivide, modulo } = calculator; + + const bases = isArray(currency.base) ? currency.base : [currency.base]; + const divisors = getDivisorsFn(bases.map((base) => power(base, scale))); + const value = divisors.reduce( + (amounts, divisor, index) => { + const amountLeft = amounts[index]; + + const quotient = integerDivide(amountLeft, divisor); + const remainder = modulo(amountLeft, divisor); + + return [...amounts.filter((_, i) => i !== index), quotient, remainder]; + }, + [amount] + ); + + if (!transformer) { + return value; + } + + return transformer({ value, currency }); + }; +} diff --git a/packages/core/src/api/transformScale.ts b/packages/core/src/api/transformScale.ts index 4bdb229d4..3b6f5e7b2 100644 --- a/packages/core/src/api/transformScale.ts +++ b/packages/core/src/api/transformScale.ts @@ -1,34 +1,31 @@ -import type { Calculator, Dinero } from '../types'; -import { greaterThan } from '../utils'; +import { down } from '../divide'; +import type { Calculator, Dinero, DivideOperation } from '../types'; +import { computeBase, greaterThan } from '../utils'; export type TransformScaleParams = readonly [ dineroObject: Dinero, - newScale: TAmount + newScale: TAmount, + divide?: DivideOperation ]; export function transformScale(calculator: Calculator) { const greaterThanFn = greaterThan(calculator); + const computeBaseFn = computeBase(calculator); return function transformScaleFn( - ...[dineroObject, newScale]: TransformScaleParams + ...[dineroObject, newScale, divide = down]: TransformScaleParams ) { const { amount, currency, scale } = dineroObject.toJSON(); - const isNewScaleLarger = greaterThanFn(newScale, scale); - const operation = isNewScaleLarger - ? calculator.multiply - : calculator.integerDivide; - const terms = isNewScaleLarger - ? ([newScale, scale] as const) - : ([scale, newScale] as const); + const isLarger = greaterThanFn(newScale, scale); + const operation = isLarger ? calculator.multiply : divide; + const [a, b] = isLarger ? [newScale, scale] : [scale, newScale]; + const base = computeBaseFn(currency.base); - const factor = calculator.power( - currency.base, - calculator.subtract(...terms) - ); + const factor = calculator.power(base, calculator.subtract(a, b)); return dineroObject.create({ - amount: operation(amount, factor), + amount: operation(amount, factor, calculator), currency, scale: newScale, }); diff --git a/packages/core/src/api/trimScale.ts b/packages/core/src/api/trimScale.ts index f826cf119..1e883f5a1 100644 --- a/packages/core/src/api/trimScale.ts +++ b/packages/core/src/api/trimScale.ts @@ -1,5 +1,5 @@ import type { Calculator, Dinero } from '../types'; -import { countTrailingZeros, equal, maximum } from '../utils'; +import { computeBase, countTrailingZeros, equal, maximum } from '../utils'; import { transformScale } from './transformScale'; @@ -10,14 +10,15 @@ export function trimScale(calculator: Calculator) { const equalFn = equal(calculator); const maximumFn = maximum(calculator); const transformScaleFn = transformScale(calculator); + const computeBaseFn = computeBase(calculator); return function trimScaleFn(...[dineroObject]: TrimScaleParams) { const { amount, currency, scale } = dineroObject.toJSON(); - const { base, exponent } = currency; + const base = computeBaseFn(currency.base); const trailingZerosLength = countTrailingZerosFn(amount, base); const difference = calculator.subtract(scale, trailingZerosLength); - const newScale = maximumFn([difference, exponent]); + const newScale = maximumFn([difference, currency.exponent]); if (equalFn(newScale, scale)) { return dineroObject; diff --git a/packages/core/src/checks/messages.ts b/packages/core/src/checks/messages.ts index e2bcd78f3..7dc801a3a 100644 --- a/packages/core/src/checks/messages.ts +++ b/packages/core/src/checks/messages.ts @@ -4,3 +4,4 @@ export const INVALID_RATIOS_MESSAGE = 'Ratios are invalid.'; export const UNEQUAL_SCALES_MESSAGE = 'Objects must have the same scale.'; export const UNEQUAL_CURRENCIES_MESSAGE = 'Objects must have the same currency.'; +export const NON_DECIMAL_CURRENCY_MESSAGE = 'Currency is not decimal.'; diff --git a/packages/core/src/divide/__tests__/down.test.ts b/packages/core/src/divide/__tests__/down.test.ts new file mode 100644 index 000000000..066d70529 --- /dev/null +++ b/packages/core/src/divide/__tests__/down.test.ts @@ -0,0 +1,46 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { down } from '../down'; + +describe('down', () => { + describe('decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(down(14, 10, calculator)).toBe(1); + }); + it('rounds down with a negative quotient below half', () => { + expect(down(-14, 10, calculator)).toBe(-2); + }); + it('rounds down with a positive half quotient', () => { + expect(down(15, 10, calculator)).toBe(1); + }); + it('rounds down with a negative half quotient', () => { + expect(down(-15, 10, calculator)).toBe(-2); + }); + it('rounds down with a positive quotient above half', () => { + expect(down(16, 10, calculator)).toBe(1); + }); + it('rounds down with a negative quotient above half', () => { + expect(down(-16, 10, calculator)).toBe(-2); + }); + }); + describe('non-decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(down(22, 5, calculator)).toBe(4); + }); + it('rounds down with a negative quotient below half', () => { + expect(down(-22, 5, calculator)).toBe(-5); + }); + it('rounds down with a positive half quotient', () => { + expect(down(3, 2, calculator)).toBe(1); + }); + it('rounds down with a negative half quotient', () => { + expect(down(-3, 2, calculator)).toBe(-2); + }); + it('rounds down with a positive quotient above half', () => { + expect(down(24, 5, calculator)).toBe(4); + }); + it('rounds down with a negative quotient above half', () => { + expect(down(-24, 5, calculator)).toBe(-5); + }); + }); +}); diff --git a/packages/core/src/divide/__tests__/halfAwayFromZero.test.ts b/packages/core/src/divide/__tests__/halfAwayFromZero.test.ts new file mode 100644 index 000000000..23a1c8e2b --- /dev/null +++ b/packages/core/src/divide/__tests__/halfAwayFromZero.test.ts @@ -0,0 +1,46 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { halfAwayFromZero } from '../halfAwayFromZero'; + +describe('halfAwayFromZero', () => { + describe('decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfAwayFromZero(14, 10, calculator)).toBe(1); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfAwayFromZero(-14, 10, calculator)).toBe(-1); + }); + it('rounds to the nearest integer away from zero with a positive half quotient', () => { + expect(halfAwayFromZero(15, 10, calculator)).toBe(2); + }); + it('rounds to the nearest integer away from zero with a negative half quotient', () => { + expect(halfAwayFromZero(-25, 10, calculator)).toBe(-3); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfAwayFromZero(16, 10, calculator)).toBe(2); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfAwayFromZero(-16, 10, calculator)).toBe(-2); + }); + }); + describe('non-decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfAwayFromZero(22, 5, calculator)).toBe(4); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfAwayFromZero(-22, 5, calculator)).toBe(-4); + }); + it('rounds to the nearest integer away from zero with a positive half quotient', () => { + expect(halfAwayFromZero(3, 2, calculator)).toBe(2); + }); + it('rounds to the nearest integer away from zero with a negative half quotient', () => { + expect(halfAwayFromZero(-5, 2, calculator)).toBe(-3); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfAwayFromZero(24, 5, calculator)).toBe(5); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfAwayFromZero(-24, 5, calculator)).toBe(-5); + }); + }); +}); diff --git a/packages/core/src/divide/__tests__/halfDown.test.ts b/packages/core/src/divide/__tests__/halfDown.test.ts new file mode 100644 index 000000000..b9f5511e4 --- /dev/null +++ b/packages/core/src/divide/__tests__/halfDown.test.ts @@ -0,0 +1,46 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { halfDown } from '../halfDown'; + +describe('halfDown', () => { + describe('decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfDown(14, 10, calculator)).toBe(1); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfDown(-14, 10, calculator)).toBe(-1); + }); + it('rounds down with a positive half quotient', () => { + expect(halfDown(15, 10, calculator)).toBe(1); + }); + it('rounds down with a negative half quotient', () => { + expect(halfDown(-15, 10, calculator)).toBe(-2); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfDown(16, 10, calculator)).toBe(2); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfDown(-16, 10, calculator)).toBe(-2); + }); + }); + describe('non-decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfDown(22, 5, calculator)).toBe(4); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfDown(-22, 5, calculator)).toBe(-4); + }); + it('rounds down with a positive half quotient', () => { + expect(halfDown(3, 2, calculator)).toBe(1); + }); + it('rounds down with a negative half quotient', () => { + expect(halfDown(-3, 2, calculator)).toBe(-2); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfDown(24, 5, calculator)).toBe(5); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfDown(-24, 5, calculator)).toBe(-5); + }); + }); +}); diff --git a/packages/core/src/divide/__tests__/halfEven.test.ts b/packages/core/src/divide/__tests__/halfEven.test.ts new file mode 100644 index 000000000..2e83c4aa6 --- /dev/null +++ b/packages/core/src/divide/__tests__/halfEven.test.ts @@ -0,0 +1,52 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { halfEven } from '../halfEven'; + +describe('halfEven', () => { + describe('decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfEven(14, 10, calculator)).toBe(1); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfEven(-14, 10, calculator)).toBe(-1); + }); + it('rounds to nearest even integer with a positive half quotient rounding to an even integer', () => { + expect(halfEven(15, 10, calculator)).toBe(2); + }); + it('rounds to nearest even integer with a positive half quotient rounding to an odd integer', () => { + expect(halfEven(25, 10, calculator)).toBe(2); + }); + it('rounds to nearest even integer with a negative half quotient', () => { + expect(halfEven(-25, 10, calculator)).toBe(-2); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfEven(16, 10, calculator)).toBe(2); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfEven(-16, 10, calculator)).toBe(-2); + }); + }); + describe('non-decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfEven(22, 5, calculator)).toBe(4); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfEven(-22, 5, calculator)).toBe(-4); + }); + it('rounds to nearest even integer with a positive half quotient rounding to an even integer', () => { + expect(halfEven(3, 2, calculator)).toBe(2); + }); + it('rounds to nearest even integer with a positive half quotient rounding to an odd integer', () => { + expect(halfEven(5, 2, calculator)).toBe(2); + }); + it('rounds to nearest even integer with a negative half quotient', () => { + expect(halfEven(-5, 2, calculator)).toBe(-2); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfEven(24, 5, calculator)).toBe(5); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfEven(-24, 5, calculator)).toBe(-5); + }); + }); +}); diff --git a/packages/core/src/divide/__tests__/halfOdd.test.ts b/packages/core/src/divide/__tests__/halfOdd.test.ts new file mode 100644 index 000000000..ed1c3d23a --- /dev/null +++ b/packages/core/src/divide/__tests__/halfOdd.test.ts @@ -0,0 +1,52 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { halfOdd } from '../halfOdd'; + +describe('halfOdd', () => { + describe('decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfOdd(14, 10, calculator)).toBe(1); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfOdd(-14, 10, calculator)).toBe(-1); + }); + it('rounds to nearest odd integer with a positive half quotient rounding to an even integer', () => { + expect(halfOdd(15, 10, calculator)).toBe(1); + }); + it('rounds to nearest odd integer with a positive half quotient rounding to an odd integer', () => { + expect(halfOdd(25, 10, calculator)).toBe(3); + }); + it('rounds to nearest odd integer with a negative half quotient', () => { + expect(halfOdd(-25, 10, calculator)).toBe(-3); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfOdd(16, 10, calculator)).toBe(2); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfOdd(-16, 10, calculator)).toBe(-2); + }); + }); + describe('non-decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfOdd(22, 5, calculator)).toBe(4); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfOdd(-22, 5, calculator)).toBe(-4); + }); + it('rounds to nearest odd integer with a positive half quotient rounding to an even integer', () => { + expect(halfOdd(3, 2, calculator)).toBe(1); + }); + it('rounds to nearest odd integer with a positive half quotient rounding to an odd integer', () => { + expect(halfOdd(5, 2, calculator)).toBe(3); + }); + it('rounds to nearest odd integer with a negative half quotient', () => { + expect(halfOdd(-5, 2, calculator)).toBe(-3); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfOdd(24, 5, calculator)).toBe(5); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfOdd(-24, 5, calculator)).toBe(-5); + }); + }); +}); diff --git a/packages/core/src/divide/__tests__/halfTowardsZero.test.ts b/packages/core/src/divide/__tests__/halfTowardsZero.test.ts new file mode 100644 index 000000000..a8eb70034 --- /dev/null +++ b/packages/core/src/divide/__tests__/halfTowardsZero.test.ts @@ -0,0 +1,46 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { halfTowardsZero } from '../halfTowardsZero'; + +describe('halfTowardsZero', () => { + describe('decimal factors', () => { + it('rounds down with a positive float below half', () => { + expect(halfTowardsZero(14, 10, calculator)).toBe(1); + }); + it('rounds up with a negative float below half', () => { + expect(halfTowardsZero(-14, 10, calculator)).toBe(-1); + }); + it('rounds to the nearest integer towards zero with a positive half float', () => { + expect(halfTowardsZero(15, 10, calculator)).toBe(1); + }); + it('rounds to the nearest integer towards zero with a negative half float', () => { + expect(halfTowardsZero(-25, 10, calculator)).toBe(-2); + }); + it('rounds up with a positive float above half', () => { + expect(halfTowardsZero(16, 10, calculator)).toBe(2); + }); + it('rounds down with a negative float above half', () => { + expect(halfTowardsZero(-16, 10, calculator)).toBe(-2); + }); + }); + describe('non-decimal factors', () => { + it('rounds down with a positive float below half', () => { + expect(halfTowardsZero(22, 5, calculator)).toBe(4); + }); + it('rounds up with a negative float below half', () => { + expect(halfTowardsZero(-22, 5, calculator)).toBe(-4); + }); + it('rounds to the nearest integer towards zero with a positive half float', () => { + expect(halfTowardsZero(3, 2, calculator)).toBe(1); + }); + it('rounds to the nearest integer towards zero with a negative half float', () => { + expect(halfTowardsZero(-5, 2, calculator)).toBe(-2); + }); + it('rounds up with a positive float above half', () => { + expect(halfTowardsZero(24, 5, calculator)).toBe(5); + }); + it('rounds down with a negative float above half', () => { + expect(halfTowardsZero(-24, 5, calculator)).toBe(-5); + }); + }); +}); diff --git a/packages/core/src/divide/__tests__/halfUp.test.ts b/packages/core/src/divide/__tests__/halfUp.test.ts new file mode 100644 index 000000000..bc2555e12 --- /dev/null +++ b/packages/core/src/divide/__tests__/halfUp.test.ts @@ -0,0 +1,46 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { halfUp } from '../halfUp'; + +describe('halfUp', () => { + describe('decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfUp(14, 10, calculator)).toBe(1); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfUp(-14, 10, calculator)).toBe(-1); + }); + it('rounds up with a positive half quotient', () => { + expect(halfUp(15, 10, calculator)).toBe(2); + }); + it('rounds up with a negative half quotient', () => { + expect(halfUp(-15, 10, calculator)).toBe(-1); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfUp(16, 10, calculator)).toBe(2); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfUp(-16, 10, calculator)).toBe(-2); + }); + }); + describe('non-decimal factors', () => { + it('rounds down with a positive quotient below half', () => { + expect(halfUp(22, 5, calculator)).toBe(4); + }); + it('rounds up with a negative quotient below half', () => { + expect(halfUp(-22, 5, calculator)).toBe(-4); + }); + it('rounds up with a positive half quotient', () => { + expect(halfUp(3, 2, calculator)).toBe(2); + }); + it('rounds up with a negative half quotient', () => { + expect(halfUp(-3, 2, calculator)).toBe(-1); + }); + it('rounds up with a positive quotient above half', () => { + expect(halfUp(24, 5, calculator)).toBe(5); + }); + it('rounds down with a negative quotient above half', () => { + expect(halfUp(-24, 5, calculator)).toBe(-5); + }); + }); +}); diff --git a/packages/core/src/divide/__tests__/up.test.ts b/packages/core/src/divide/__tests__/up.test.ts new file mode 100644 index 000000000..03ebb0a20 --- /dev/null +++ b/packages/core/src/divide/__tests__/up.test.ts @@ -0,0 +1,46 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { up } from '../up'; + +describe('up', () => { + describe('decimal factors', () => { + it('rounds up with a positive quotient below half', () => { + expect(up(14, 10, calculator)).toBe(2); + }); + it('rounds up with a negative quotient below half', () => { + expect(up(-14, 10, calculator)).toBe(-1); + }); + it('rounds up with a positive half quotient', () => { + expect(up(15, 10, calculator)).toBe(2); + }); + it('rounds up with a negative half quotient', () => { + expect(up(-15, 10, calculator)).toBe(-1); + }); + it('rounds up with a positive quotient above half', () => { + expect(up(16, 10, calculator)).toBe(2); + }); + it('rounds up with a negative quotient above half', () => { + expect(up(-16, 10, calculator)).toBe(-1); + }); + }); + describe('non-decimal factors', () => { + it('rounds up with a positive quotient below half', () => { + expect(up(22, 5, calculator)).toBe(5); + }); + it('rounds up with a negative quotient below half', () => { + expect(up(-22, 5, calculator)).toBe(-4); + }); + it('rounds up with a positive half quotient', () => { + expect(up(3, 2, calculator)).toBe(2); + }); + it('rounds up with a negative half quotient', () => { + expect(up(-3, 2, calculator)).toBe(-1); + }); + it('rounds up with a positive quotient above half', () => { + expect(up(24, 5, calculator)).toBe(5); + }); + it('rounds up with a negative quotient above half', () => { + expect(up(-24, 5, calculator)).toBe(-4); + }); + }); +}); diff --git a/packages/core/src/divide/down.ts b/packages/core/src/divide/down.ts new file mode 100644 index 000000000..725b9aec8 --- /dev/null +++ b/packages/core/src/divide/down.ts @@ -0,0 +1,16 @@ +import type { DivideOperation } from '..'; +import { greaterThanOrEqual } from '../utils'; + +export const down: DivideOperation = (amount, factor, calculator) => { + const greaterThanOrEqualFn = greaterThanOrEqual(calculator); + + const zero = calculator.zero(); + const isPositive = greaterThanOrEqualFn(amount, zero); + const quotient = calculator.integerDivide(amount, factor); + + if (isPositive) { + return quotient; + } + + return calculator.decrement(quotient); +}; diff --git a/packages/core/src/divide/halfAwayFromZero.ts b/packages/core/src/divide/halfAwayFromZero.ts new file mode 100644 index 000000000..71936ca47 --- /dev/null +++ b/packages/core/src/divide/halfAwayFromZero.ts @@ -0,0 +1,23 @@ +import type { DivideOperation } from '..'; +import { sign, isHalf, absolute } from '../utils'; + +import { halfUp, up } from '.'; + +export const halfAwayFromZero: DivideOperation = ( + amount, + factor, + calculator +) => { + const signFn = sign(calculator); + const isHalfFn = isHalf(calculator); + const absoluteFn = absolute(calculator); + + if (!isHalfFn(amount, factor)) { + return halfUp(amount, factor, calculator); + } + + return calculator.multiply( + signFn(amount), + up(absoluteFn(amount), factor, calculator) + ); +}; diff --git a/packages/core/src/divide/halfDown.ts b/packages/core/src/divide/halfDown.ts new file mode 100644 index 000000000..20d0947e8 --- /dev/null +++ b/packages/core/src/divide/halfDown.ts @@ -0,0 +1,14 @@ +import type { DivideOperation } from '..'; +import { isHalf } from '../utils'; + +import { down, halfUp } from '.'; + +export const halfDown: DivideOperation = (amount, factor, calculator) => { + const isHalfFn = isHalf(calculator); + + if (isHalfFn(amount, factor)) { + return down(amount, factor, calculator); + } + + return halfUp(amount, factor, calculator); +}; diff --git a/packages/core/src/divide/halfEven.ts b/packages/core/src/divide/halfEven.ts new file mode 100644 index 000000000..5881f7daa --- /dev/null +++ b/packages/core/src/divide/halfEven.ts @@ -0,0 +1,17 @@ +import type { DivideOperation } from '..'; +import { isEven, isHalf } from '../utils'; + +import { halfUp } from '.'; + +export const halfEven: DivideOperation = (amount, factor, calculator) => { + const isEvenFn = isEven(calculator); + const isHalfFn = isHalf(calculator); + + const rounded = halfUp(amount, factor, calculator); + + if (!isHalfFn(amount, factor)) { + return rounded; + } + + return isEvenFn(rounded) ? rounded : calculator.decrement(rounded); +}; diff --git a/packages/core/src/divide/halfOdd.ts b/packages/core/src/divide/halfOdd.ts new file mode 100644 index 000000000..b6b30fa14 --- /dev/null +++ b/packages/core/src/divide/halfOdd.ts @@ -0,0 +1,17 @@ +import type { DivideOperation } from '..'; +import { isEven, isHalf } from '../utils'; + +import { halfUp } from '.'; + +export const halfOdd: DivideOperation = (amount, factor, calculator) => { + const isEvenFn = isEven(calculator); + const isHalfFn = isHalf(calculator); + + const rounded = halfUp(amount, factor, calculator); + + if (!isHalfFn(amount, factor)) { + return rounded; + } + + return isEvenFn(rounded) ? calculator.decrement(rounded) : rounded; +}; diff --git a/packages/core/src/divide/halfTowardsZero.ts b/packages/core/src/divide/halfTowardsZero.ts new file mode 100644 index 000000000..65b7b90d0 --- /dev/null +++ b/packages/core/src/divide/halfTowardsZero.ts @@ -0,0 +1,23 @@ +import type { DivideOperation } from '..'; +import { sign, isHalf, absolute } from '../utils'; + +import { halfUp, down } from '.'; + +export const halfTowardsZero: DivideOperation = ( + amount, + factor, + calculator +) => { + const signFn = sign(calculator); + const isHalfFn = isHalf(calculator); + const absoluteFn = absolute(calculator); + + if (!isHalfFn(amount, factor)) { + return halfUp(amount, factor, calculator); + } + + return calculator.multiply( + signFn(amount), + down(absoluteFn(amount), factor, calculator) + ); +}; diff --git a/packages/core/src/divide/halfUp.ts b/packages/core/src/divide/halfUp.ts new file mode 100644 index 000000000..6bbecb4eb --- /dev/null +++ b/packages/core/src/divide/halfUp.ts @@ -0,0 +1,26 @@ +import type { DivideOperation } from '..'; +import { greaterThan, isHalf, absolute } from '../utils'; + +import { down, up } from '.'; + +export const halfUp: DivideOperation = (amount, factor, calculator) => { + const greaterThanFn = greaterThan(calculator); + const isHalfFn = isHalf(calculator); + const absoluteFn = absolute(calculator); + + const zero = calculator.zero(); + const remainder = absoluteFn(calculator.modulo(amount, factor)); + const difference = calculator.subtract(factor, remainder); + const isLessThanHalf = greaterThanFn(difference, remainder); + const isPositive = greaterThanFn(amount, calculator.increment(zero)); + + if ( + isHalfFn(amount, factor) || + (isLessThanHalf && !isPositive) || + (!isLessThanHalf && isPositive) + ) { + return up(amount, factor, calculator); + } + + return down(amount, factor, calculator); +}; diff --git a/packages/core/src/round/index.ts b/packages/core/src/divide/index.ts similarity index 100% rename from packages/core/src/round/index.ts rename to packages/core/src/divide/index.ts diff --git a/packages/core/src/divide/up.ts b/packages/core/src/divide/up.ts new file mode 100644 index 000000000..c2705c55f --- /dev/null +++ b/packages/core/src/divide/up.ts @@ -0,0 +1,16 @@ +import type { DivideOperation } from '..'; +import { greaterThanOrEqual } from '../utils'; + +export const up: DivideOperation = (amount, factor, calculator) => { + const greaterThanOrEqualFn = greaterThanOrEqual(calculator); + + const zero = calculator.zero(); + const isPositive = greaterThanOrEqualFn(amount, zero); + const quotient = calculator.integerDivide(amount, factor); + + if (isPositive) { + return calculator.increment(quotient); + } + + return quotient; +}; diff --git a/packages/core/src/helpers/createDinero.ts b/packages/core/src/helpers/createDinero.ts index a6756a0a6..0f4bb06f1 100644 --- a/packages/core/src/helpers/createDinero.ts +++ b/packages/core/src/helpers/createDinero.ts @@ -1,14 +1,19 @@ /* eslint-disable functional/no-mixed-type, functional/no-return-void, functional/no-expression-statement */ -import type { Calculator, Dinero, DineroOptions } from '../types'; +import type { Calculator, Dinero, DineroOptions, Formatter } from '../types'; export type CreateDineroOptions = { readonly calculator: Calculator; + readonly formatter?: Formatter; readonly onCreate?: (options: DineroOptions) => void; }; export function createDinero({ calculator, onCreate, + formatter = { + toNumber: Number, + toString: String, + }, }: CreateDineroOptions) { return function dinero({ amount, @@ -21,6 +26,7 @@ export function createDinero({ return { calculator, + formatter, create: dinero, toJSON() { return { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c183de328..b4442efee 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,5 @@ export * from './api'; export * from './checks'; export * from './helpers'; -export * from './round'; +export * from './divide'; export * from './types'; diff --git a/packages/core/src/round/__tests__/down.test.ts b/packages/core/src/round/__tests__/down.test.ts deleted file mode 100644 index 9803b6f97..000000000 --- a/packages/core/src/round/__tests__/down.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { down } from '../down'; - -describe('down', () => { - it('rounds down with a positive float below half', () => { - expect(down(1.4)).toBe(1); - }); - it('rounds down with a negative float below half', () => { - expect(down(-1.4)).toBe(-2); - }); - it('rounds down with a positive half float', () => { - expect(down(1.5)).toBe(1); - }); - it('rounds down with a negative half float', () => { - expect(down(-1.5)).toBe(-2); - }); - it('rounds down with a positive float above half', () => { - expect(down(1.6)).toBe(1); - }); - it('rounds down with a negative float above half', () => { - expect(down(-1.6)).toBe(-2); - }); -}); diff --git a/packages/core/src/round/__tests__/halfAwayFromZero.test.ts b/packages/core/src/round/__tests__/halfAwayFromZero.test.ts deleted file mode 100644 index 6b75e7056..000000000 --- a/packages/core/src/round/__tests__/halfAwayFromZero.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { halfAwayFromZero } from '../halfAwayFromZero'; - -describe('halfAwayFromZero', () => { - it('rounds down with a positive float below half', () => { - expect(halfAwayFromZero(1.4)).toBe(1); - }); - it('rounds up with a negative float below half', () => { - expect(halfAwayFromZero(-1.4)).toBe(-1); - }); - it('rounds to the nearest integer away from zero with a positive half float', () => { - expect(halfAwayFromZero(1.5)).toBe(2); - }); - it('rounds to the nearest integer away from zero with a negative half float', () => { - expect(halfAwayFromZero(-2.5)).toBe(-3); - }); - it('rounds up with a positive float above half', () => { - expect(halfAwayFromZero(1.6)).toBe(2); - }); - it('rounds down with a negative float above half', () => { - expect(halfAwayFromZero(-1.6)).toBe(-2); - }); -}); diff --git a/packages/core/src/round/__tests__/halfDown.test.ts b/packages/core/src/round/__tests__/halfDown.test.ts deleted file mode 100644 index 2db4341c8..000000000 --- a/packages/core/src/round/__tests__/halfDown.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { halfDown } from '../halfDown'; - -describe('halfDown', () => { - it('rounds down with a positive float below half', () => { - expect(halfDown(1.4)).toBe(1); - }); - it('rounds down with a negative float below half', () => { - expect(halfDown(-1.4)).toBe(-1); - }); - it('rounds down with a positive half float', () => { - expect(halfDown(1.5)).toBe(1); - }); - it('rounds down with a negative half float', () => { - expect(halfDown(-1.5)).toBe(-2); - }); - it('rounds up with a positive float above half', () => { - expect(halfDown(1.6)).toBe(2); - }); - it('rounds down with a negative float above half', () => { - expect(halfDown(-1.6)).toBe(-2); - }); -}); diff --git a/packages/core/src/round/__tests__/halfEven.test.ts b/packages/core/src/round/__tests__/halfEven.test.ts deleted file mode 100644 index b10585e69..000000000 --- a/packages/core/src/round/__tests__/halfEven.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { halfEven } from '../halfEven'; - -describe('halfEven', () => { - it('rounds down with a positive float below half', () => { - expect(halfEven(1.4)).toBe(1); - }); - it('rounds down with a negative float below half', () => { - expect(halfEven(-1.4)).toBe(-1); - }); - it('rounds to nearest even integer with a positive half float rounding to an even integer', () => { - expect(halfEven(1.5)).toBe(2); - }); - it('rounds to nearest even integer with a positive half float rounding to an odd integer', () => { - expect(halfEven(2.5)).toBe(2); - }); - it('rounds to nearest even integer with a negative half float', () => { - expect(halfEven(-2.5)).toBe(-2); - }); - it('rounds up with a positive float above half', () => { - expect(halfEven(1.6)).toBe(2); - }); - it('rounds down with a negative float above half', () => { - expect(halfEven(-1.6)).toBe(-2); - }); -}); diff --git a/packages/core/src/round/__tests__/halfOdd.test.ts b/packages/core/src/round/__tests__/halfOdd.test.ts deleted file mode 100644 index 34add0493..000000000 --- a/packages/core/src/round/__tests__/halfOdd.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { halfOdd } from '../halfOdd'; - -describe('halfOdd', () => { - it('rounds down with a positive float below half', () => { - expect(halfOdd(1.4)).toBe(1); - }); - it('rounds down with a negative float below half', () => { - expect(halfOdd(-1.4)).toBe(-1); - }); - it('rounds to nearest odd integer with a positive half float rounding to an even integer', () => { - expect(halfOdd(1.5)).toBe(1); - }); - it('rounds to nearest odd integer with a positive half float rounding to an odd integer', () => { - expect(halfOdd(2.5)).toBe(3); - }); - it('rounds to nearest odd integer with a negative half float', () => { - expect(halfOdd(-2.5)).toBe(-3); - }); - it('rounds up with a positive float above half', () => { - expect(halfOdd(1.6)).toBe(2); - }); - it('rounds down with a negative float above half', () => { - expect(halfOdd(-1.6)).toBe(-2); - }); -}); diff --git a/packages/core/src/round/__tests__/halfTowardsZero.test.ts b/packages/core/src/round/__tests__/halfTowardsZero.test.ts deleted file mode 100644 index b5ac10495..000000000 --- a/packages/core/src/round/__tests__/halfTowardsZero.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { halfTowardsZero } from '../halfTowardsZero'; - -describe('halfTowardsZero', () => { - it('rounds down with a positive float below half', () => { - expect(halfTowardsZero(1.4)).toBe(1); - }); - it('rounds up with a negative float below half', () => { - expect(halfTowardsZero(-1.4)).toBe(-1); - }); - it('rounds to the nearest integer towards zero with a positive half float', () => { - expect(halfTowardsZero(1.5)).toBe(1); - }); - it('rounds to the nearest integer towards zero with a negative half float', () => { - expect(halfTowardsZero(-2.5)).toBe(-2); - }); - it('rounds up with a positive float above half', () => { - expect(halfTowardsZero(1.6)).toBe(2); - }); - it('rounds down with a negative float above half', () => { - expect(halfTowardsZero(-1.6)).toBe(-2); - }); -}); diff --git a/packages/core/src/round/__tests__/halfUp.test.ts b/packages/core/src/round/__tests__/halfUp.test.ts deleted file mode 100644 index f9d6afba3..000000000 --- a/packages/core/src/round/__tests__/halfUp.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { halfUp } from '../halfUp'; - -describe('halfUp', () => { - it('rounds down with a positive float below half', () => { - expect(halfUp(1.4)).toBe(1); - }); - it('rounds down with a negative float below half', () => { - expect(halfUp(-1.4)).toBe(-1); - }); - it('rounds up with a positive half float', () => { - expect(halfUp(1.5)).toBe(2); - }); - it('rounds up with a negative half float', () => { - expect(halfUp(-2.5)).toBe(-2); - }); - it('rounds up with a positive float above half', () => { - expect(halfUp(1.6)).toBe(2); - }); - it('rounds down with a negative float above half', () => { - expect(halfUp(-1.6)).toBe(-2); - }); -}); diff --git a/packages/core/src/round/__tests__/up.test.ts b/packages/core/src/round/__tests__/up.test.ts deleted file mode 100644 index ec285b77f..000000000 --- a/packages/core/src/round/__tests__/up.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { up } from '../up'; - -describe('up', () => { - it('rounds up with a positive float below half', () => { - expect(up(1.4)).toBe(2); - }); - it('rounds up with a negative float below half', () => { - expect(up(-1.4)).toBe(-1); - }); - it('rounds up with a positive half float', () => { - expect(up(1.5)).toBe(2); - }); - it('rounds up with a negative half float', () => { - expect(up(-1.5)).toBe(-1); - }); - it('rounds up with a positive float above half', () => { - expect(up(1.6)).toBe(2); - }); - it('rounds up with a negative float above half', () => { - expect(up(-1.6)).toBe(-1); - }); -}); diff --git a/packages/core/src/round/down.ts b/packages/core/src/round/down.ts deleted file mode 100644 index aa3641746..000000000 --- a/packages/core/src/round/down.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RoundingMode } from '../types'; - -/** - * Round a number down. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const down: RoundingMode = (value) => { - return Math.floor(value); -}; diff --git a/packages/core/src/round/halfAwayFromZero.ts b/packages/core/src/round/halfAwayFromZero.ts deleted file mode 100644 index 7f634c098..000000000 --- a/packages/core/src/round/halfAwayFromZero.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { RoundingMode } from '../types'; -import { isHalf } from '../utils'; - -/** - * Round a number with half values to nearest integer farthest from zero. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const halfAwayFromZero: RoundingMode = (value) => { - return isHalf(value) - ? Math.sign(value) * Math.ceil(Math.abs(value)) - : Math.round(value); -}; diff --git a/packages/core/src/round/halfDown.ts b/packages/core/src/round/halfDown.ts deleted file mode 100644 index 9812e0b66..000000000 --- a/packages/core/src/round/halfDown.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { RoundingMode } from '../types'; -import { isHalf } from '../utils'; - -/** - * Round a number with half values down. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const halfDown: RoundingMode = (value) => { - return isHalf(value) ? Math.floor(value) : Math.round(value); -}; diff --git a/packages/core/src/round/halfEven.ts b/packages/core/src/round/halfEven.ts deleted file mode 100644 index 606b54cda..000000000 --- a/packages/core/src/round/halfEven.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { RoundingMode } from '../types'; -import { isEven, isHalf } from '../utils'; - -/** - * Round a number with half values to nearest even integer. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const halfEven: RoundingMode = (value) => { - const rounded = Math.round(value); - - if (!isHalf(value)) { - return rounded; - } - - return isEven(rounded) ? rounded : rounded - 1; -}; diff --git a/packages/core/src/round/halfOdd.ts b/packages/core/src/round/halfOdd.ts deleted file mode 100644 index b51185fae..000000000 --- a/packages/core/src/round/halfOdd.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { RoundingMode } from '../types'; -import { isEven, isHalf } from '../utils'; - -/** - * Round a number with half values to nearest odd integer. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const halfOdd: RoundingMode = (value) => { - const rounded = Math.round(value); - - if (!isHalf(value)) { - return rounded; - } - - return isEven(rounded) ? rounded - 1 : rounded; -}; diff --git a/packages/core/src/round/halfTowardsZero.ts b/packages/core/src/round/halfTowardsZero.ts deleted file mode 100644 index 62e9bf491..000000000 --- a/packages/core/src/round/halfTowardsZero.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { RoundingMode } from '../types'; -import { isHalf } from '../utils'; - -/** - * Round a number with half values to nearest integer closest to zero. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const halfTowardsZero: RoundingMode = (value) => { - return isHalf(value) - ? Math.sign(value) * Math.floor(Math.abs(value)) - : Math.round(value); -}; diff --git a/packages/core/src/round/halfUp.ts b/packages/core/src/round/halfUp.ts deleted file mode 100644 index 5ad109b92..000000000 --- a/packages/core/src/round/halfUp.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RoundingMode } from '../types'; - -/** - * Round a number with half values up. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const halfUp: RoundingMode = (value) => { - return Math.round(value); -}; diff --git a/packages/core/src/round/up.ts b/packages/core/src/round/up.ts deleted file mode 100644 index e4ddc7204..000000000 --- a/packages/core/src/round/up.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RoundingMode } from '../types'; - -/** - * Round a number up. - * - * @param value - The number to round. - * - * @returns The rounded number. - */ -export const up: RoundingMode = (value) => { - return Math.ceil(value); -}; diff --git a/packages/core/src/types/Calculator.ts b/packages/core/src/types/Calculator.ts index b125cb15e..8f6c0a579 100644 --- a/packages/core/src/types/Calculator.ts +++ b/packages/core/src/types/Calculator.ts @@ -1,5 +1,5 @@ /* eslint-disable functional/no-mixed-type */ -import type { BinaryOperation, TransformOperation, UnaryOperation } from '.'; +import type { BinaryOperation, UnaryOperation } from '.'; export enum ComparisonOperator { LT = -1, @@ -17,6 +17,5 @@ export type Calculator = { readonly multiply: BinaryOperation; readonly power: BinaryOperation; readonly subtract: BinaryOperation; - readonly toNumber: TransformOperation; readonly zero: () => TInput; }; diff --git a/packages/core/src/types/Dinero.ts b/packages/core/src/types/Dinero.ts index 07fa45b04..d1178c1ae 100644 --- a/packages/core/src/types/Dinero.ts +++ b/packages/core/src/types/Dinero.ts @@ -1,7 +1,8 @@ -import type { Calculator, DineroOptions, DineroSnapshot } from '.'; +import type { Calculator, DineroOptions, DineroSnapshot, Formatter } from '.'; export type Dinero = { readonly calculator: Calculator; + readonly formatter: Formatter; readonly create: (options: DineroOptions) => Dinero; readonly toJSON: () => DineroSnapshot; }; diff --git a/packages/core/src/types/DivideOperation.ts b/packages/core/src/types/DivideOperation.ts new file mode 100644 index 000000000..642cc7f9d --- /dev/null +++ b/packages/core/src/types/DivideOperation.ts @@ -0,0 +1,7 @@ +import type { Calculator } from '.'; + +export type DivideOperation = ( + amount: TAmount, + factor: TAmount, + calculator: Calculator +) => TAmount; diff --git a/packages/core/src/types/Formatter.ts b/packages/core/src/types/Formatter.ts index 1bf82a756..e8e35671e 100644 --- a/packages/core/src/types/Formatter.ts +++ b/packages/core/src/types/Formatter.ts @@ -1,3 +1,4 @@ -import type { Dinero } from '.'; - -export type Formatter = (dineroObject: Dinero) => string; +export type Formatter = { + readonly toNumber: (value?: TAmount) => number; + readonly toString: (value?: TAmount) => string; +}; diff --git a/packages/core/src/types/RoundingMode.ts b/packages/core/src/types/RoundingMode.ts deleted file mode 100644 index b9e9f8d82..000000000 --- a/packages/core/src/types/RoundingMode.ts +++ /dev/null @@ -1 +0,0 @@ -export type RoundingMode = (value: number) => number; diff --git a/packages/core/src/types/RoundingOptions.ts b/packages/core/src/types/RoundingOptions.ts deleted file mode 100644 index f3848cc91..000000000 --- a/packages/core/src/types/RoundingOptions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { RoundingMode } from './RoundingMode'; - -export type RoundingOptions = { - readonly digits?: TAmount; - readonly round?: RoundingMode; -}; diff --git a/packages/core/src/types/TransformOperation.ts b/packages/core/src/types/TransformOperation.ts deleted file mode 100644 index c7027edb5..000000000 --- a/packages/core/src/types/TransformOperation.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type TransformOperation = ( - input: TInput -) => TOutput; diff --git a/packages/core/src/types/Transformer.ts b/packages/core/src/types/Transformer.ts index 5caab5972..f19fd7709 100644 --- a/packages/core/src/types/Transformer.ts +++ b/packages/core/src/types/Transformer.ts @@ -1,13 +1,10 @@ import type { Currency } from '@dinero.js/currencies'; -import type { Dinero } from './Dinero'; - -export type TransformerOptions = { - readonly amount: number; +export type TransformerOptions = { + readonly value: TValue; readonly currency: Currency; - readonly dineroObject: Dinero; }; -export type Transformer = ( - options: TransformerOptions -) => string; +export type Transformer = ( + options: TransformerOptions +) => TOutput; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index e704aab37..27b218a38 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -4,11 +4,9 @@ export * from './Dinero'; export * from './DineroFactory'; export * from './DineroOptions'; export * from './DineroSnapshot'; +export * from './DivideOperation'; export * from './Formatter'; export * from './Rates'; -export * from './RoundingMode'; -export * from './RoundingOptions'; export * from './ScaledAmount'; export * from './Transformer'; -export * from './TransformOperation'; export * from './UnaryOperation'; diff --git a/packages/core/src/utils/__tests__/absolute.test.ts b/packages/core/src/utils/__tests__/absolute.test.ts new file mode 100644 index 000000000..feec62871 --- /dev/null +++ b/packages/core/src/utils/__tests__/absolute.test.ts @@ -0,0 +1,20 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { absolute } from '../absolute'; + +const absoluteFn = absolute(calculator); + +describe('absolute', () => { + it('returns the value with positive values', () => { + expect(absoluteFn(5)).toBe(5); + }); + it('returns the negation of the value with negative values', () => { + expect(absoluteFn(-5)).toBe(5); + }); + it('returns the value with positive zero', () => { + expect(absoluteFn(0)).toBe(0); + }); + it('returns the negation of the value with negative zero', () => { + expect(absoluteFn(-0)).toBe(0); + }); +}); diff --git a/packages/core/src/utils/__tests__/computeBase.test.ts b/packages/core/src/utils/__tests__/computeBase.test.ts new file mode 100644 index 000000000..c65063660 --- /dev/null +++ b/packages/core/src/utils/__tests__/computeBase.test.ts @@ -0,0 +1,14 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { computeBase as createComputeBase } from '../computeBase'; + +const computeBase = createComputeBase(calculator); + +describe('computeBase', () => { + it('returns non-array values as is', () => { + expect(computeBase(100)).toBe(100); + }); + it('computes array values', () => { + expect(computeBase([20, 12, 7])).toBe(1680); + }); +}); diff --git a/packages/core/src/utils/__tests__/getDivisors.test.ts b/packages/core/src/utils/__tests__/getDivisors.test.ts new file mode 100644 index 000000000..d862eeb1e --- /dev/null +++ b/packages/core/src/utils/__tests__/getDivisors.test.ts @@ -0,0 +1,17 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { getDivisors } from '../getDivisors'; + +const getDivisorsFn = getDivisors(calculator); + +describe('#getDivisors', () => { + it('returns the same divisor with one base', () => { + expect(getDivisorsFn([100])).toEqual([100]); + }); + it('recursively computes divisors with two bases', () => { + expect(getDivisorsFn([20, 12])).toEqual([240, 12]); + }); + it('recursively computes divisors with more than two bases', () => { + expect(getDivisorsFn([20, 12, 7])).toEqual([1680, 84, 7]); + }); +}); diff --git a/packages/core/src/utils/__tests__/isArray.test.ts b/packages/core/src/utils/__tests__/isArray.test.ts new file mode 100644 index 000000000..b1ede50fa --- /dev/null +++ b/packages/core/src/utils/__tests__/isArray.test.ts @@ -0,0 +1,10 @@ +import { isArray } from '..'; + +describe('isArray', () => { + it('returns true with arrays', () => { + expect(isArray([])).toBe(true); + }); + it('returns false with numbers', () => { + expect(isArray(5)).toBe(false); + }); +}); diff --git a/packages/core/src/utils/__tests__/isEven.test.ts b/packages/core/src/utils/__tests__/isEven.test.ts index a60179c56..49bd1c974 100644 --- a/packages/core/src/utils/__tests__/isEven.test.ts +++ b/packages/core/src/utils/__tests__/isEven.test.ts @@ -1,16 +1,20 @@ +import { calculator } from '@dinero.js/calculator-number'; + import { isEven } from '../isEven'; +const isEvenFn = isEven(calculator); + describe('isEven', () => { it('returns true for a positive even integer', () => { - expect(isEven(202)).toBe(true); + expect(isEvenFn(202)).toBe(true); }); it('returns true for a negative even integer', () => { - expect(isEven(-202)).toBe(true); + expect(isEvenFn(-202)).toBe(true); }); it('returns false for a positive odd integer', () => { - expect(isEven(101)).toBe(false); + expect(isEvenFn(101)).toBe(false); }); it('returns false for a negative odd integer', () => { - expect(isEven(-101)).toBe(false); + expect(isEvenFn(-101)).toBe(false); }); }); diff --git a/packages/core/src/utils/__tests__/isHalf.test.ts b/packages/core/src/utils/__tests__/isHalf.test.ts index a71e4c973..21de34bd3 100644 --- a/packages/core/src/utils/__tests__/isHalf.test.ts +++ b/packages/core/src/utils/__tests__/isHalf.test.ts @@ -1,13 +1,17 @@ +import { calculator } from '@dinero.js/calculator-number'; + import { isHalf } from '../isHalf'; -describe('#isHalf', () => { +const isHalfFn = isHalf(calculator); + +describe('isHalf', () => { it('returns true with a half number', () => { - expect(isHalf(2.5)).toBe(true); + expect(isHalfFn(5, 10)).toBe(true); }); it('returns true with a negative half number', () => { - expect(isHalf(-2.5)).toBe(true); + expect(isHalfFn(-5, 10)).toBe(true); }); it('returns false with a non-half number', () => { - expect(isHalf(2)).toBe(false); + expect(isHalfFn(2, 10)).toBe(false); }); }); diff --git a/packages/core/src/utils/__tests__/sign.test.ts b/packages/core/src/utils/__tests__/sign.test.ts new file mode 100644 index 000000000..f9438fca7 --- /dev/null +++ b/packages/core/src/utils/__tests__/sign.test.ts @@ -0,0 +1,20 @@ +import { calculator } from '@dinero.js/calculator-number'; + +import { sign } from '../sign'; + +const signFn = sign(calculator); + +describe('sign', () => { + it('returns 0 with positive zero', () => { + expect(signFn(0)).toBe(0); + }); + it('returns 0 with negative zero', () => { + expect(signFn(-0)).toBe(0); + }); + it('returns 1 with positive values', () => { + expect(signFn(5)).toBe(1); + }); + it('returns -1 with negative values', () => { + expect(signFn(-5)).toBe(-1); + }); +}); diff --git a/packages/core/src/utils/absolute.ts b/packages/core/src/utils/absolute.ts new file mode 100644 index 000000000..3ce525c55 --- /dev/null +++ b/packages/core/src/utils/absolute.ts @@ -0,0 +1,24 @@ +import type { Calculator } from '../types'; + +import { equal } from './equal'; +import { lessThan } from './lessThan'; + +export function absolute(calculator: Calculator) { + const equalFn = equal(calculator); + const lessThanFn = lessThan(calculator); + const zero = calculator.zero(); + + return (input: TAmount) => { + if (equalFn(input, zero)) { + return zero; + } + + if (lessThanFn(input, zero)) { + const minusOne = calculator.decrement(zero); + + return calculator.multiply(minusOne, input); + } + + return input; + }; +} diff --git a/packages/core/src/utils/computeBase.ts b/packages/core/src/utils/computeBase.ts new file mode 100644 index 000000000..034be141d --- /dev/null +++ b/packages/core/src/utils/computeBase.ts @@ -0,0 +1,13 @@ +import type { Calculator } from '../types'; + +import { isArray } from './isArray'; + +export function computeBase(calculator: Calculator) { + return (base: TAmount | readonly TAmount[]) => { + if (isArray(base)) { + return base.reduce((acc, curr) => calculator.multiply(acc, curr)); + } + + return base; + }; +} diff --git a/packages/core/src/utils/getDivisors.ts b/packages/core/src/utils/getDivisors.ts new file mode 100644 index 000000000..6a532a7b9 --- /dev/null +++ b/packages/core/src/utils/getDivisors.ts @@ -0,0 +1,13 @@ +import type { Calculator } from '../types'; + +export function getDivisors(calculator: Calculator) { + const { multiply } = calculator; + + return (bases: readonly TAmount[]) => { + return bases.reduce((divisors, _, i) => { + const divisor = bases.slice(i).reduce((acc, curr) => multiply(acc, curr)); + + return [...divisors, divisor]; + }, []); + }; +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 6c241df5b..7e3ca4704 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,10 +1,14 @@ +export * from './absolute'; export * from './compare'; +export * from './computeBase'; export * from './countTrailingZeros'; export * from './distribute'; export * from './equal'; export * from './getAmountAndScale'; +export * from './getDivisors'; export * from './greaterThan'; export * from './greaterThanOrEqual'; +export * from './isArray'; export * from './isEven'; export * from './isHalf'; export * from './isScaledAmount'; @@ -12,3 +16,4 @@ export * from './lessThan'; export * from './lessThanOrEqual'; export * from './maximum'; export * from './minimum'; +export * from './sign'; diff --git a/packages/core/src/utils/isArray.ts b/packages/core/src/utils/isArray.ts new file mode 100644 index 000000000..8c930da61 --- /dev/null +++ b/packages/core/src/utils/isArray.ts @@ -0,0 +1,5 @@ +export function isArray( + maybeArray: TType | readonly TType[] +): maybeArray is readonly TType[] { + return Array.isArray(maybeArray); +} diff --git a/packages/core/src/utils/isEven.ts b/packages/core/src/utils/isEven.ts index 952e21402..ef29ca239 100644 --- a/packages/core/src/utils/isEven.ts +++ b/packages/core/src/utils/isEven.ts @@ -1,10 +1,13 @@ -/** - * Return whether a number is even. - * - * @param value - The number to test. - * - * @returns Whether the number is even. - */ -export function isEven(value: number) { - return value % 2 === 0; +import type { Calculator } from '../types'; + +import { equal } from '.'; + +export function isEven(calculator: Calculator) { + const equalFn = equal(calculator); + const zero = calculator.zero(); + const two = calculator.increment(calculator.increment(zero)); + + return (input: TAmount) => { + return equalFn(calculator.modulo(input, two), zero); + }; } diff --git a/packages/core/src/utils/isHalf.ts b/packages/core/src/utils/isHalf.ts index deb669c91..89c6552e4 100644 --- a/packages/core/src/utils/isHalf.ts +++ b/packages/core/src/utils/isHalf.ts @@ -1,10 +1,15 @@ -/** - * Return whether a number is half. - * - * @param value - The number to test. - * - * @returns Whether the number is half. - */ -export function isHalf(value: number) { - return Math.abs(value) % 1 === 0.5; +import type { Calculator } from '../types'; + +import { equal, absolute } from '.'; + +export function isHalf(calculator: Calculator) { + const equalFn = equal(calculator); + const absoluteFn = absolute(calculator); + + return (input: TAmount, total: TAmount) => { + const remainder = absoluteFn(calculator.modulo(input, total)); + const difference = calculator.subtract(total, remainder); + + return equalFn(difference, remainder); + }; } diff --git a/packages/core/src/utils/sign.ts b/packages/core/src/utils/sign.ts new file mode 100644 index 000000000..a5ca52169 --- /dev/null +++ b/packages/core/src/utils/sign.ts @@ -0,0 +1,21 @@ +import type { Calculator } from '../types'; + +import { equal } from './equal'; +import { lessThan } from './lessThan'; + +export function sign(calculator: Calculator) { + const equalFn = equal(calculator); + const lessThanFn = lessThan(calculator); + const zero = calculator.zero(); + + return (input: TAmount) => { + if (equalFn(input, zero)) { + return zero; + } + + const one = calculator.increment(zero); + const minusOne = calculator.decrement(zero); + + return lessThanFn(input, zero) ? minusOne : one; + }; +} diff --git a/packages/currencies/etc/currencies.api.md b/packages/currencies/etc/currencies.api.md index 53afd0e7c..30c386135 100644 --- a/packages/currencies/etc/currencies.api.md +++ b/packages/currencies/etc/currencies.api.md @@ -124,7 +124,7 @@ export const CUP: Currency; // @public (undocumented) export type Currency = { readonly code: string; - readonly base: TAmount; + readonly base: TAmount | readonly TAmount[]; readonly exponent: TAmount; }; diff --git a/packages/currencies/src/types/Currency.ts b/packages/currencies/src/types/Currency.ts index e5eda76a4..6728a27b7 100644 --- a/packages/currencies/src/types/Currency.ts +++ b/packages/currencies/src/types/Currency.ts @@ -6,7 +6,7 @@ export type Currency = { /** * The base, or radix of the currency. */ - readonly base: TAmount; + readonly base: TAmount | readonly TAmount[]; /** * The exponent of the currency. */ diff --git a/packages/dinero.js/etc/dinero.js.api.md b/packages/dinero.js/etc/dinero.js.api.md index ad8556d50..0232415b8 100644 --- a/packages/dinero.js/etc/dinero.js.api.md +++ b/packages/dinero.js/etc/dinero.js.api.md @@ -16,6 +16,7 @@ import { Dinero } from '@dinero.js/core'; import { DineroFactory } from '@dinero.js/core'; import { DineroOptions } from '@dinero.js/core'; import { DineroSnapshot } from '@dinero.js/core'; +import { DivideOperation } from '@dinero.js/core'; import { down } from '@dinero.js/core'; import type { EqualParams } from '@dinero.js/core'; import { Formatter } from '@dinero.js/core'; @@ -40,31 +41,36 @@ import type { MinimumParams } from '@dinero.js/core'; import type { MultiplyParams } from '@dinero.js/core'; import type { NormalizeScaleParams } from '@dinero.js/core'; import { Rates } from '@dinero.js/core'; -import { RoundingOptions } from '@dinero.js/core'; import type { SubtractParams } from '@dinero.js/core'; -import type { ToFormatParams } from '@dinero.js/core'; import { toSnapshot as toSnapshot_2 } from '@dinero.js/core'; -import type { ToUnitParams } from '@dinero.js/core'; import { Transformer as Transformer_2 } from '@dinero.js/core'; import type { TransformScaleParams } from '@dinero.js/core'; import type { TrimScaleParams } from '@dinero.js/core'; import { up } from '@dinero.js/core'; // @public -export function add(...[augend, addend]: AddParams): Dinero; +export function add( +...[augend, addend]: AddParams +): Dinero; // @public -export function allocate(...[dineroObject, ratios]: AllocateParams): Dinero[]; +export function allocate( +...[dineroObject, ratios]: AllocateParams +): Dinero[]; export { Calculator } // @public -export function compare(...[dineroObject, comparator]: CompareParams): ComparisonOperator; +export function compare( +...[dineroObject, comparator]: CompareParams +): ComparisonOperator; export { ComparisonOperator } // @public -export function convert(...[dineroObject, newCurrency, rates]: ConvertParams): Dinero; +export function convert( +...[dineroObject, newCurrency, rates]: ConvertParams +): Dinero; export { createDinero } @@ -73,7 +79,11 @@ export { Currency } export { Dinero } // @public -export const dinero: ({ amount, currency: { code, base, exponent }, scale, }: DineroOptions) => Dinero; +export const dinero: ({ + amount, + currency: { code, base, exponent }, + scale, +}: DineroOptions) => Dinero; export { DineroFactory } @@ -81,18 +91,26 @@ export { DineroOptions } export { DineroSnapshot } +export { DivideOperation } + export { down } // @public -export function equal(...[dineroObject, comparator]: EqualParams): boolean; +export function equal( +...[dineroObject, comparator]: EqualParams +): boolean; export { Formatter } // @public -export function greaterThan(...[dineroObject, comparator]: GreaterThanParams): boolean; +export function greaterThan( +...[dineroObject, comparator]: GreaterThanParams +): boolean; // @public -export function greaterThanOrEqual(...[dineroObject, comparator]: GreaterThanOrEqualParams): boolean; +export function greaterThanOrEqual( +...[dineroObject, comparator]: GreaterThanOrEqualParams +): boolean; export { halfAwayFromZero } @@ -107,64 +125,96 @@ export { halfTowardsZero } export { halfUp } // @public -export function hasSubUnits(...[dineroObject]: HasSubUnitsParams): boolean; +export function hasSubUnits( +...[dineroObject]: HasSubUnitsParams +): boolean; // @public -export function haveSameAmount(...[dineroObjects]: HaveSameAmountParams): boolean; +export function haveSameAmount( +...[dineroObjects]: HaveSameAmountParams +): boolean; // @public export const haveSameCurrency: typeof haveSameCurrency_2; // @public -export function isNegative(...[dineroObject]: IsNegativeParams): boolean; +export function isNegative( +...[dineroObject]: IsNegativeParams +): boolean; // @public -export function isPositive(...[dineroObject]: IsPositiveParams): boolean; +export function isPositive( +...[dineroObject]: IsPositiveParams +): boolean; // @public -export function isZero(...[dineroObject]: IsZeroParams): boolean; +export function isZero( +...[dineroObject]: IsZeroParams +): boolean; // @public -export function lessThan(...[dineroObject, comparator]: LessThanParams): boolean; +export function lessThan( +...[dineroObject, comparator]: LessThanParams +): boolean; // @public -export function lessThanOrEqual(...[dineroObject, comparator]: LessThanOrEqualParams): boolean; +export function lessThanOrEqual( +...[dineroObject, comparator]: LessThanOrEqualParams +): boolean; // @public -export function maximum(...[dineroObjects]: MaximumParams): Dinero; +export function maximum( +...[dineroObjects]: MaximumParams +): Dinero; // @public -export function minimum(...[dineroObjects]: MinimumParams): Dinero; +export function minimum( +...[dineroObjects]: MinimumParams +): Dinero; // @public -export function multiply(...[multiplicand, multiplier]: MultiplyParams): Dinero; +export function multiply( +...[multiplicand, multiplier]: MultiplyParams +): Dinero; // @public -export function normalizeScale(...[dineroObjects]: NormalizeScaleParams): Dinero[]; +export function normalizeScale( +...[dineroObjects]: NormalizeScaleParams +): Dinero[]; export { Rates } -export { RoundingOptions } - // @public -export function subtract(...[minuend, subtrahend]: SubtractParams): Dinero; +export function subtract( +...[minuend, subtrahend]: SubtractParams +): Dinero; -// @public -export function toFormat(...[dineroObject, transformer]: ToFormatParams): string; +// @public (undocumented) +export function toDecimal(dineroObject: Dinero): string; + +// @public (undocumented) +export function toDecimal(dineroObject: Dinero, transformer: Transformer_2): TOutput; // @public export const toSnapshot: typeof toSnapshot_2; -// @public -export function toUnit(...[dineroObject, options]: ToUnitParams): number; +// @public (undocumented) +export function toUnits(dineroObject: Dinero): readonly TAmount[]; + +// @public (undocumented) +export function toUnits(dineroObject: Dinero, transformer: Transformer_2): TOutput; export { Transformer_2 as Transformer } // @public -export function transformScale(...[dineroObject, newScale]: TransformScaleParams): Dinero; +export function transformScale( +...[dineroObject, newScale, divide]: TransformScaleParams +): Dinero; // @public -export function trimScale(...[dineroObject]: TrimScaleParams): Dinero; +export function trimScale( +...[dineroObject]: TrimScaleParams +): Dinero; export { up } diff --git a/packages/dinero.js/package.json b/packages/dinero.js/package.json index dc1cfbc0a..d291fae6a 100644 --- a/packages/dinero.js/package.json +++ b/packages/dinero.js/package.json @@ -47,5 +47,8 @@ "@dinero.js/calculator-number": "2.0.0-alpha.10", "@dinero.js/core": "2.0.0-alpha.10", "@dinero.js/currencies": "2.0.0-alpha.10" + }, + "devDependencies": { + "@dinero.js/calculator-bigint": "2.0.0-alpha.10" } } diff --git a/packages/dinero.js/src/api/__tests__/hasSubUnits.test.ts b/packages/dinero.js/src/api/__tests__/hasSubUnits.test.ts index c57cbc0cb..ace860316 100644 --- a/packages/dinero.js/src/api/__tests__/hasSubUnits.test.ts +++ b/packages/dinero.js/src/api/__tests__/hasSubUnits.test.ts @@ -1,4 +1,4 @@ -import { USD } from '@dinero.js/currencies'; +import { MGA, USD } from '@dinero.js/currencies'; import Big from 'big.js'; import { castToBigintCurrency, @@ -13,84 +13,165 @@ import { hasSubUnits } from '..'; describe('hasSubUnits', () => { describe('number', () => { const dinero = createNumberDinero; + const GBP = { code: 'GBP', base: [20, 12], exponent: 1 }; - it('returns false when there are no sub-units', () => { - const d = dinero({ amount: 1100, currency: USD }); + describe('decimal currencies', () => { + it('returns false when there are no sub-units', () => { + const d = dinero({ amount: 1100, currency: USD }); - expect(hasSubUnits(d)).toBe(false); - }); - it('returns true when there are sub-units based on a custom scale', () => { - const d = dinero({ amount: 1100, currency: USD, scale: 3 }); + expect(hasSubUnits(d)).toBe(false); + }); + it('returns true when there are sub-units based on a custom scale', () => { + const d = dinero({ amount: 1100, currency: USD, scale: 3 }); - expect(hasSubUnits(d)).toBe(true); - }); - it('returns true when there are sub-units', () => { - const d = dinero({ amount: 1150, currency: USD }); + expect(hasSubUnits(d)).toBe(true); + }); + it('returns true when there are sub-units', () => { + const d = dinero({ amount: 1150, currency: USD }); + + expect(hasSubUnits(d)).toBe(true); + }); + it('returns false when there are no sub-units based on a custom scale', () => { + const d = dinero({ amount: 1150, currency: USD, scale: 1 }); - expect(hasSubUnits(d)).toBe(true); + expect(hasSubUnits(d)).toBe(false); + }); }); - it('returns false when there are no sub-units based on a custom scale', () => { - const d = dinero({ amount: 1150, currency: USD, scale: 1 }); + describe('non-decimal currencies', () => { + it('returns false when there are no sub-units', () => { + const d = dinero({ amount: 10, currency: MGA }); - expect(hasSubUnits(d)).toBe(false); + expect(hasSubUnits(d)).toBe(false); + }); + it('returns true when there are sub-units', () => { + const d = dinero({ amount: 11, currency: MGA }); + + expect(hasSubUnits(d)).toBe(true); + }); + it('returns false when there are no sub-units based on a multi-base', () => { + const d = dinero({ amount: 240, currency: GBP }); + + expect(hasSubUnits(d)).toBe(false); + }); + it('returns true when there are sub-units based on a multi-base', () => { + const d = dinero({ amount: 267, currency: GBP }); + + expect(hasSubUnits(d)).toBe(true); + }); }); }); describe('bigint', () => { const dinero = createBigintDinero; const bigintUSD = castToBigintCurrency(USD); + const bigintMGA = castToBigintCurrency(MGA); + const bigintGBP = { code: 'GBP', base: [20n, 12n], exponent: 1n }; - it('returns false when there are no sub-units', () => { - const d = dinero({ amount: 1100n, currency: bigintUSD }); + describe('decimal currencies', () => { + it('returns false when there are no sub-units', () => { + const d = dinero({ amount: 1100n, currency: bigintUSD }); - expect(hasSubUnits(d)).toBe(false); - }); - it('returns true when there are sub-units based on a custom scale', () => { - const d = dinero({ amount: 1100n, currency: bigintUSD, scale: 3n }); + expect(hasSubUnits(d)).toBe(false); + }); + it('returns true when there are sub-units based on a custom scale', () => { + const d = dinero({ amount: 1100n, currency: bigintUSD, scale: 3n }); - expect(hasSubUnits(d)).toBe(true); - }); - it('returns true when there are sub-units', () => { - const d = dinero({ amount: 1150n, currency: bigintUSD }); + expect(hasSubUnits(d)).toBe(true); + }); + it('returns true when there are sub-units', () => { + const d = dinero({ amount: 1150n, currency: bigintUSD }); + + expect(hasSubUnits(d)).toBe(true); + }); + it('returns false when there are no sub-units based on a custom scale', () => { + const d = dinero({ amount: 1150n, currency: bigintUSD, scale: 1n }); - expect(hasSubUnits(d)).toBe(true); + expect(hasSubUnits(d)).toBe(false); + }); }); - it('returns false when there are no sub-units based on a custom scale', () => { - const d = dinero({ amount: 1150n, currency: bigintUSD, scale: 1n }); + describe('non-decimal currencies', () => { + it('returns false when there are no sub-units', () => { + const d = dinero({ amount: 10n, currency: bigintMGA }); + + expect(hasSubUnits(d)).toBe(false); + }); + it('returns true when there are sub-units', () => { + const d = dinero({ amount: 11n, currency: bigintMGA }); + + expect(hasSubUnits(d)).toBe(true); + }); + it('returns false when there are no sub-units based on a multi-base', () => { + const d = dinero({ amount: 240n, currency: bigintGBP }); + + expect(hasSubUnits(d)).toBe(false); + }); + it('returns true when there are sub-units based on a multi-base', () => { + const d = dinero({ amount: 267n, currency: bigintGBP }); - expect(hasSubUnits(d)).toBe(false); + expect(hasSubUnits(d)).toBe(true); + }); }); }); describe('Big.js', () => { const dinero = createBigjsDinero; const bigjsUSD = castToBigjsCurrency(USD); + const bigjsMGA = castToBigjsCurrency(MGA); + const bigjsGBP = { + code: 'GBP', + base: [new Big(20), new Big(12)], + exponent: new Big(1), + }; + + describe('decimal currencies', () => { + it('returns false when there are no sub-units', () => { + const d = dinero({ amount: new Big(1100), currency: bigjsUSD }); + + expect(hasSubUnits(d)).toBe(false); + }); + it('returns true when there are sub-units based on a custom scale', () => { + const d = dinero({ + amount: new Big(1100), + currency: bigjsUSD, + scale: new Big(3), + }); + + expect(hasSubUnits(d)).toBe(true); + }); + it('returns true when there are sub-units', () => { + const d = dinero({ amount: new Big(1150), currency: bigjsUSD }); - it('returns false when there are no sub-units', () => { - const d = dinero({ amount: new Big(1100), currency: bigjsUSD }); - - expect(hasSubUnits(d)).toBe(false); + expect(hasSubUnits(d)).toBe(true); + }); + it('returns false when there are no sub-units based on a custom scale', () => { + const d = dinero({ + amount: new Big(1150), + currency: bigjsUSD, + scale: new Big(1), + }); + + expect(hasSubUnits(d)).toBe(false); + }); }); - it('returns true when there are sub-units based on a custom scale', () => { - const d = dinero({ - amount: new Big(1100), - currency: bigjsUSD, - scale: new Big(3), + describe('non-decimal currencies', () => { + it('returns false when there are no sub-units', () => { + const d = dinero({ amount: new Big(10), currency: bigjsMGA }); + + expect(hasSubUnits(d)).toBe(false); }); + it('returns true when there are sub-units', () => { + const d = dinero({ amount: new Big(11), currency: bigjsMGA }); - expect(hasSubUnits(d)).toBe(true); - }); - it('returns true when there are sub-units', () => { - const d = dinero({ amount: new Big(1150), currency: bigjsUSD }); + expect(hasSubUnits(d)).toBe(true); + }); + it('returns false when there are no sub-units based on a multi-base', () => { + const d = dinero({ amount: new Big(240), currency: bigjsGBP }); - expect(hasSubUnits(d)).toBe(true); - }); - it('returns false when there are no sub-units based on a custom scale', () => { - const d = dinero({ - amount: new Big(1150), - currency: bigjsUSD, - scale: new Big(1), + expect(hasSubUnits(d)).toBe(false); }); + it('returns true when there are sub-units based on a multi-base', () => { + const d = dinero({ amount: new Big(267), currency: bigjsGBP }); - expect(hasSubUnits(d)).toBe(false); + expect(hasSubUnits(d)).toBe(true); + }); }); }); }); diff --git a/packages/dinero.js/src/api/__tests__/haveSameCurrency.test.ts b/packages/dinero.js/src/api/__tests__/haveSameCurrency.test.ts index 5fbedcc84..c2b787821 100644 --- a/packages/dinero.js/src/api/__tests__/haveSameCurrency.test.ts +++ b/packages/dinero.js/src/api/__tests__/haveSameCurrency.test.ts @@ -44,6 +44,25 @@ describe('haveSameCurrency', () => { }, }); + expect(haveSameCurrency([d1, d2])).toBe(true); + }); + it('returns true when multi-base currencies are structurally equal', () => { + const GBP = { code: 'GBP', base: [20, 12], exponent: 1 }; + const d1 = dinero({ amount: 240, currency: GBP }); + const d2 = dinero({ amount: 240, currency: GBP }); + + expect(haveSameCurrency([d1, d2])).toBe(true); + }); + it('returns true when multi-base currencies compute to the same base', () => { + const d1 = dinero({ + amount: 240, + currency: { code: 'GBP', base: [20, 12], exponent: 1 }, + }); + const d2 = dinero({ + amount: 240, + currency: { code: 'GBP', base: 240, exponent: 1 }, + }); + expect(haveSameCurrency([d1, d2])).toBe(true); }); }); @@ -82,6 +101,25 @@ describe('haveSameCurrency', () => { }, }); + expect(haveSameCurrency([d1, d2])).toBe(true); + }); + it('returns true when multi-base currencies are structurally equal', () => { + const GBP = { code: 'GBP', base: [20n, 12n], exponent: 1n }; + const d1 = dinero({ amount: 240n, currency: GBP }); + const d2 = dinero({ amount: 240n, currency: GBP }); + + expect(haveSameCurrency([d1, d2])).toBe(true); + }); + it('returns true when multi-base currencies compute to the same base', () => { + const d1 = dinero({ + amount: 240n, + currency: { code: 'GBP', base: [20n, 12n], exponent: 1n }, + }); + const d2 = dinero({ + amount: 240n, + currency: { code: 'GBP', base: 240n, exponent: 1n }, + }); + expect(haveSameCurrency([d1, d2])).toBe(true); }); }); @@ -120,6 +158,33 @@ describe('haveSameCurrency', () => { }, }); + expect(haveSameCurrency([d1, d2])).toBe(true); + }); + it('returns true when multi-base currencies are structurally equal', () => { + const GBP = { + code: 'GBP', + base: [new Big(20), new Big(20)], + exponent: new Big(1), + }; + const d1 = dinero({ amount: new Big(240), currency: GBP }); + const d2 = dinero({ amount: new Big(240), currency: GBP }); + + expect(haveSameCurrency([d1, d2])).toBe(true); + }); + it('returns true when multi-base currencies compute to the same base', () => { + const d1 = dinero({ + amount: new Big(240), + currency: { + code: 'GBP', + base: [new Big(20), new Big(12)], + exponent: new Big(1), + }, + }); + const d2 = dinero({ + amount: new Big(240), + currency: { code: 'GBP', base: new Big(240), exponent: new Big(1) }, + }); + expect(haveSameCurrency([d1, d2])).toBe(true); }); }); diff --git a/packages/dinero.js/src/api/__tests__/toDecimal.test.ts b/packages/dinero.js/src/api/__tests__/toDecimal.test.ts new file mode 100644 index 000000000..56206d24b --- /dev/null +++ b/packages/dinero.js/src/api/__tests__/toDecimal.test.ts @@ -0,0 +1,222 @@ +import { MGA, USD } from '@dinero.js/currencies'; +import Big from 'big.js'; +import { + castToBigintCurrency, + castToBigjsCurrency, + createNumberDinero, + createBigintDinero, + createBigjsDinero, +} from 'test-utils'; + +import { toDecimal } from '..'; + +describe('toDecimal', () => { + describe('number', () => { + const dinero = createNumberDinero; + + describe('decimal currencies', () => { + it('returns the amount in decimal format', () => { + const d = dinero({ amount: 1050, currency: USD }); + + expect(toDecimal(d)).toEqual('10.50'); + }); + it('returns the amount in decimal format based on a custom scale', () => { + const d = dinero({ amount: 10545, currency: USD, scale: 3 }); + + expect(toDecimal(d)).toEqual('10.545'); + }); + it('returns the amount in decimal format with trailing zeros', () => { + const d = dinero({ amount: 1000, currency: USD }); + + expect(toDecimal(d)).toBe('10.00'); + }); + it('returns the amount in decimal format with leading zeros', () => { + const d = dinero({ amount: 1005, currency: USD }); + + expect(toDecimal(d)).toBe('10.05'); + }); + it('returns the amount in decimal format and pads the decimal part', () => { + const d = dinero({ amount: 500, currency: USD }); + + expect(toDecimal(d)).toBe('5.00'); + }); + it('uses a custom transformer', () => { + const d = dinero({ amount: 1050, currency: USD }); + + expect( + toDecimal(d, ({ value, currency }) => `${currency.code} ${value}`) + ).toBe('USD 10.50'); + }); + }); + describe('non-decimal currencies', () => { + it('throws when passing a Dinero object using a non-decimal currency', () => { + const d = dinero({ amount: 13, currency: MGA }); + + expect(() => { + toDecimal(d); + }).toThrowErrorMatchingInlineSnapshot( + `"[Dinero.js] Currency is not decimal."` + ); + }); + it('throws when passing a Dinero object using a multi-base currency which compiles to a multiple of 10', () => { + const d = dinero({ + amount: 13, + currency: { code: 'ABC', exponent: 1, base: [5, 2] }, + }); + + expect(() => { + toDecimal(d); + }).toThrowErrorMatchingInlineSnapshot( + `"[Dinero.js] Currency is not decimal."` + ); + }); + }); + }); + describe('bigint', () => { + const dinero = createBigintDinero; + const bigintUSD = castToBigintCurrency(USD); + const bigintMGA = castToBigintCurrency(MGA); + + describe('decimal currencies', () => { + it('returns the amount in decimal format', () => { + const d = dinero({ amount: 1050n, currency: bigintUSD }); + + expect(toDecimal(d)).toEqual('10.50'); + }); + it('returns the amount in decimal format with large integers', () => { + const d = dinero({ amount: 1000000000000000050n, currency: bigintUSD }); + + expect(toDecimal(d)).toEqual('10000000000000000.50'); + }); + it('returns the amount in decimal format based on a custom scale', () => { + const d = dinero({ amount: 10545n, currency: bigintUSD, scale: 3n }); + + expect(toDecimal(d)).toEqual('10.545'); + }); + it('returns the amount in decimal format with trailing zeros', () => { + const d = dinero({ amount: 1000n, currency: bigintUSD }); + + expect(toDecimal(d)).toBe('10.00'); + }); + it('returns the amount in decimal format with leading zeros', () => { + const d = dinero({ amount: 1005n, currency: bigintUSD }); + + expect(toDecimal(d)).toBe('10.05'); + }); + it('returns the amount in decimal format and pads the decimal part', () => { + const d = dinero({ amount: 500n, currency: bigintUSD }); + + expect(toDecimal(d)).toBe('5.00'); + }); + it('uses a custom transformer', () => { + const d = dinero({ amount: 1050n, currency: bigintUSD }); + + expect( + toDecimal(d, ({ value, currency }) => `${currency.code} ${value}`) + ).toBe('USD 10.50'); + }); + }); + describe('non-decimal currencies', () => { + it('throws when passing a Dinero object using a non-decimal currency', () => { + const d = dinero({ amount: 13n, currency: bigintMGA }); + + expect(() => { + toDecimal(d); + }).toThrowErrorMatchingInlineSnapshot( + `"[Dinero.js] Currency is not decimal."` + ); + }); + it('throws when passing a Dinero object using a multi-base currency which compiles to a multiple of 10', () => { + const d = dinero({ + amount: 13n, + currency: { code: 'ABC', exponent: 1n, base: [5n, 2n] }, + }); + + expect(() => { + toDecimal(d); + }).toThrowErrorMatchingInlineSnapshot( + `"[Dinero.js] Currency is not decimal."` + ); + }); + }); + }); + describe('Big.js', () => { + const dinero = createBigjsDinero; + const bigjsUSD = castToBigjsCurrency(USD); + const bigjsMGA = castToBigjsCurrency(MGA); + + describe('decimal currencies', () => { + it('returns the amount in decimal format', () => { + const d = dinero({ amount: new Big(1050), currency: bigjsUSD }); + + expect(toDecimal(d)).toEqual('10.50'); + }); + it('returns the amount in decimal format with large integers', () => { + const d = dinero({ + amount: new Big('1000000000000000050'), + currency: bigjsUSD, + }); + + expect(toDecimal(d)).toEqual('10000000000000000.50'); + }); + it('returns the amount in decimal format based on a custom scale', () => { + const d = dinero({ + amount: new Big(10545), + currency: bigjsUSD, + scale: new Big(3), + }); + + expect(toDecimal(d)).toEqual('10.545'); + }); + it('returns the amount in decimal format with trailing zeros', () => { + const d = dinero({ amount: new Big(1000), currency: bigjsUSD }); + + expect(toDecimal(d)).toBe('10.00'); + }); + it('returns the amount in decimal format with leading zeros', () => { + const d = dinero({ amount: new Big(1005), currency: bigjsUSD }); + + expect(toDecimal(d)).toBe('10.05'); + }); + it('returns the amount in decimal format and pads the decimal part', () => { + const d = dinero({ amount: new Big(500), currency: bigjsUSD }); + + expect(toDecimal(d)).toBe('5.00'); + }); + it('uses a custom transformer', () => { + const d = dinero({ amount: new Big(1050), currency: bigjsUSD }); + + expect( + toDecimal(d, ({ value, currency }) => `${currency.code} ${value}`) + ).toBe('USD 10.50'); + }); + }); + describe('non-decimal currencies', () => { + it('throws when passing a Dinero object using a non-decimal currency', () => { + const d = dinero({ amount: new Big(13), currency: bigjsMGA }); + + expect(() => { + toDecimal(d); + }).toThrowErrorMatchingInlineSnapshot( + `"[Dinero.js] Currency is not decimal."` + ); + }); + it('throws when passing a Dinero object using a multi-base currency which compiles to a multiple of 10', () => { + const d = dinero({ + amount: new Big(13), + currency: { + code: 'ABC', + exponent: new Big(1), + base: [new Big(5), new Big(2)], + }, + }); + + expect(() => { + toDecimal(d); + }).toThrowErrorMatchingInlineSnapshot( + `"[Dinero.js] Currency is not decimal."` + ); + }); + }); + }); +}); diff --git a/packages/dinero.js/src/api/__tests__/toFormat.test.ts b/packages/dinero.js/src/api/__tests__/toFormat.test.ts deleted file mode 100644 index 4a8177ec5..000000000 --- a/packages/dinero.js/src/api/__tests__/toFormat.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { USD } from '@dinero.js/currencies'; -import Big from 'big.js'; -import { - castToBigintCurrency, - castToBigjsCurrency, - createNumberDinero, - createBigintDinero, - createBigjsDinero, -} from 'test-utils'; - -import { toFormat } from '..'; - -describe('toFormat', () => { - describe('number', () => { - const dinero = createNumberDinero; - - it('formats the Dinero object with the passed transformer', () => { - const formatter = ({ amount, currency }) => `${currency.code} ${amount}`; - const d = dinero({ amount: 500, currency: USD }); - - expect(toFormat(d, formatter)).toBe('USD 5'); - }); - it('formats the Dinero object with the passed transformer using the scale', () => { - const formatter = ({ amount, currency }) => `${currency.code} ${amount}`; - const d = dinero({ amount: 4545, currency: USD, scale: 3 }); - - expect(toFormat(d, formatter)).toBe('USD 4.545'); - }); - }); - describe('bigint', () => { - const dinero = createBigintDinero; - const bigintUSD = castToBigintCurrency(USD); - - it('formats the Dinero object with the passed transformer', () => { - const formatter = ({ amount, currency }) => `${currency.code} ${amount}`; - const d = dinero({ amount: 500n, currency: bigintUSD }); - - expect(toFormat(d, formatter)).toBe('USD 5'); - }); - it('formats the Dinero object with the passed transformer using the scale', () => { - const formatter = ({ amount, currency }) => `${currency.code} ${amount}`; - const d = dinero({ amount: 4545n, currency: bigintUSD, scale: 3n }); - - expect(toFormat(d, formatter)).toBe('USD 4.545'); - }); - }); - describe('Big.js', () => { - const dinero = createBigjsDinero; - const bigjsUSD = castToBigjsCurrency(USD); - - it('formats the Dinero object with the passed transformer', () => { - const formatter = ({ amount, currency }) => `${currency.code} ${amount}`; - const d = dinero({ amount: new Big(500), currency: bigjsUSD }); - - expect(toFormat(d, formatter)).toBe('USD 5'); - }); - it('formats the Dinero object with the passed transformer using the scale', () => { - const formatter = ({ amount, currency }) => `${currency.code} ${amount}`; - const d = dinero({ - amount: new Big(4545), - currency: bigjsUSD, - scale: new Big(3), - }); - - expect(toFormat(d, formatter)).toBe('USD 4.545'); - }); - }); -}); diff --git a/packages/dinero.js/src/api/__tests__/toUnit.test.ts b/packages/dinero.js/src/api/__tests__/toUnit.test.ts deleted file mode 100644 index 9d06763b0..000000000 --- a/packages/dinero.js/src/api/__tests__/toUnit.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { down } from '@dinero.js/core'; -import { USD } from '@dinero.js/currencies'; - -import { toUnit } from '..'; -import { dinero } from '../..'; - -describe('toUnit', () => { - it('returns the amount in currency unit', () => { - const d = dinero({ amount: 1050, currency: USD }); - - expect(toUnit(d)).toBe(10.5); - }); - it('returns the amount in currency unit, based on a custom scale', () => { - const d = dinero({ amount: 10545, currency: USD, scale: 3 }); - - expect(toUnit(d)).toBe(10.545); - }); - it('returns the amount in currency unit, rounded to one fraction digit', () => { - const d = dinero({ amount: 1055, currency: USD }); - - expect(toUnit(d, { digits: 1, round: down })).toBe(10.5); - }); - it('returns the negative amount in currency unit, rounded to one fraction digit', () => { - const d = dinero({ amount: -1055, currency: USD }); - - expect(toUnit(d, { digits: 1, round: down })).toBe(-10.6); - }); - it('returns the amount in currency unit, rounded to two fraction digits', () => { - const d = dinero({ amount: 1055, currency: USD }); - - expect(toUnit(d, { digits: 2, round: down })).toBe(10.55); - }); - it('returns the amount in currency unit, rounded to no fraction digit', () => { - const d = dinero({ amount: 1055, currency: USD }); - - expect(toUnit(d, { digits: 0, round: down })).toBe(10); - }); -}); diff --git a/packages/dinero.js/src/api/__tests__/toUnits.test.ts b/packages/dinero.js/src/api/__tests__/toUnits.test.ts new file mode 100644 index 000000000..7095da44c --- /dev/null +++ b/packages/dinero.js/src/api/__tests__/toUnits.test.ts @@ -0,0 +1,227 @@ +import { USD } from '@dinero.js/currencies'; +import Big from 'big.js'; + +import { toUnits } from '..'; +import { + castToBigintCurrency, + castToBigjsCurrency, + createNumberDinero, + createBigintDinero, + createBigjsDinero, +} from '../../../../../test/utils'; + +describe('toUnits', () => { + describe('number', () => { + const dinero = createNumberDinero; + + describe('decimal currencies', () => { + it('returns the amount in currency units', () => { + const d = dinero({ amount: 1050, currency: USD }); + + expect(toUnits(d)).toEqual([10, 50]); + }); + it('returns the amount in currency units, based on a custom scale', () => { + const d = dinero({ amount: 10545, currency: USD, scale: 3 }); + + expect(toUnits(d)).toEqual([10, 545]); + }); + it('returns the amount in currency units, with a single trailing zero', () => { + const d = dinero({ amount: 1000, currency: USD }); + + expect(toUnits(d)).toEqual([10, 0]); + }); + it('uses a custom transformer', () => { + const d = dinero({ amount: 1050, currency: USD }); + + expect( + toUnits( + d, + ({ value, currency }) => `${value.join('.')} ${currency.code}` + ) + ).toBe('10.50 USD'); + }); + }); + describe('non-decimal currencies', () => { + it('returns the amount in currency units', () => { + const GRD = { code: 'GRD', base: 6, exponent: 1 }; + const d = dinero({ amount: 9, currency: GRD }); + + expect(toUnits(d)).toEqual([1, 3]); + }); + it('handles currencies with multiple subdivisions', () => { + const GBP = { code: 'GBP', base: [20, 12], exponent: 1 }; + const d = dinero({ amount: 267, currency: GBP }); + + expect(toUnits(d)).toEqual([1, 2, 3]); + }); + it('handles intermediary zero values', () => { + const GBP = { code: 'GBP', base: [20, 12], exponent: 1 }; + const d = dinero({ amount: 2, currency: GBP }); + + expect(toUnits(d)).toEqual([0, 0, 2]); + }); + it('uses a custom transformer', () => { + const GBP = { code: 'GBP', base: [20, 12], exponent: 1 }; + const d = dinero({ amount: 267, currency: GBP }); + const labels = ['pounds', 'shillings', 'pence']; + + expect( + toUnits(d, ({ value }) => + value + .filter((amount) => amount > 0) + .map((amount, index) => `${amount} ${labels[index]}`) + .join(', ') + ) + ).toBe('1 pounds, 2 shillings, 3 pence'); + }); + }); + }); + describe('bigint', () => { + const dinero = createBigintDinero; + const bigintUSD = castToBigintCurrency(USD); + + describe('decimal currencies', () => { + it('returns the amount in currency units', () => { + const d = dinero({ amount: 1050n, currency: bigintUSD }); + + expect(toUnits(d)).toEqual([10n, 50n]); + }); + it('returns the amount in currency units, based on a custom scale', () => { + const d = dinero({ amount: 10545n, currency: bigintUSD, scale: 3n }); + + expect(toUnits(d)).toEqual([10n, 545n]); + }); + it('returns the amount in currency units, with a single trailing zero', () => { + const d = dinero({ amount: 1000n, currency: bigintUSD }); + + expect(toUnits(d)).toEqual([10n, 0n]); + }); + it('uses a custom transformer', () => { + const d = dinero({ amount: 1050n, currency: bigintUSD }); + + expect( + toUnits( + d, + ({ value, currency }) => `${value.join('.')} ${currency.code}` + ) + ).toBe('10.50 USD'); + }); + }); + describe('non-decimal currencies', () => { + it('returns the amount in currency units', () => { + const GRD = { code: 'GRD', base: 6n, exponent: 1n }; + const d = dinero({ amount: 9n, currency: GRD }); + + expect(toUnits(d)).toEqual([1n, 3n]); + }); + it('handles currencies with multiple subdivisions', () => { + const GBP = { code: 'GBP', base: [20n, 12n], exponent: 1n }; + const d = dinero({ amount: 267n, currency: GBP }); + + expect(toUnits(d)).toEqual([1n, 2n, 3n]); + }); + it('handles intermediary zero values', () => { + const GBP = { code: 'GBP', base: [20n, 12n], exponent: 1n }; + const d = dinero({ amount: 2n, currency: GBP }); + + expect(toUnits(d)).toEqual([0n, 0n, 2n]); + }); + it('uses a custom transformer', () => { + const GBP = { code: 'GBP', base: [20n, 12n], exponent: 1n }; + const d = dinero({ amount: 267n, currency: GBP }); + const labels = ['pounds', 'shillings', 'pence']; + + expect( + toUnits(d, ({ value }) => + value + .filter((amount) => amount > 0n) + .map((amount, index) => `${amount} ${labels[index]}`) + .join(', ') + ) + ).toBe('1 pounds, 2 shillings, 3 pence'); + }); + }); + }); + describe('Big.js', () => { + const dinero = createBigjsDinero; + const bigjsUSD = castToBigjsCurrency(USD); + + describe('decimal currencies', () => { + it('returns the amount in currency units', () => { + const d = dinero({ amount: new Big(1050), currency: bigjsUSD }); + + expect(toUnits(d)).toEqual([new Big(10), new Big(50)]); + }); + it('returns the amount in currency units, based on a custom scale', () => { + const d = dinero({ + amount: new Big(10545), + currency: bigjsUSD, + scale: new Big(3), + }); + + expect(toUnits(d)).toEqual([new Big(10), new Big(545)]); + }); + it('returns the amount in currency units, with a single trailing zero', () => { + const d = dinero({ amount: new Big(1000), currency: bigjsUSD }); + + expect(toUnits(d)).toEqual([new Big(10), new Big(0)]); + }); + it('uses a custom transformer', () => { + const d = dinero({ amount: new Big(1050), currency: bigjsUSD }); + + expect( + toUnits( + d, + ({ value, currency }) => `${value.join('.')} ${currency.code}` + ) + ).toBe('10.50 USD'); + }); + }); + describe('non-decimal currencies', () => { + it('returns the amount in currency units', () => { + const GRD = { code: 'GRD', base: new Big(6), exponent: new Big(1) }; + const d = dinero({ amount: new Big(9), currency: GRD }); + + expect(toUnits(d)).toEqual([new Big(1), new Big(3)]); + }); + it('handles currencies with multiple subdivisions', () => { + const GBP = { + code: 'GBP', + base: [new Big(20), new Big(12)], + exponent: new Big(1), + }; + const d = dinero({ amount: new Big(267), currency: GBP }); + + expect(toUnits(d)).toEqual([new Big(1), new Big(2), new Big(3)]); + }); + it('handles intermediary zero values', () => { + const GBP = { + code: 'GBP', + base: [new Big(20), new Big(12)], + exponent: new Big(1), + }; + const d = dinero({ amount: new Big(2), currency: GBP }); + + expect(toUnits(d)).toEqual([new Big(0), new Big(0), new Big(2)]); + }); + it('uses a custom transformer', () => { + const GBP = { + code: 'GBP', + base: [new Big(20), new Big(12)], + exponent: new Big(1), + }; + const d = dinero({ amount: new Big(267), currency: GBP }); + const labels = ['pounds', 'shillings', 'pence']; + + expect( + toUnits(d, ({ value }) => + value + .filter((amount) => amount > new Big(0)) + .map((amount, index) => `${amount} ${labels[index]}`) + .join(', ') + ) + ).toBe('1 pounds, 2 shillings, 3 pence'); + }); + }); + }); +}); diff --git a/packages/dinero.js/src/api/__tests__/transformScale.test.ts b/packages/dinero.js/src/api/__tests__/transformScale.test.ts index 671eda2ca..689ddb613 100644 --- a/packages/dinero.js/src/api/__tests__/transformScale.test.ts +++ b/packages/dinero.js/src/api/__tests__/transformScale.test.ts @@ -1,4 +1,14 @@ -import { USD } from '@dinero.js/currencies'; +import { + down, + halfAwayFromZero, + halfDown, + halfEven, + halfOdd, + halfTowardsZero, + halfUp, + up, +} from '@dinero.js/core'; +import { USD, MGA } from '@dinero.js/currencies'; import Big from 'big.js'; import { castToBigintCurrency, @@ -10,118 +20,974 @@ import { import { toSnapshot, transformScale } from '..'; +const ABC = { code: 'ABC', base: 6, exponent: 1 }; + +type DivideOperation = Parameters[2]; + describe('transformScale', () => { describe('number', () => { const dinero = createNumberDinero; - it('returns a new Dinero object with a new scale and a converted amount', () => { - const d = dinero({ amount: 500, currency: USD, scale: 2 }); - const snapshot = toSnapshot(transformScale(d, 4)); + describe('decimal currencies', () => { + it('returns a new Dinero object with a new scale and a converted amount', () => { + const d = dinero({ amount: 500, currency: USD, scale: 2 }); + const snapshot = toSnapshot(transformScale(d, 4)); - expect(snapshot).toMatchObject({ amount: 50000, scale: 4 }); - }); - it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { - const d = dinero({ amount: 14270, currency: USD, scale: 2 }); - const snapshot = toSnapshot(transformScale(d, 0)); + expect(snapshot).toMatchObject({ amount: 50000, scale: 4 }); + }); + it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { + const d = dinero({ amount: 14270, currency: USD, scale: 2 }); + const snapshot = toSnapshot(transformScale(d, 0)); - expect(snapshot).toMatchObject({ amount: 142, scale: 0 }); - }); - it('converts between scales correctly', () => { - const d = dinero({ amount: 333336, currency: USD, scale: 5 }); - const snapshot = toSnapshot(transformScale(d, 2)); + expect(snapshot).toMatchObject({ amount: 142, scale: 0 }); + }); + it('converts between scales correctly', () => { + const d = dinero({ amount: 333336, currency: USD, scale: 5 }); + const snapshot = toSnapshot(transformScale(d, 2)); + + expect(snapshot).toMatchObject({ amount: 333, scale: 2 }); + }); + it('converts from long initial scales correctly', () => { + const d = dinero({ amount: 3333333336, currency: USD, scale: 9 }); + const snapshot = toSnapshot(transformScale(d, 2)); + + expect(snapshot).toMatchObject({ amount: 333, scale: 2 }); + }); + it('uses the provided `up` divide function', () => { + const d = dinero({ amount: 10455, currency: USD, scale: 3 }); + + const snapshot = toSnapshot(transformScale(d, 2, up)); + + expect(snapshot).toMatchObject({ amount: 1046, scale: 2 }); + }); + it('uses the provided `down` divide function', () => { + const d = dinero({ amount: 10455, currency: USD, scale: 3 }); + + const snapshot = toSnapshot(transformScale(d, 2, down)); + + expect(snapshot).toMatchObject({ amount: 1045, scale: 2 }); + }); + it('uses the provided `halfOdd` divide function', () => { + const d1 = dinero({ amount: 10415, currency: USD, scale: 3 }); + const d2 = dinero({ amount: 10425, currency: USD, scale: 3 }); + + expect(toSnapshot(transformScale(d1, 2, halfOdd))).toMatchObject({ + amount: 1041, + scale: 2, + }); + expect(toSnapshot(transformScale(d2, 2, halfOdd))).toMatchObject({ + amount: 1043, + scale: 2, + }); + }); + it('uses the provided `halfEven` divide function', () => { + const d1 = dinero({ amount: 10425, currency: USD, scale: 3 }); + const d2 = dinero({ amount: 10435, currency: USD, scale: 3 }); + + expect(toSnapshot(transformScale(d1, 2, halfEven))).toMatchObject({ + amount: 1042, + scale: 2, + }); + expect(toSnapshot(transformScale(d2, 2, halfEven))).toMatchObject({ + amount: 1044, + scale: 2, + }); + }); + it('uses the provided `halfDown` divide function', () => { + const d1 = dinero({ amount: 10455, currency: USD, scale: 3 }); + const d2 = dinero({ amount: 10456, currency: USD, scale: 3 }); + + expect(toSnapshot(transformScale(d1, 2, halfDown))).toMatchObject({ + amount: 1045, + scale: 2, + }); + expect(toSnapshot(transformScale(d2, 2, halfDown))).toMatchObject({ + amount: 1046, + scale: 2, + }); + }); + it('uses the provided `halfUp` divide function', () => { + const d1 = dinero({ amount: 10454, currency: USD, scale: 3 }); + const d2 = dinero({ amount: 10455, currency: USD, scale: 3 }); + + expect(toSnapshot(transformScale(d1, 2, halfUp))).toMatchObject({ + amount: 1045, + scale: 2, + }); + expect(toSnapshot(transformScale(d2, 2, halfUp))).toMatchObject({ + amount: 1046, + scale: 2, + }); + }); + it('uses the provided `halfTowardsZero` divide function', () => { + const d1 = dinero({ amount: 10415, currency: USD, scale: 3 }); + + const snapshot = toSnapshot(transformScale(d1, 2, halfTowardsZero)); + + expect(snapshot).toMatchObject({ + amount: 1041, + scale: 2, + }); + }); + it('uses the provided `halfAwayFromZero` divide function', () => { + const d1 = dinero({ amount: 10415, currency: USD, scale: 3 }); + + const snapshot = toSnapshot(transformScale(d1, 2, halfAwayFromZero)); + + expect(snapshot).toMatchObject({ + amount: 1042, + scale: 2, + }); + }); + it('uses a custom divide function', () => { + const divideFn = jest.fn(() => 1045) as DivideOperation; + const d = dinero({ amount: 10455, currency: USD, scale: 3 }); + + const snapshot = toSnapshot(transformScale(d, 2, divideFn)); - expect(snapshot).toMatchObject({ amount: 333, scale: 2 }); + expect(snapshot).toMatchObject({ amount: 1045, scale: 2 }); + expect(divideFn).toHaveBeenNthCalledWith( + 1, + 10455, + 10, + expect.objectContaining({ + add: expect.any(Function), + compare: expect.any(Function), + decrement: expect.any(Function), + increment: expect.any(Function), + integerDivide: expect.any(Function), + modulo: expect.any(Function), + multiply: expect.any(Function), + power: expect.any(Function), + subtract: expect.any(Function), + zero: expect.any(Function), + }) + ); + }); }); - it('converts from long initial scales correctly', () => { - const d = dinero({ amount: 3333333336, currency: USD, scale: 9 }); - const snapshot = toSnapshot(transformScale(d, 2)); + describe('non-decimal currencies', () => { + it('returns a new Dinero object with a new scale and a converted amount', () => { + const d = dinero({ amount: 5, currency: MGA }); + const snapshot = toSnapshot(transformScale(d, 2)); + + expect(snapshot).toMatchObject({ amount: 25, scale: 2 }); + }); + it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { + const d = dinero({ amount: 26, currency: MGA, scale: 2 }); + const snapshot = toSnapshot(transformScale(d, 1)); - expect(snapshot).toMatchObject({ amount: 333, scale: 2 }); + expect(snapshot).toMatchObject({ amount: 5, scale: 1 }); + }); + it('uses the provided `up` divide function', () => { + const d = dinero({ amount: 33, currency: ABC, scale: 2 }); + const snapshot = toSnapshot(transformScale(d, 1, up)); + + expect(snapshot).toMatchObject({ amount: 6, scale: 1 }); + }); + it('uses the provided `down` divide function', () => { + const d = dinero({ amount: 33, currency: ABC, scale: 2 }); + const snapshot = toSnapshot(transformScale(d, 1, down)); + + expect(snapshot).toMatchObject({ amount: 5, scale: 1 }); + }); + it('uses the provided `halfOdd` divide function', () => { + const d1 = dinero({ amount: 33, currency: ABC, scale: 2 }); + const d2 = dinero({ amount: 39, currency: ABC, scale: 2 }); + + expect(toSnapshot(transformScale(d1, 1, halfOdd))).toMatchObject({ + amount: 5, + scale: 1, + }); + expect(toSnapshot(transformScale(d2, 1, halfOdd))).toMatchObject({ + amount: 7, + scale: 1, + }); + }); + it('uses the provided `halfEven` divide function', () => { + const d1 = dinero({ amount: 33, currency: ABC, scale: 2 }); + const d2 = dinero({ amount: 39, currency: ABC, scale: 2 }); + + expect(toSnapshot(transformScale(d1, 1, halfEven))).toMatchObject({ + amount: 6, + scale: 1, + }); + expect(toSnapshot(transformScale(d2, 1, halfEven))).toMatchObject({ + amount: 6, + scale: 1, + }); + }); + it('uses the provided `halfDown` divide function', () => { + const d1 = dinero({ amount: 33, currency: ABC, scale: 2 }); + const d2 = dinero({ amount: 39, currency: ABC, scale: 2 }); + + expect(toSnapshot(transformScale(d1, 1, halfDown))).toMatchObject({ + amount: 5, + scale: 1, + }); + expect(toSnapshot(transformScale(d2, 1, halfDown))).toMatchObject({ + amount: 6, + scale: 1, + }); + }); + it('uses the provided `halfUp` divide function', () => { + const d1 = dinero({ amount: 33, currency: ABC, scale: 2 }); + const d2 = dinero({ amount: 39, currency: ABC, scale: 2 }); + + expect(toSnapshot(transformScale(d1, 1, halfUp))).toMatchObject({ + amount: 6, + scale: 1, + }); + expect(toSnapshot(transformScale(d2, 1, halfUp))).toMatchObject({ + amount: 7, + scale: 1, + }); + }); + it('uses the provided `halfTowardsZero` divide function', () => { + const d1 = dinero({ amount: 33, currency: ABC, scale: 2 }); + const d2 = dinero({ amount: 39, currency: ABC, scale: 2 }); + + expect( + toSnapshot(transformScale(d1, 1, halfTowardsZero)) + ).toMatchObject({ + amount: 5, + scale: 1, + }); + expect( + toSnapshot(transformScale(d2, 1, halfTowardsZero)) + ).toMatchObject({ + amount: 6, + scale: 1, + }); + }); + it('uses the provided `halfAwayFromZero` divide function', () => { + const d1 = dinero({ amount: 33, currency: ABC, scale: 2 }); + const d2 = dinero({ amount: 39, currency: ABC, scale: 2 }); + + expect( + toSnapshot(transformScale(d1, 1, halfAwayFromZero)) + ).toMatchObject({ + amount: 6, + scale: 1, + }); + expect( + toSnapshot(transformScale(d2, 1, halfAwayFromZero)) + ).toMatchObject({ + amount: 7, + scale: 1, + }); + }); }); }); describe('bigint', () => { const dinero = createBigintDinero; const bigintUSD = castToBigintCurrency(USD); + const bigintMGA = castToBigintCurrency(MGA); + const bigintABC = castToBigintCurrency(ABC); - it('returns a new Dinero object with a new scale and a converted amount', () => { - const d = dinero({ amount: 500n, currency: bigintUSD, scale: 2n }); - const snapshot = toSnapshot(transformScale(d, 4n)); + describe('decimal currencies', () => { + it('returns a new Dinero object with a new scale and a converted amount', () => { + const d = dinero({ amount: 500n, currency: bigintUSD, scale: 2n }); + const snapshot = toSnapshot(transformScale(d, 4n)); - expect(snapshot).toMatchObject({ amount: 50000n, scale: 4n }); - }); - it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { - const d = dinero({ amount: 14270n, currency: bigintUSD, scale: 2n }); - const snapshot = toSnapshot(transformScale(d, 0n)); + expect(snapshot).toMatchObject({ amount: 50000n, scale: 4n }); + }); + it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { + const d = dinero({ amount: 14270n, currency: bigintUSD, scale: 2n }); + const snapshot = toSnapshot(transformScale(d, 0n)); - expect(snapshot).toMatchObject({ amount: 142n, scale: 0n }); - }); - it('converts between scales correctly', () => { - const d = dinero({ amount: 333336n, currency: bigintUSD, scale: 5n }); - const snapshot = toSnapshot(transformScale(d, 2n)); + expect(snapshot).toMatchObject({ amount: 142n, scale: 0n }); + }); + it('converts between scales correctly', () => { + const d = dinero({ amount: 333336n, currency: bigintUSD, scale: 5n }); + const snapshot = toSnapshot(transformScale(d, 2n)); + + expect(snapshot).toMatchObject({ amount: 333n, scale: 2n }); + }); + it('converts from long initial scales correctly', () => { + const d = dinero({ + amount: 3333333336n, + currency: bigintUSD, + scale: 9n, + }); + const snapshot = toSnapshot(transformScale(d, 2n)); + + expect(snapshot).toMatchObject({ amount: 333n, scale: 2n }); + }); + it('uses the provided `up` divide function', () => { + const d = dinero({ amount: 10455n, currency: bigintUSD, scale: 3n }); + + const snapshot = toSnapshot(transformScale(d, 2n, up)); + + expect(snapshot).toMatchObject({ amount: 1046n, scale: 2n }); + }); + it('uses the provided `down` divide function', () => { + const d = dinero({ amount: 10455n, currency: bigintUSD, scale: 3n }); + + const snapshot = toSnapshot(transformScale(d, 2n, down)); + + expect(snapshot).toMatchObject({ amount: 1045n, scale: 2n }); + }); + it('uses the provided `halfOdd` divide function', () => { + const d1 = dinero({ amount: 10415n, currency: bigintUSD, scale: 3n }); + const d2 = dinero({ amount: 10425n, currency: bigintUSD, scale: 3n }); + + expect(toSnapshot(transformScale(d1, 2n, halfOdd))).toMatchObject({ + amount: 1041n, + scale: 2n, + }); + expect(toSnapshot(transformScale(d2, 2n, halfOdd))).toMatchObject({ + amount: 1043n, + scale: 2n, + }); + }); + it('uses the provided `halfEven` divide function', () => { + const d1 = dinero({ amount: 10425n, currency: bigintUSD, scale: 3n }); + const d2 = dinero({ amount: 10435n, currency: bigintUSD, scale: 3n }); + + expect(toSnapshot(transformScale(d1, 2n, halfEven))).toMatchObject({ + amount: 1042n, + scale: 2n, + }); + expect(toSnapshot(transformScale(d2, 2n, halfEven))).toMatchObject({ + amount: 1044n, + scale: 2n, + }); + }); + it('uses the provided `halfDown` divide function', () => { + const d1 = dinero({ amount: 10455n, currency: bigintUSD, scale: 3n }); + const d2 = dinero({ amount: 10456n, currency: bigintUSD, scale: 3n }); + + expect(toSnapshot(transformScale(d1, 2n, halfDown))).toMatchObject({ + amount: 1045n, + scale: 2n, + }); + expect(toSnapshot(transformScale(d2, 2n, halfDown))).toMatchObject({ + amount: 1046n, + scale: 2n, + }); + }); + it('uses the provided `halfUp` divide function', () => { + const d1 = dinero({ amount: 10454n, currency: bigintUSD, scale: 3n }); + const d2 = dinero({ amount: 10455n, currency: bigintUSD, scale: 3n }); + + expect(toSnapshot(transformScale(d1, 2n, halfUp))).toMatchObject({ + amount: 1045n, + scale: 2n, + }); + expect(toSnapshot(transformScale(d2, 2n, halfUp))).toMatchObject({ + amount: 1046n, + scale: 2n, + }); + }); + it('uses the provided `halfTowardsZero` divide function', () => { + const d1 = dinero({ amount: 10415n, currency: bigintUSD, scale: 3n }); + + const snapshot = toSnapshot(transformScale(d1, 2n, halfTowardsZero)); + + expect(snapshot).toMatchObject({ + amount: 1041n, + scale: 2n, + }); + }); + it('uses the provided `halfAwayFromZero` divide function', () => { + const d1 = dinero({ amount: 10415n, currency: bigintUSD, scale: 3n }); + + const snapshot = toSnapshot(transformScale(d1, 2n, halfAwayFromZero)); + + expect(snapshot).toMatchObject({ + amount: 1042n, + scale: 2n, + }); + }); + it('uses a custom divide function', () => { + const divideFn = jest.fn(() => 1045n) as DivideOperation; + const d = dinero({ amount: 10455n, currency: bigintUSD, scale: 3n }); - expect(snapshot).toMatchObject({ amount: 333n, scale: 2n }); + const snapshot = toSnapshot(transformScale(d, 2n, divideFn)); + + expect(snapshot).toMatchObject({ amount: 1045n, scale: 2n }); + expect(divideFn).toHaveBeenNthCalledWith( + 1, + 10455n, + 10n, + expect.objectContaining({ + add: expect.any(Function), + compare: expect.any(Function), + decrement: expect.any(Function), + increment: expect.any(Function), + integerDivide: expect.any(Function), + modulo: expect.any(Function), + multiply: expect.any(Function), + power: expect.any(Function), + subtract: expect.any(Function), + zero: expect.any(Function), + }) + ); + }); }); - it('converts from long initial scales correctly', () => { - const d = dinero({ amount: 3333333336n, currency: bigintUSD, scale: 9n }); - const snapshot = toSnapshot(transformScale(d, 2n)); + describe('non-decimal currencies', () => { + it('returns a new Dinero object with a new scale and a converted amount', () => { + const d = dinero({ amount: 5n, currency: bigintMGA }); + const snapshot = toSnapshot(transformScale(d, 2n)); + + expect(snapshot).toMatchObject({ amount: 25n, scale: 2n }); + }); + it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { + const d = dinero({ amount: 26n, currency: bigintMGA, scale: 2n }); + const snapshot = toSnapshot(transformScale(d, 1n)); + + expect(snapshot).toMatchObject({ amount: 5n, scale: 1n }); + }); + it('uses the provided `up` divide function', () => { + const d = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const snapshot = toSnapshot(transformScale(d, 1n, up)); + + expect(snapshot).toMatchObject({ amount: 6n, scale: 1n }); + }); + it('uses the provided `down` divide function', () => { + const d = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const snapshot = toSnapshot(transformScale(d, 1n, down)); + + expect(snapshot).toMatchObject({ amount: 5n, scale: 1n }); + }); + it('uses the provided `halfOdd` divide function', () => { + const d1 = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const d2 = dinero({ amount: 39n, currency: bigintABC, scale: 2n }); + + expect(toSnapshot(transformScale(d1, 1n, halfOdd))).toMatchObject({ + amount: 5n, + scale: 1n, + }); + expect(toSnapshot(transformScale(d2, 1n, halfOdd))).toMatchObject({ + amount: 7n, + scale: 1n, + }); + }); + it('uses the provided `halfEven` divide function', () => { + const d1 = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const d2 = dinero({ amount: 39n, currency: bigintABC, scale: 2n }); + + expect(toSnapshot(transformScale(d1, 1n, halfEven))).toMatchObject({ + amount: 6n, + scale: 1n, + }); + expect(toSnapshot(transformScale(d2, 1n, halfEven))).toMatchObject({ + amount: 6n, + scale: 1n, + }); + }); + it('uses the provided `halfDown` divide function', () => { + const d1 = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const d2 = dinero({ amount: 39n, currency: bigintABC, scale: 2n }); + + expect(toSnapshot(transformScale(d1, 1n, halfDown))).toMatchObject({ + amount: 5n, + scale: 1n, + }); + expect(toSnapshot(transformScale(d2, 1n, halfDown))).toMatchObject({ + amount: 6n, + scale: 1n, + }); + }); + it('uses the provided `halfUp` divide function', () => { + const d1 = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const d2 = dinero({ amount: 39n, currency: bigintABC, scale: 2n }); + + expect(toSnapshot(transformScale(d1, 1n, halfUp))).toMatchObject({ + amount: 6n, + scale: 1n, + }); + expect(toSnapshot(transformScale(d2, 1n, halfUp))).toMatchObject({ + amount: 7n, + scale: 1n, + }); + }); + it('uses the provided `halfTowardsZero` divide function', () => { + const d1 = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const d2 = dinero({ amount: 39n, currency: bigintABC, scale: 2n }); - expect(snapshot).toMatchObject({ amount: 333n, scale: 2n }); + expect( + toSnapshot(transformScale(d1, 1n, halfTowardsZero)) + ).toMatchObject({ + amount: 5n, + scale: 1n, + }); + expect( + toSnapshot(transformScale(d2, 1n, halfTowardsZero)) + ).toMatchObject({ + amount: 6n, + scale: 1n, + }); + }); + it('uses the provided `halfAwayFromZero` divide function', () => { + const d1 = dinero({ amount: 33n, currency: bigintABC, scale: 2n }); + const d2 = dinero({ amount: 39n, currency: bigintABC, scale: 2n }); + + expect( + toSnapshot(transformScale(d1, 1n, halfAwayFromZero)) + ).toMatchObject({ + amount: 6n, + scale: 1n, + }); + expect( + toSnapshot(transformScale(d2, 1n, halfAwayFromZero)) + ).toMatchObject({ + amount: 7n, + scale: 1n, + }); + }); }); }); describe('Big.js', () => { const dinero = createBigjsDinero; const bigjsUSD = castToBigjsCurrency(USD); + const bigjsMGA = castToBigjsCurrency(MGA); + const bigjsABC = castToBigjsCurrency(ABC); - it('returns a new Dinero object with a new scale and a converted amount', () => { - const d = dinero({ - amount: new Big(500), - currency: bigjsUSD, - scale: new Big(2), + describe('decimal currencies', () => { + it('returns a new Dinero object with a new scale and a converted amount', () => { + const d = dinero({ + amount: new Big(500), + currency: bigjsUSD, + scale: new Big(2), + }); + const snapshot = toSnapshot(transformScale(d, new Big(4))); + + expect(snapshot).toMatchObject({ + amount: new Big(50000), + scale: new Big(4), + }); }); - const snapshot = toSnapshot(transformScale(d, new Big(4))); + it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { + const d = dinero({ + amount: new Big(14270), + currency: bigjsUSD, + scale: new Big(2), + }); + const snapshot = toSnapshot(transformScale(d, new Big(0))); - expect(snapshot).toMatchObject({ - amount: new Big(50000), - scale: new Big(4), + expect(snapshot).toMatchObject({ + amount: new Big(142), + scale: new Big(0), + }); }); - }); - it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { - const d = dinero({ - amount: new Big(14270), - currency: bigjsUSD, - scale: new Big(2), + it('converts between scales correctly', () => { + const d = dinero({ + amount: new Big(333336), + currency: bigjsUSD, + scale: new Big(5), + }); + const snapshot = toSnapshot(transformScale(d, new Big(2))); + + expect(snapshot).toMatchObject({ + amount: new Big(333), + scale: new Big(2), + }); }); - const snapshot = toSnapshot(transformScale(d, new Big(0))); + it('converts from long initial scales correctly', () => { + const d = dinero({ + amount: new Big(3333333336), + currency: bigjsUSD, + scale: new Big(9), + }); + const snapshot = toSnapshot(transformScale(d, new Big(2))); - expect(snapshot).toMatchObject({ - amount: new Big(142), - scale: new Big(0), + expect(snapshot).toMatchObject({ + amount: new Big(333), + scale: new Big(2), + }); }); - }); - it('converts between scales correctly', () => { - const d = dinero({ - amount: new Big(333336), - currency: bigjsUSD, - scale: new Big(5), + it('uses the provided `up` divide function', () => { + const d = dinero({ + amount: new Big(10455), + currency: bigjsUSD, + scale: new Big(3), + }); + + const snapshot = toSnapshot(transformScale(d, new Big(2), up)); + + expect(snapshot).toMatchObject({ + amount: new Big(1046), + scale: new Big(2), + }); }); - const snapshot = toSnapshot(transformScale(d, new Big(2))); + it('uses the provided `down` divide function', () => { + const d = dinero({ + amount: new Big(10455), + currency: bigjsUSD, + scale: new Big(3), + }); - expect(snapshot).toMatchObject({ - amount: new Big(333), - scale: new Big(2), + const snapshot = toSnapshot(transformScale(d, new Big(2), down)); + + expect(snapshot).toMatchObject({ + amount: new Big(1045), + scale: new Big(2), + }); + }); + it('uses the provided `halfOdd` divide function', () => { + const d1 = dinero({ + amount: new Big(10415), + currency: bigjsUSD, + scale: new Big(3), + }); + const d2 = dinero({ + amount: new Big(10425), + currency: bigjsUSD, + scale: new Big(3), + }); + + expect( + toSnapshot(transformScale(d1, new Big(2), halfOdd)) + ).toMatchObject({ + amount: new Big(1041), + scale: new Big(2), + }); + expect( + toSnapshot(transformScale(d2, new Big(2), halfOdd)) + ).toMatchObject({ + amount: new Big(1043), + scale: new Big(2), + }); + }); + it('uses the provided `halfEven` divide function', () => { + const d1 = dinero({ + amount: new Big(10425), + currency: bigjsUSD, + scale: new Big(3), + }); + const d2 = dinero({ + amount: new Big(10435), + currency: bigjsUSD, + scale: new Big(3), + }); + + expect( + toSnapshot(transformScale(d1, new Big(2), halfEven)) + ).toMatchObject({ + amount: new Big(1042), + scale: new Big(2), + }); + expect( + toSnapshot(transformScale(d2, new Big(2), halfEven)) + ).toMatchObject({ + amount: new Big(1044), + scale: new Big(2), + }); + }); + it('uses the provided `halfDown` divide function', () => { + const d1 = dinero({ + amount: new Big(10455), + currency: bigjsUSD, + scale: new Big(3), + }); + const d2 = dinero({ + amount: new Big(10456), + currency: bigjsUSD, + scale: new Big(3), + }); + + expect( + toSnapshot(transformScale(d1, new Big(2), halfDown)) + ).toMatchObject({ + amount: new Big(1045), + scale: new Big(2), + }); + expect( + toSnapshot(transformScale(d2, new Big(2), halfDown)) + ).toMatchObject({ + amount: new Big(1046), + scale: new Big(2), + }); + }); + it('uses the provided `halfUp` divide function', () => { + const d1 = dinero({ + amount: new Big(10454), + currency: bigjsUSD, + scale: new Big(3), + }); + const d2 = dinero({ + amount: new Big(10455), + currency: bigjsUSD, + scale: new Big(3), + }); + + expect( + toSnapshot(transformScale(d1, new Big(2), halfUp)) + ).toMatchObject({ + amount: new Big(1045), + scale: new Big(2), + }); + expect( + toSnapshot(transformScale(d2, new Big(2), halfUp)) + ).toMatchObject({ + amount: new Big(1046), + scale: new Big(2), + }); + }); + it('uses the provided `halfTowardsZero` divide function', () => { + const d1 = dinero({ + amount: new Big(10415), + currency: bigjsUSD, + scale: new Big(3), + }); + + const snapshot = toSnapshot( + transformScale(d1, new Big(2), halfTowardsZero) + ); + + expect(snapshot).toMatchObject({ + amount: new Big(1041), + scale: new Big(2), + }); + }); + it('uses the provided `halfAwayFromZero` divide function', () => { + const d1 = dinero({ + amount: new Big(10415), + currency: bigjsUSD, + scale: new Big(3), + }); + + const snapshot = toSnapshot( + transformScale(d1, new Big(2), halfAwayFromZero) + ); + + expect(snapshot).toMatchObject({ + amount: new Big(1042), + scale: new Big(2), + }); + }); + it('uses a custom divide function', () => { + const divideFn = jest.fn(() => new Big(1045)) as DivideOperation; + const d = dinero({ + amount: new Big(10455), + currency: bigjsUSD, + scale: new Big(3), + }); + + const snapshot = toSnapshot(transformScale(d, new Big(2), divideFn)); + + expect(snapshot).toMatchObject({ + amount: new Big(1045), + scale: new Big(2), + }); + expect(divideFn).toHaveBeenNthCalledWith( + 1, + new Big(10455), + new Big(10), + expect.objectContaining({ + add: expect.any(Function), + compare: expect.any(Function), + decrement: expect.any(Function), + increment: expect.any(Function), + integerDivide: expect.any(Function), + modulo: expect.any(Function), + multiply: expect.any(Function), + power: expect.any(Function), + subtract: expect.any(Function), + zero: expect.any(Function), + }) + ); }); }); - it('converts from long initial scales correctly', () => { - const d = dinero({ - amount: new Big(3333333336), - currency: bigjsUSD, - scale: new Big(9), + describe('non-decimal currencies', () => { + it('returns a new Dinero object with a new scale and a converted amount', () => { + const d = dinero({ amount: new Big(5), currency: bigjsMGA }); + const snapshot = toSnapshot(transformScale(d, new Big(2))); + + expect(snapshot).toMatchObject({ + amount: new Big(25), + scale: new Big(2), + }); + }); + it('returns a new Dinero object with a new scale and a converted, rounded down amount', () => { + const d = dinero({ + amount: new Big(26), + currency: bigjsMGA, + scale: new Big(2), + }); + const snapshot = toSnapshot(transformScale(d, new Big(1))); + + expect(snapshot).toMatchObject({ + amount: new Big(5), + scale: new Big(1), + }); + }); + it('uses the provided `up` divide function', () => { + const d = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const snapshot = toSnapshot(transformScale(d, new Big(1), up)); + + expect(snapshot).toMatchObject({ + amount: new Big(6), + scale: new Big(1), + }); + }); + it('uses the provided `down` divide function', () => { + const d = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const snapshot = toSnapshot(transformScale(d, new Big(1), down)); + + expect(snapshot).toMatchObject({ + amount: new Big(5), + scale: new Big(1), + }); + }); + it('uses the provided `halfOdd` divide function', () => { + const d1 = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const d2 = dinero({ + amount: new Big(39), + currency: bigjsABC, + scale: new Big(2), + }); + + expect( + toSnapshot(transformScale(d1, new Big(1), halfOdd)) + ).toMatchObject({ + amount: new Big(5), + scale: new Big(1), + }); + expect( + toSnapshot(transformScale(d2, new Big(1), halfOdd)) + ).toMatchObject({ + amount: new Big(7), + scale: new Big(1), + }); + }); + it('uses the provided `halfEven` divide function', () => { + const d1 = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const d2 = dinero({ + amount: new Big(39), + currency: bigjsABC, + scale: new Big(2), + }); + + expect( + toSnapshot(transformScale(d1, new Big(1), halfEven)) + ).toMatchObject({ + amount: new Big(6), + scale: new Big(1), + }); + expect( + toSnapshot(transformScale(d2, new Big(1), halfEven)) + ).toMatchObject({ + amount: new Big(6), + scale: new Big(1), + }); + }); + it('uses the provided `halfDown` divide function', () => { + const d1 = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const d2 = dinero({ + amount: new Big(39), + currency: bigjsABC, + scale: new Big(2), + }); + + expect( + toSnapshot(transformScale(d1, new Big(1), halfDown)) + ).toMatchObject({ + amount: new Big(5), + scale: new Big(1), + }); + expect( + toSnapshot(transformScale(d2, new Big(1), halfDown)) + ).toMatchObject({ + amount: new Big(6), + scale: new Big(1), + }); + }); + it('uses the provided `halfUp` divide function', () => { + const d1 = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const d2 = dinero({ + amount: new Big(39), + currency: bigjsABC, + scale: new Big(2), + }); + + expect( + toSnapshot(transformScale(d1, new Big(1), halfUp)) + ).toMatchObject({ + amount: new Big(6), + scale: new Big(1), + }); + expect( + toSnapshot(transformScale(d2, new Big(1), halfUp)) + ).toMatchObject({ + amount: new Big(7), + scale: new Big(1), + }); + }); + it('uses the provided `halfTowardsZero` divide function', () => { + const d1 = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const d2 = dinero({ + amount: new Big(39), + currency: bigjsABC, + scale: new Big(2), + }); + + expect( + toSnapshot(transformScale(d1, new Big(1), halfTowardsZero)) + ).toMatchObject({ + amount: new Big(5), + scale: new Big(1), + }); + expect( + toSnapshot(transformScale(d2, new Big(1), halfTowardsZero)) + ).toMatchObject({ + amount: new Big(6), + scale: new Big(1), + }); }); - const snapshot = toSnapshot(transformScale(d, new Big(2))); + it('uses the provided `halfAwayFromZero` divide function', () => { + const d1 = dinero({ + amount: new Big(33), + currency: bigjsABC, + scale: new Big(2), + }); + const d2 = dinero({ + amount: new Big(39), + currency: bigjsABC, + scale: new Big(2), + }); - expect(snapshot).toMatchObject({ - amount: new Big(333), - scale: new Big(2), + expect( + toSnapshot(transformScale(d1, new Big(1), halfAwayFromZero)) + ).toMatchObject({ + amount: new Big(6), + scale: new Big(1), + }); + expect( + toSnapshot(transformScale(d2, new Big(1), halfAwayFromZero)) + ).toMatchObject({ + amount: new Big(7), + scale: new Big(1), + }); }); }); }); diff --git a/packages/dinero.js/src/api/index.ts b/packages/dinero.js/src/api/index.ts index debb6de4d..1f7fb0e39 100644 --- a/packages/dinero.js/src/api/index.ts +++ b/packages/dinero.js/src/api/index.ts @@ -18,8 +18,8 @@ export * from './minimum'; export * from './multiply'; export * from './normalizeScale'; export * from './subtract'; -export * from './toFormat'; +export * from './toDecimal'; export * from './toSnapshot'; -export * from './toUnit'; +export * from './toUnits'; export * from './transformScale'; export * from './trimScale'; diff --git a/packages/dinero.js/src/api/toDecimal.ts b/packages/dinero.js/src/api/toDecimal.ts new file mode 100644 index 000000000..53b9a0b43 --- /dev/null +++ b/packages/dinero.js/src/api/toDecimal.ts @@ -0,0 +1,28 @@ +import { toDecimal as coreToDecimal } from '@dinero.js/core'; +import type { ToDecimalParams, Dinero, Transformer } from '@dinero.js/core'; + +export function toDecimal(dineroObject: Dinero): string; + +export function toDecimal( + dineroObject: Dinero, + transformer: Transformer +): TOutput; + +/** + * Get the amount of a Dinero object in decimal form. + * + * @param dineroObject - The Dinero object to format. + * @param transformer - A transformer function. + * + * @returns The amount in decimal form. + * + * @public + */ +export function toDecimal( + ...[dineroObject, transformer]: ToDecimalParams +) { + const { calculator } = dineroObject; + const toDecimalFn = coreToDecimal(calculator); + + return toDecimalFn(dineroObject, transformer); +} diff --git a/packages/dinero.js/src/api/toFormat.ts b/packages/dinero.js/src/api/toFormat.ts deleted file mode 100644 index 154af62e8..000000000 --- a/packages/dinero.js/src/api/toFormat.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { toFormat as coreToFormat } from '@dinero.js/core'; -import type { ToFormatParams } from '@dinero.js/core'; - -/** - * Format a Dinero object with a custom transformer. - * - * @param dineroObject - The Dinero object to format. - * @param transformer - A transformer function. - * - * @returns The object as a formatted string. - * - * @public - */ -export function toFormat( - ...[dineroObject, transformer]: ToFormatParams -) { - const { calculator } = dineroObject; - const formatter = coreToFormat(calculator); - - return formatter(dineroObject, transformer); -} diff --git a/packages/dinero.js/src/api/toSnapshot.ts b/packages/dinero.js/src/api/toSnapshot.ts index ce07ff775..5394fa276 100644 --- a/packages/dinero.js/src/api/toSnapshot.ts +++ b/packages/dinero.js/src/api/toSnapshot.ts @@ -3,7 +3,8 @@ import { toSnapshot as coreToSnapshot } from '@dinero.js/core'; /** * Get a snapshot of a Dinero object. * - * @param dineroObject - The Dinero object to transform. + * @param dineroObject - The Dinero object to format. + * @param transformer - A transformer function. * * @returns A snapshot of the object. * diff --git a/packages/dinero.js/src/api/toUnit.ts b/packages/dinero.js/src/api/toUnit.ts deleted file mode 100644 index 266f350e4..000000000 --- a/packages/dinero.js/src/api/toUnit.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { toUnit as coreToUnit } from '@dinero.js/core'; -import type { ToUnitParams } from '@dinero.js/core'; - -/** - * Get the amount of a Dinero object in units. - * - * @param dineroObject - The Dinero object to transform. - * @param options.digits - The number of fraction digits to round to. - * @param options.round - The rounding function to use. - * - * @returns The amount in units. - * - * @public - */ -export function toUnit( - ...[dineroObject, options]: ToUnitParams -) { - const { calculator } = dineroObject; - const toUnitFn = coreToUnit(calculator); - - return toUnitFn(dineroObject, options); -} diff --git a/packages/dinero.js/src/api/toUnits.ts b/packages/dinero.js/src/api/toUnits.ts new file mode 100644 index 000000000..cb4d8a536 --- /dev/null +++ b/packages/dinero.js/src/api/toUnits.ts @@ -0,0 +1,30 @@ +import { toUnits as coreToUnits } from '@dinero.js/core'; +import type { ToUnitsParams, Dinero, Transformer } from '@dinero.js/core'; + +export function toUnits( + dineroObject: Dinero +): readonly TAmount[]; + +export function toUnits( + dineroObject: Dinero, + transformer: Transformer +): TOutput; + +/** + * Get the amount of a Dinero object in units. + * + * @param dineroObject - The Dinero object to format. + * @param transformer - A transformer function. + * + * @returns The amount in units. + * + * @public + */ +export function toUnits( + ...[dineroObject, transformer]: ToUnitsParams +) { + const { calculator } = dineroObject; + const toUnitsFn = coreToUnits(calculator); + + return toUnitsFn(dineroObject, transformer); +} diff --git a/packages/dinero.js/src/api/transformScale.ts b/packages/dinero.js/src/api/transformScale.ts index 05802a7c1..a2efb60e4 100644 --- a/packages/dinero.js/src/api/transformScale.ts +++ b/packages/dinero.js/src/api/transformScale.ts @@ -6,16 +6,17 @@ import type { TransformScaleParams } from '@dinero.js/core'; * * @param dineroObject - The Dinero object to transform. * @param newScale - The new scale. + * @param divide - A custom divide function. * * @returns A new Dinero object. * * @public */ export function transformScale( - ...[dineroObject, newScale]: TransformScaleParams + ...[dineroObject, newScale, divide]: TransformScaleParams ) { const { calculator } = dineroObject; const transformScaleFn = coreTransformScale(calculator); - return transformScaleFn(dineroObject, newScale); + return transformScaleFn(dineroObject, newScale, divide); } diff --git a/packages/dinero.js/src/index.ts b/packages/dinero.js/src/index.ts index 8d8431a1b..b5ecc0745 100644 --- a/packages/dinero.js/src/index.ts +++ b/packages/dinero.js/src/index.ts @@ -7,9 +7,9 @@ export type { DineroFactory, DineroOptions, DineroSnapshot, + DivideOperation, Formatter, Rates, - RoundingOptions, Transformer, } from '@dinero.js/core'; export { diff --git a/test/utils/castToBigintCurrency.ts b/test/utils/castToBigintCurrency.ts index 3192220cb..9de65edd7 100644 --- a/test/utils/castToBigintCurrency.ts +++ b/test/utils/castToBigintCurrency.ts @@ -5,7 +5,9 @@ export function castToBigintCurrency( ): Currency { return { ...currency, - base: BigInt(currency.base), + base: Array.isArray(currency.base) + ? currency.base.map(BigInt) + : BigInt(currency.base as number), exponent: BigInt(currency.exponent), }; } diff --git a/test/utils/castToBigjsCurrency.ts b/test/utils/castToBigjsCurrency.ts index 0fd9c8c95..a5b34d7cb 100644 --- a/test/utils/castToBigjsCurrency.ts +++ b/test/utils/castToBigjsCurrency.ts @@ -5,7 +5,9 @@ import type { Currency } from 'dinero.js'; export function castToBigjsCurrency(currency: Currency): Currency { return { ...currency, - base: new Big(currency.base), + base: Array.isArray(currency.base) + ? currency.base.map((b) => new Big(b)) + : new Big(currency.base as number), exponent: new Big(currency.exponent), }; } diff --git a/test/utils/createBigjsDinero.ts b/test/utils/createBigjsDinero.ts index 732794ff2..08ce651f9 100644 --- a/test/utils/createBigjsDinero.ts +++ b/test/utils/createBigjsDinero.ts @@ -14,7 +14,6 @@ const dinero = createDinero({ multiply: (a, b) => a.times(b), power: (a, b) => a.pow(Number(b)), subtract: (a, b) => a.minus(b), - toNumber: (v) => v.toNumber(), zero: () => new Big(0), }, }); diff --git a/website/data/docs/api/conversions/transform-scale.mdx b/website/data/docs/api/conversions/transform-scale.mdx index 13f137146..49fa084a5 100644 --- a/website/data/docs/api/conversions/transform-scale.mdx +++ b/website/data/docs/api/conversions/transform-scale.mdx @@ -6,13 +6,11 @@ returns: Dinero Transform a Dinero object to a new scale. -Transforming to a higher scale means that the internal `amount` value increases by orders of magnitude. If you're using the default Dinero.js implementation (with the `number` calculator), be careful not to exceed the minimum and maximum safe integers. +When transforming to a higher scale, the internal `amount` value increases by orders of magnitude. If you're using the default Dinero.js implementation (with the `number` calculator), be careful not to exceed the minimum and maximum safe integers. - +When transforming to a smaller scale, the `amount` loses precision. By default, the function rounds down the amount. You can specify how to round by [passing a custom divide function](#pass-a-custom-divide-function). -When transforming to smaller scales, `transformScale` truncates the amount. - - +For convenience, Dinero.js provides the following divide functions: `up`, `down`, `halfUp`, `halfDown`, `halfOdd`, `halfEven` ([bankers rounding](https://wiki.c2.com/?BankersRounding)), `halfTowardsZero`, and `halfAwayFromZero`. ## Parameters @@ -30,6 +28,12 @@ The new scale. + + +A custom divide function. + + + ## Code examples @@ -44,3 +48,14 @@ const d = dinero({ amount: 500, currency: USD, scale: 2 }); transformScale(d, 4); // a Dinero object with amount 50000 and scale 4 ``` + +### Pass a custom divide function + +```js +import { dinero, transformScale, up } from 'dinero.js'; +import { USD } from '@dinero.js/currencies'; + +const d = dinero({ amount: 10455, currency: USD, scale: 3 }); + +transformScale(d, 2, up); // a Dinero object with amount 1046 and scale 2 +``` diff --git a/website/data/docs/api/formatting/to-decimal.mdx b/website/data/docs/api/formatting/to-decimal.mdx new file mode 100644 index 000000000..b7a3d9f9e --- /dev/null +++ b/website/data/docs/api/formatting/to-decimal.mdx @@ -0,0 +1,61 @@ +--- +title: toDecimal +description: Get the amount of a Dinero object in decimal format. +returns: TOutput = string +--- + +Get the amount of a Dinero object in a stringified decimal representation. + +The number of decimal places depends on the [`scale`](/docs/core-concepts/scale) of your object—or, when unspecified, the [`exponent`](/docs/core-concepts/currency#currency-exponent) of its currency. + + + +You can only use this function with Dinero objects that are single-based and use a decimal currency. + + + +## Parameters + + + + + +The Dinero object to format. + + + + + +An optional transformer function. + + + + + +## Code examples + +### Format an object in decimal format + +```js +import { dinero, toDecimal } from 'dinero.js'; +import { USD } from '@dinero.js/currencies'; + +const d1 = dinero({ amount: 1050, currency: USD }); +const d2 = dinero({ amount: 10545, currency: USD, scale: 3 }); + +toDecimal(d1); // "10.50" +toDecimal(d2); // "10.545" +``` + +### Use a custom transformer + +If you need to further transform the value before returning it, you can pass a custom function. + +```js +import { dinero, toDecimal } from 'dinero.js'; +import { USD } from '@dinero.js/currencies'; + +const d = dinero({ amount: 1050, currency: USD }); + +toDecimal(d, ({ value, currency }) => `${currency.code} ${value}`); // "USD 10.50" +``` diff --git a/website/data/docs/api/formatting/to-format.mdx b/website/data/docs/api/formatting/to-format.mdx deleted file mode 100644 index c31af8ff5..000000000 --- a/website/data/docs/api/formatting/to-format.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: toFormat -description: Format a Dinero object with a custom transformer. -returns: string ---- - -Format a Dinero object with a custom transformer. - -The `transformer` parameter exposes the amount in rounded units, the currency, and the initial Dinero object. The latter can be useful when [formatting non-decimal currencies](/docs/advanced/formatting-non-decimal-currencies). - -You can also specify rounding `options` to determine how to round the amount. - -## Parameters - - - - - -The Dinero object to format. - - - - - -A transformer function. - - - - - -## Code examples - -### Format an object with the passed transformer - -```js -import { dinero, toFormat } from 'dinero.js'; -import { USD } from '@dinero.js/currencies'; - -const d = dinero({ amount: 500, currency: USD }); - -toFormat(d, ({ amount, currency }) => `${currency.code} ${amount}`); // "USD 5" -``` - -### Build a reusable formatter - -If you're formatting many objects, you might want to reuse the same transformer without having to pass it every time. To do so, you can wrap `toFormat` in a formatter function that accepts a Dinero object and returns it formatted using a predefined formatter. - -```js -import { dinero, toFormat } from 'dinero.js'; -import { USD } from '@dinero.js/currencies'; - -function format(dineroObject) { - return toFormat( - dineroObject, - ({ amount, currency }) => `${currency.code} ${amount}` - ); -} - -const d = dinero({ amount: 5000, currency: USD }); - -format(d); // "USD 50" -``` - -You can even build your own reusable higher-order function to build formatters. This can be useful if you need to create multiple formatters, for example to cater to multiple locales. - -```js -// ... - -function createFormatter(transformer) { - return function format(dineroObject) { - return toFormat(dineroObject, transformer); - }; -} - -const frenchFormat = createFormatter(({ amount, currency }) => { - return `${amount.toFixed(currency.exponent).replace('.', ',')} ${ - currency.code - }`; -}); - -const americanFormat = createFormatter(({ amount, currency }) => { - return `${currency.code} ${amount.toFixed(currency.exponent)}`; -}); -``` - -In such a situation, you can also [create a single formatter based on the Internationalization API](/docs/guides/formatting-in-a-multilingual-site). diff --git a/website/data/docs/api/formatting/to-unit.mdx b/website/data/docs/api/formatting/to-unit.mdx deleted file mode 100644 index a0adf9826..000000000 --- a/website/data/docs/api/formatting/to-unit.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: toUnit -description: Get the amount of a Dinero object in units. -returns: number ---- - -Get the amount of a Dinero object in major currency unit. - -By default, the number of represented fraction digits depends on the amount and scale of the Dinero object. You can specify how many fraction digits you want to represent and pass a rounding function. - -For convenience, Dinero.js provides the following rounding functions: `up`, `down`, `halfUp`, `halfDown`, `halfOdd`, `halfEven` ([bankers rounding](https://wiki.c2.com/?BankersRounding)), `halfTowardsZero`, and `halfAwayFromZero`. - -## Parameters - - - - - -The Dinero object to format. - - - - - -A mapping of options. - - - - - -The number of fraction digits to round to. - - - - - -The rounding function to use. - - - - - -## Code examples - -### Format an object in major currency unit - -```js -import { dinero, toUnit } from 'dinero.js'; -import { USD } from '@dinero.js/currencies'; - -const d = dinero({ amount: 1050, currency: USD }); - -toUnit(d); // 10.5 -``` - -### Format an object with a custom scale - -```js -import { dinero, toUnit } from 'dinero.js'; -import { USD } from '@dinero.js/currencies'; - -const d = dinero({ amount: 10545, currency: USD, scale: 3 }); - -toUnit(d); // 10.545 -``` - -### Format an object rounded to one fraction digit - -```js -import { dinero, toUnit, down } from 'dinero.js'; -import { USD } from '@dinero.js/currencies'; - -const d = dinero({ amount: 1055, currency: USD }); - -toUnit(d, { digits: 1, round: down }); // 10.5 -``` diff --git a/website/data/docs/api/formatting/to-units.mdx b/website/data/docs/api/formatting/to-units.mdx new file mode 100644 index 000000000..f3d2c60ac --- /dev/null +++ b/website/data/docs/api/formatting/to-units.mdx @@ -0,0 +1,86 @@ +--- +title: toUnits +description: Get the amount of a Dinero object in units. +returns: TOutput = TAmount[] +--- + +Get the amount of a Dinero object in units. + +This function returns the total amount divided into each unit and sub-unit, as an array. For example, an object representing $10.45 expressed as `1045` (with currency `USD` and no custom `scale`) would return `[10, 45]` for 10 dollars and 45 cents. + +When specifying multiple bases, the function returns as many units as necessary. + +## Parameters + + + + + +The Dinero object to format. + + + + + +An optional transformer function. + + + + + +## Code examples + +### Format an object in units + +```js +import { dinero, toUnits } from 'dinero.js'; +import { USD } from '@dinero.js/currencies'; + +const d1 = dinero({ amount: 1050, currency: USD }); +const d2 = dinero({ amount: 10545, currency: USD, scale: 3 }); + +toUnits(d1); // [10, 50] +toUnits(d2); // [10, 545] +``` + +### Format a non-decimal object + +```js +import { dinero, toUnits } from 'dinero.js'; + +const GRD = { code: 'GRD', base: 6, exponent: 1 }; +const d = dinero({ amount: 9, currency: GRD }); + +toUnits(d); // [1, 3] +``` + +### Format an object with multiple subdivisions + +```js +import { dinero, toUnits } from 'dinero.js'; + +const GBP = { code: 'GBP', base: [20, 12], exponent: 1 }; +const d = dinero({ amount: 267, currency: GBP }); + +toUnits(d); // [1, 2, 3] +``` + +### Use a custom transformer + +If you need to further transform the value before returning it, you can pass a custom function. + +```js +import { dinero, toUnits } from 'dinero.js'; + +const GBP = { code: 'GBP', base: [20, 12], exponent: 1 }; +const d = dinero({ amount: 267, currency: GBP }); + +const labels = ['pounds', 'shillings', 'pence']; + +toUnits(d, ({ value }) => + value + .filter((amount) => amount > 0) + .map((amount, index) => `${amount} ${labels[index]}`) + .join(', ') +); +``` diff --git a/website/data/docs/core-concepts/amount.mdx b/website/data/docs/core-concepts/amount.mdx index 8d79ba8e1..6c8519a68 100644 --- a/website/data/docs/core-concepts/amount.mdx +++ b/website/data/docs/core-concepts/amount.mdx @@ -34,7 +34,7 @@ When working with currencies with no minor units, you need to set the [currency ## Non-decimal currencies -When using a non-decimal currency (with or without multiple subdivisions), you should express the amount in the smallest subdivision. +When using a non-decimal currency, you should express the amount in the smallest subdivision. If the currency has multiple subdivisions (such as the pre-decimal British pound sterling), you can specify them with an array. ```js import { dinero } from 'dinero.js'; @@ -51,9 +51,11 @@ const GRD = { const d1 = dinero({ amount: 6, currency: GRD }); // Pre-decimal Great Britain pound sterling +// 20 shillings in a pound +// 12 pence in a shilling const GBP = { code: 'GBP', - base: 240, + base: [20, 12], exponent: 1, }; diff --git a/website/data/docs/core-concepts/currency.mdx b/website/data/docs/core-concepts/currency.mdx index 709e5bd49..e3af1f9ea 100644 --- a/website/data/docs/core-concepts/currency.mdx +++ b/website/data/docs/core-concepts/currency.mdx @@ -50,20 +50,20 @@ const MRU = { Some currencies have multiple subdivisions. For example, before [decimalization](https://en.wikipedia.org/wiki/Decimalisation), the British pound sterling was divided into 20 shillings, and each shilling into 12 pence. You also have examples in fiction, like Harry Potter, where one Galleon is divided into 17 Sickles, and each Sickle into 29 Knuts. -To represent these currencies, you can take how many of the smallest subdivision there are in the major one. There are 240 pence in a pound sterling, and in Harry Potter, 493 Knuts in a Galleon. +To represent these currencies, you can specify each subdivision with an array. ```js // Pre-decimal Great Britain pound sterling const GBP = { code: 'GBP', - base: 240, + base: [20, 12], exponent: 1, }; // Great Britain wizarding currency (Harry Potter universe) const GBW = { code: 'GBW', - base: 493, + base: [17, 29], exponent: 1, }; ``` diff --git a/website/data/docs/core-concepts/formatting.mdx b/website/data/docs/core-concepts/formatting.mdx index 82a6027bc..2cdeaab95 100644 --- a/website/data/docs/core-concepts/formatting.mdx +++ b/website/data/docs/core-concepts/formatting.mdx @@ -3,49 +3,53 @@ title: Formatting description: Formatting Dinero objects into rounded numbers or string representation. --- -When working with money on the front end, comes a time when you need to display amounts on the user interface. The Dinero.js API provides functions to format objects. +When working with money on the front end, comes a time when you need to display amounts on the user interface. **The Dinero.js API provides functions to format Dinero objects.** ```js -import { dinero, toUnit, down } from 'dinero.js'; +import { dinero, toUnits, down } from 'dinero.js'; import { USD } from '@dinero.js/currencies'; const d = dinero({ amount: 1055, currency: USD }); -toUnit(d, { digits: 1, round: down }); // 10.5 +toUnits(d); // [10, 55] ``` ## Displaying an object -Dinero objects are ideal to safely manipulate money, but at some point, you need to display them. The `toFormat` function lets you display objects the way you want. It exposes a pre-formatted amount for convenience. +The [`toDecimal`](/docs/api/formatting/to-decimal) function exposes a pre-formatted amount in decimal format and the object's `currency`. It lets you display objects the way you want using a transformer function. ```js -import { dinero, toFormat } from 'dinero.js'; -import { USD } from '@dinero.js/currencies'; +import { dinero, toDecimal, toUnits } from 'dinero.js'; +import { USD, MGA } from '@dinero.js/currencies'; -const transformer = ({ amount, currency }) => `${currency.code} ${amount}`; +const d1 = dinero({ amount: 5000, currency: USD }); +const d2 = dinero({ amount: 13, currency: MGA }); -const d = dinero({ amount: 5000, currency: USD }); +toDecimal(d1, ({ value, currency }) => `${currency.code} ${value}`); // "USD 50.00" +toUnits(d1, ({ value }) => `${value[0]} dollars, ${value[1]} cents`); // "50 dollars, 0 cents" -toFormat(d, transformer); // "USD 50" +toUnits(d2, ({ value }) => `${value[0]} ariary, ${value[1]} iraimbilanja`); // "2 ariary, 3 iraimbilanja" ``` -Dinero.js uses the scale of the object to determine how many decimal places to represent. You can adjust it in the `transformer`. +Dinero.js uses the object's scale to determine how many decimal places to represent. You can adjust it in the `transformer`. ```js -import { dinero, toFormat, up } from 'dinero.js'; +import { dinero, toDecimal, up } from 'dinero.js'; import { USD } from '@dinero.js/currencies'; -const transformer = ({ amount, currency }) => `${currency.code} ${amount.toFixed(1)}`; +const transformer = ({ value, currency }) => { + return `${currency.code} ${Number(value).toFixed(1)}`; +}; const d = dinero({ amount: 4545, currency: USD }); -toFormat(d, transformer, options); // "USD 45.5" +toDecimal(d, transformer); // "USD 45.5" ``` If you're formatting many objects, you might want to reuse the same transformer without having to pass it every time. To do so, you can write your own higher-order function to build formatters. ```js -import { dinero, toFormat } from 'dinero.js'; +import { dinero, toDecimal } from 'dinero.js'; import { USD } from '@dinero.js/currencies'; // This function lets you pass a transformer and rounding options. @@ -53,19 +57,19 @@ import { USD } from '@dinero.js/currencies'; // the closured transformer. function createFormatter(transformer) { return function formatter(dineroObject) { - return toFormat(dineroObject, transformer); + return toDecimal(dineroObject, transformer); }; } // This function is reusable to format any Dinero object // with the same transformer. const format = createFormatter( - ({ amount, currency }) => `${currency.code} ${amount}` + ({ value, currency }) => `${currency.code} ${value}` ); const d = dinero({ amount: 5000, currency: USD }); -format(d); // "USD 50" +format(d); // "USD 50.00" ``` + ``` Note that the code samples used in this documentation can use JavaScript syntax not natively supported by older browsers like Internet Explorer 11. If your site supports older browsers, make sure to use a tool like [Babel](https://babeljs.io/) to transform your code into code that works in the browsers you target. diff --git a/website/data/docs/getting-started/quick-start.mdx b/website/data/docs/getting-started/quick-start.mdx index e4bcbb77d..dee03dedb 100644 --- a/website/data/docs/getting-started/quick-start.mdx +++ b/website/data/docs/getting-started/quick-start.mdx @@ -127,17 +127,18 @@ const d4 = dinero({ amount: 1150, currency: USD }); hasSubUnits(d4); // returns true ``` -You can display Dinero objects into any format, exactly the way you want. Dinero.js lets you build a formatter that exposes a pre-formatted amount, in major currency units. +Dinero.js provides [formatting functions](/docs/core-concepts/formatting) that expose a pre-formatted amount. You can use them as-is, or pass a custom transformer function to further customize the output. ```js -import { dinero, toFormat } from 'dinero.js'; -import { USD } from '@dinero.js/currencies'; - -const transformer = ({ amount, currency }) => `${currency.code} ${amount}`; +import { dinero, toDecimal, toUnits } from 'dinero.js'; +import { USD, MGA } from '@dinero.js/currencies'; -const price = dinero({ amount: 1150, currency: USD }); +const d1 = dinero({ amount: 5000, currency: USD }); +const d2 = dinero({ amount: 13, currency: MGA }); -toFormat(price, transformer); // "USD 11.5" +toDecimal(d1); // "50.00" +toDecimal(d1, ({ value, currency }) => `${currency.code} ${value}`); // "USD 50.00" +toUnits(d2, ({ value }) => `${value[0]} ariary, ${value[1]} iraimbilanja`); // "2 ariary, 3 iraimbilanja" ``` Dinero objects pick up their scale from their currency exponent. If you want to represent amounts differently, you can specify a scale manually. diff --git a/website/data/docs/getting-started/upgrade-guide.mdx b/website/data/docs/getting-started/upgrade-guide.mdx index 952bcb442..a3f6d9fc7 100644 --- a/website/data/docs/getting-started/upgrade-guide.mdx +++ b/website/data/docs/getting-started/upgrade-guide.mdx @@ -132,12 +132,12 @@ Former methods and new functions don't all have the same signature. Refer to the -| Dinero v1.x | Dinero v2 | -|-----------------------------|----------------------------------------------------------------------| -| `d1.toFormat(format)` | [`toFormat(d1, ...args)`](/docs/api/formatting/to-format) | -| `d1.toObject()` | [`toSnapshot(d1)`](/docs/api/formatting/to-snapshot) | -| `d1.toUnit(...args)` | [`toUnit(d1, ...args)`](/docs/api/formatting/to-unit) | -| `d1.toRoundedUnit(...args)` | [`toUnit(d1, ...args)`](/docs/api/formatting/to-unit) | +| Dinero v1.x | Dinero v2 | +|-----------------------------|----------------------------------------------------------------------------------------| +| `d1.toFormat(format)` | Dropped, [see replacement](#replace-tounit-and-toroundedunit-with-tounits-or-todecimal) | +| `d1.toObject()` | [`toSnapshot(d1)`](/docs/api/formatting/to-snapshot) | +| `d1.toUnit(...args)` | Dropped, [see replacement](#replace-tounit-and-toroundedunit-with-tounits-or-todecimal) | +| `d1.toRoundedUnit(...args)` | Dropped, [see replacement](#replace-tounit-and-toroundedunit-with-tounits-or-todecimal) | @@ -277,6 +277,28 @@ Dinero.js v2 no longer has a built-in `percentage` function. You can build your ]} /> +## Replace toUnit and toRoundedUnit with toUnits or toDecimal + +Dinero.js v2 no longer has a built-in `toUnit` and `toRoundedUnit` functions. Use [`toUnits`](/docs/api/formatting/to-units) or [`toDecimal`](/docs/api/formatting/to-decimal) instead. + + + ## Dropped support for locale In v1.x, object formatting relied upon the [Internationalization API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). You could pass a locale to each Dinero object to control how to format it. In v2, formatting is dependency-free and provides you full control. You no longer need to rely on a locale, therefore this concept is gone. @@ -284,11 +306,11 @@ In v1.x, object formatting relied upon the [Internationalization API](https://de To replicate the same formatting you had in v1.x, you can create a formatter that wraps around the Internationalization API. ```js -import { toFormat } from 'dinero.js'; +import { toDecimal } from 'dinero.js'; function createIntlFormatter(locale, options = {}) { - function transformer({ amount, currency }) { - return amount.toLocaleString(locale, { + function transformer({ value, currency }) { + return Number(value).toLocaleString(locale, { ...options, style: 'currency', currency: currency.code, @@ -296,7 +318,7 @@ function createIntlFormatter(locale, options = {}) { }); return function formatter(dineroObject) { - return toFormat(dineroObject, transformer); + return toDecimal(dineroObject, transformer); }; } @@ -320,8 +342,8 @@ intlFormat(d); // "$5.00" label: 'API', links: [ { - title: 'To format', - url: '/docs/api/formatting/to-format', + title: 'To decimal', + url: '/docs/api/formatting/to-decimal', }, ], }, diff --git a/website/data/docs/guides/formatting-in-a-multilingual-site.mdx b/website/data/docs/guides/formatting-in-a-multilingual-site.mdx index 506bef146..244bea556 100644 --- a/website/data/docs/guides/formatting-in-a-multilingual-site.mdx +++ b/website/data/docs/guides/formatting-in-a-multilingual-site.mdx @@ -5,26 +5,26 @@ description: Displaying currencies in a site or application that supports severa Different languages and locations can have radically different formatting styles when it comes to money. For example, ten U.S. dollars in American English should be written down "$10.00". However, in Canadian French, the same amount would be "10,00 $ US". -Dinero.js provides a [`toFormat`](/docs/api/formatting/to-format) function that gives you full control over how to format a Dinero object. Yet, if you're working on a multilingual site or app, such as a booking site for a hotel, **you might want to rely on established formatting conventions.** +Dinero.js provides formatting functions that give you full control over how to format a Dinero object. ## Building a custom Internationalization formatter -ECMAScript provides an [Internationalization API](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl) (`Intl`) that lets you natively format monetary values into a given language by passing a locale. You can create your own `Intl` formatter by wrapping `toFormat`. +ECMAScript provides an [Internationalization API](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl) (`Intl`) that lets you natively format monetary values into a given language by passing a locale. You can create your own `Intl` formatter by wrapping [`toDecimal`](/docs/api/formatting/to-decimal). ```js -import { dinero, toFormat } from 'dinero.js'; +import { dinero, toDecimal } from 'dinero.js'; import { USD } from '@dinero.js/currencies'; function intlFormat(dineroObject, locale, options = {}) { - function transformer({ amount, currency }) { - return amount.toLocaleString(locale, { + function transformer({ value, currency }) { + return Number(value).toLocaleString(locale, { ...options, style: 'currency', currency: currency.code, }); }; - return toFormat(dineroObject, transformer); + return toDecimal(dineroObject, transformer); }; const d = dinero({ amount: 1000, currency: USD }); diff --git a/website/data/docs/guides/formatting-non-decimal-currencies.mdx b/website/data/docs/guides/formatting-non-decimal-currencies.mdx index 7aafb0d5d..1852d32e2 100644 --- a/website/data/docs/guides/formatting-non-decimal-currencies.mdx +++ b/website/data/docs/guides/formatting-non-decimal-currencies.mdx @@ -7,95 +7,62 @@ The great majority of circulating currencies are decimal. If you're working with However, **you might also work with non-decimal currencies**. Typical use cases are ancient currencies such as the ancient Greek drachma, or fictional currencies like the wizarding currencies in the Harry Potter universe. If you're building a numismatic site or a game with its own currency, you might have advanced formatting needs. -Out of the box, you can format any Dinero object—decimal or not—into an accurate decimal representation. For example, there are 6 obols in a drachma, so 9 obols do represent one and a half drachmas. +## Handling currencies with a single subdivision -```js -import { dinero, toFormat } from 'dinero.js'; - -// Ancient Greek drachma -const GRD = { - code: 'GRD', - base: 6, - exponent: 1, -}; - -const d = dinero({ amount: 9, currency: GRD }); - -toFormat(d, ({ amount, currency }) => `${currency.code} ${amount}`); // "GRD 1.5" -``` - -This should cover your needs if you want accuracy and readability because people are used to reading decimal numbers. However, such a representation might not be idiomatic. - -## Building complex formatters - -In addition to the formatted amount and the currency, the `toFormat` function also exposes the original object. You can leverage it to build a more complex formatter. +Out of the box, you can format any non-decimal Dinero object using [`toUnits`](/docs/api/formatting/to-units). ```js -import { dinero, toFormat, toSnapshot } from 'dinero.js'; +import { dinero, toUnits } from 'dinero.js'; import pluralize from 'pluralize'; -// ... - -function transformer({ dineroObject }) { - const { amount, currency } = toSnapshot(dineroObject); - - const drachmas = Math.trunc(amount / currency.base); - const obols = amount % currency.base; - - const amounts = [ - { amount: drachmas, label: 'drachma' }, - { amount: obols, label: 'obol' }, - ]; +const labels = ['drachma', 'obol']; - return amounts - .filter(({ amount }) => amount > 0) - .map(({ amount, label }) => `${amount} ${pluralize(label, amount)}`) +function transformer({ value, currency }) { + return value + .filter((amount) => amount > 0) + .map((amount, index) => `${amount} ${pluralize(labels[index], amount)}`) .join(', '); } -toFormat(d, transformer); // "1 drachma, 3 obols" +const d = dinero({ + amount: 9, + currency: { + code: 'GRD', + base: 6, + exponent: 1, + }, +}); + +toUnits(d, transformer); // "1 drachma, 3 obols" ``` ## Handling currencies with multiple subdivisions While most circulating currencies have a single minor currency unit, **many ancient currencies have multiple subdivisions.** That's the case for most pre-decimalization European currencies such as the livre tournois in the French Old Regime or the pound sterling in Great Britain before 1971. That's also the case of some fictional currencies. -When working with such currencies, you can take how many of the smallest subdivision there are in the major one (e.g., 240 pence in a pound). **The intermediate subdivisions only matter when you're formatting a Dinero object.** +When working with such currencies, **you can specify each subdivision with an array.** -For example, let's say you're building a Candy Crush clone where users can buy bonuses with an in-game currency: donuts, cookies, and lollipops. In your game, a donut equals 30 cookies, and a cookie equals 16 lollipops, giving 480 lollipops to the donut. If a bonus costs 720 lollipops, you probably want to format it either into "720 lollipops" or "1 donut and 15 cookies" instead of "1.5 donuts". This way, users can understand what they can afford based on what they have. +For example, let's say you're building a Candy Crush clone where users can buy bonuses with an in-game currency: donuts, cookies, and lollipops. In your game, a donut equals 30 cookies, and a cookie equals 16 lollipops. If a bonus costs 720 lollipops, you might want to format it as "1 donut and 15 cookies". ```js -import { dinero, toFormat } from 'dinero.js'; +import { dinero, toUnits } from 'dinero.js'; const POP = { code: 'POP', - base: 480, + base: [30, 16], exponent: 1, }; -function transformer({ dineroObject }) { - const { amount, currency } = toSnapshot(dineroObject); - - const remainder = amount % currency.base; - const cookieBase = 16; - - const donuts = Math.trunc(amount / currency.base); - const cookies = Math.trunc(remainder / cookieBase); - const lollipops = remainder % cookieBase; - - const amounts = [ - { amount: donuts, label: '🍩' }, - { amount: cookies, label: '🍪' }, - { amount: lollipops, label: '🍭' }, - ]; +const labels = ['🍩', '🍪', '🍭']; - return amounts - .filter(({ amount }) => amount > 0) - .map(({ amount, label }) => `${amount} ${label}`) +function transformer({ value }) { + return value + .filter((amount) => amount > 0) + .map((amount, index) => `${amount} ${labels[index]}`) .join(' and '); } const d = dinero({ amount: 720, currency: POP }); -toFormat(d, transformer); // "1 🍩 and 15 🍪" +toUnits(d, transformer); // "1 🍩 and 15 🍪" ``` diff --git a/website/data/docs/guides/integrating-with-payment-services.mdx b/website/data/docs/guides/integrating-with-payment-services.mdx index 8f4d5d3ae..485f8e15c 100644 --- a/website/data/docs/guides/integrating-with-payment-services.mdx +++ b/website/data/docs/guides/integrating-with-payment-services.mdx @@ -42,18 +42,18 @@ const response = await client.charges.create({ ## Integrating with Paypal -The [Paypal](https://www.paypal.com/) payment platform provides APIs to process payments and manage orders. Unlike most platforms, it expects a money representation with an amount in major currency units. You can use [`toUnit`](/docs/api/formatting/to-unit) to retrieve this value. +The [Paypal](https://www.paypal.com/) payment platform provides APIs to process payments and manage orders. Unlike most platforms, it expects a string representation with an amount in major currency units. You can use [`toDecimal`](/docs/api/formatting/to-decimal) to format the object and pass this value to Paypal. ```js const paypal = require('@paypal/checkout-server-sdk'); -const { dinero, toSnapshot } = require('dinero.js'); +const { dinero, toSnapshot, toDecimal } = require('dinero.js'); const { USD } = require('@dinero.js/currencies'); function toPaypalMoney(dineroObject) { const { currency, scale } = toSnapshot(dineroObject); return { - value: toUnit(dineroObject).toFixed(scale), + value: toDecimal(dineroObject), currency_code: currency.code, }; } diff --git a/website/data/docs/guides/using-different-amount-types.mdx b/website/data/docs/guides/using-different-amount-types.mdx index ba25800e8..4da742928 100644 --- a/website/data/docs/guides/using-different-amount-types.mdx +++ b/website/data/docs/guides/using-different-amount-types.mdx @@ -90,7 +90,6 @@ const calculator: Calculator = { multiply: (a, b) => a.times(b), power: (a, b) => a.pow(Number(b)), subtract: (a, b) => a.minus(b), - toNumber: (v) => v.toNumber(), zero: () => new Big(0), }; ``` diff --git a/website/data/docs/index.mdx b/website/data/docs/index.mdx index 57a3bacd2..3e54ecb2b 100644 --- a/website/data/docs/index.mdx +++ b/website/data/docs/index.mdx @@ -228,13 +228,13 @@ addMany([d1, d2, d3]); You can also combine it with utility libraries like [Ramda](https://ramdajs.com/) if that's your jam. ```js -import { toFormat } from 'dinero.js'; +import { toDecimal } from 'dinero.js'; import { curry, flip } from 'ramda'; -const curriedToFormat = curry(flip(toFormat)); +const curriedToFormat = curry(flip(toDecimal)); -const intlFormat = curriedToFormat(({ amount, currency }) => { - return amount.toLocaleString('en-US', { +const intlFormat = curriedToFormat(({ value, currency }) => { + return Number(value).toLocaleString('en-US', { style: 'currency', currency: currency.code, }); diff --git a/website/data/sidebar.ts b/website/data/sidebar.ts index 88e23ae85..0ce34b82c 100644 --- a/website/data/sidebar.ts +++ b/website/data/sidebar.ts @@ -52,9 +52,9 @@ tree.add(Resource.create({ label: 'haveSameAmount', path: '/docs/api/comparisons tree.add(Resource.create({ label: 'haveSameCurrency', path: '/docs/api/comparisons/have-same-currency' })); tree.add(Resource.create({ label: 'hasSubUnits', path: '/docs/api/comparisons/has-sub-units' })); tree.add(Resource.create({ label: 'Formatting', path: '/docs/api/formatting' })); -tree.add(Resource.create({ label: 'toFormat', path: '/docs/api/formatting/to-format' })); tree.add(Resource.create({ label: 'toSnapshot', path: '/docs/api/formatting/to-snapshot' })); -tree.add(Resource.create({ label: 'toUnit', path: '/docs/api/formatting/to-unit' })); +tree.add(Resource.create({ label: 'toUnits', path: '/docs/api/formatting/to-units' })); +tree.add(Resource.create({ label: 'toDecimal', path: '/docs/api/formatting/to-decimal' })); tree.add(Resource.create({ label: 'FAQ', path: '/docs/faq' })); tree.add(Resource.create({ label: 'Does Dinero.js support cryptocurrencies?', path: '/docs/faq/does-dinerojs-support-cryptocurrencies' })); diff --git a/website/next.config.js b/website/next.config.js index a32a1426e..0eb85b9ea 100644 --- a/website/next.config.js +++ b/website/next.config.js @@ -6,6 +6,16 @@ module.exports = { destination: '/docs', permanent: true, }, + { + source: '/docs/api/formatting/to-unit', + destination: '/docs/api/formatting/to-units', + permanent: true, + }, + { + source: '/docs/api/formatting/to-format', + destination: '/docs/api/formatting/to-decimal', + permanent: true, + }, { source: '/docs/advanced/:slug*', destination: '/docs/guides/:slug*', diff --git a/yarn.lock b/yarn.lock index 914b93109..9c71a42ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6804,9 +6804,9 @@ load-json-file@^6.2.0: type-fest "^0.6.0" loader-utils@^1.1.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" - integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== + version "1.4.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0" + integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q== dependencies: big.js "^5.2.2" emojis-list "^3.0.0"