-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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<TAmount, TOutput>(d: Dinero<TAmount>, t: Transformer<TAmount, TOutput>): 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 <[email protected]>
- Loading branch information
1 parent
f130337
commit e7e9a19
Showing
126 changed files
with
3,142 additions
and
1,183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,17 @@ | ||
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, | ||
minimumFractionDigits, | ||
}); | ||
} | ||
|
||
return toFormat(dineroObject, transformer); | ||
return toDecimal(dineroObject, transformer); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 0 additions & 7 deletions
7
packages/calculator-bigint/src/api/__tests__/toNumber.test.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 0 additions & 7 deletions
7
packages/calculator-number/src/api/__tests__/toNumber.test.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
1 comment
on commit e7e9a19
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
dinerojs – ./
dinerojs-git-main-dinerojs.vercel.app
v2.dinerojs.com
dinerojs-dinerojs.vercel.app
@sarahdayan for some reason the
api.md
files were reformatted incorrectly in this commit. If you run the following:rm -rf packages/*/lib yarn build
Are the
api.md
modified from what was included in this commit?