jg-bignumber 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/RECIPES.md +99 -0
- package/package.json +4 -3
package/API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# <a id="api-reference"></a>jg-bignumber API reference
|
|
2
2
|
|
|
3
|
-
Every export of `jg-bignumber@1.1.
|
|
3
|
+
Every export of `jg-bignumber@1.1.1`, 29 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.
|
|
4
4
|
|
|
5
5
|
- Functions: [`adjustByPercentage`](#api-adjustByPercentage), [`calculateExchange`](#api-calculateExchange), [`calculateExchangeFee`](#api-calculateExchangeFee), [`calculateMax`](#api-calculateMax), [`calculateMean`](#api-calculateMean), [`calculateMedian`](#api-calculateMedian), [`calculateMin`](#api-calculateMin), [`calculatePercentageChange`](#api-calculatePercentageChange), [`calculatePercentageRepresentation`](#api-calculatePercentageRepresentation), [`calculateSum`](#api-calculateSum), [`calculateWeightedEntry`](#api-calculateWeightedEntry), [`getBigNumber`](#api-getBigNumber), [`isBigNumber`](#api-isBigNumber), [`isFloat`](#api-isFloat), [`isInteger`](#api-isInteger), [`isNumber`](#api-isNumber), [`prettifyValue`](#api-prettifyValue), [`processValue`](#api-processValue)
|
|
6
6
|
- Constants: [`BigNumber`](#api-BigNumber), [`ERRORS`](#api-ERRORS)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `RECIPES.md`, patterns for apps that use the package: summing money without rounding errors, and a percentage change for a dashboard. Each block is typechecked against the release.
|
|
8
|
+
|
|
3
9
|
## 1.1.0
|
|
4
10
|
|
|
5
11
|
A fresh start: breaking changes in a minor version, once, because nothing depended on 1.0.x.
|
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ calculateWeightedEntry([
|
|
|
47
47
|
|
|
48
48
|
The full API reference ships in the package as `API.md`, generated from its TSDoc for the installed version, and is at https://jg-packages.jesusgraterol.dev/packages/jg-bignumber/.
|
|
49
49
|
|
|
50
|
+
Recipes for common jobs ship as `RECIPES.md`, with every block typechecked against the release, and are at https://jg-packages.jesusgraterol.dev/packages/jg-bignumber/recipes/.
|
|
51
|
+
|
|
50
52
|
## License
|
|
51
53
|
|
|
52
54
|
[MIT](https://github.com/jesusgraterol/jg-packages/blob/main/packages/jg-bignumber/LICENSE)
|
package/RECIPES.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# jg-bignumber recipes
|
|
2
|
+
|
|
3
|
+
Patterns for apps that add up money and report how a metric changed, with exact decimal math and rounding where you choose.
|
|
4
|
+
|
|
5
|
+
## Summing money without rounding errors
|
|
6
|
+
|
|
7
|
+
Use this for a cart, an invoice, or any total of prices. JavaScript numbers are binary fractions, so `0.1 + 0.2` is `0.30000000000000004`, and each addition can add an error. Keep amounts as decimal strings from the API or the database, do the arithmetic with jg-bignumber, and round where the invoice rounds:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { calculateSum, getBigNumber, processValue, type IBigNumber } from 'jg-bignumber';
|
|
11
|
+
|
|
12
|
+
// a line of an invoice, with its price as a decimal string, as an API or a PostgreSQL numeric
|
|
13
|
+
// column sends it, so it never passes through a float
|
|
14
|
+
type IInvoiceLine = { unitPrice: string; quantity: number };
|
|
15
|
+
|
|
16
|
+
type IInvoiceTotals = { subtotal: IBigNumber; tax: IBigNumber; total: IBigNumber };
|
|
17
|
+
|
|
18
|
+
// the reader's invoice, with its tax rate as a percentage, such as '8.25'
|
|
19
|
+
declare const invoice: { lines: IInvoiceLine[]; taxRate: string };
|
|
20
|
+
|
|
21
|
+
// the cell that shows the invoice's total
|
|
22
|
+
declare const totalCell: HTMLElement;
|
|
23
|
+
|
|
24
|
+
const calculateInvoiceTotals = (lines: IInvoiceLine[], taxRate: string): IInvoiceTotals => {
|
|
25
|
+
// each line's total stays exact, so nothing is rounded before the sum
|
|
26
|
+
const lineTotals = lines.map(({ unitPrice, quantity }) =>
|
|
27
|
+
getBigNumber(unitPrice).times(quantity),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// the subtotal and the tax are each rounded to cents, as the invoice prints them, so the
|
|
31
|
+
// printed amounts add up to the printed total
|
|
32
|
+
const subtotal = calculateSum(lineTotals, { type: 'bignumber' });
|
|
33
|
+
const tax = processValue(subtotal.times(taxRate).dividedBy(100), { type: 'bignumber' });
|
|
34
|
+
return { subtotal, tax, total: calculateSum([subtotal, tax], { type: 'bignumber' }) };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const { total } = calculateInvoiceTotals(invoice.lines, invoice.taxRate);
|
|
38
|
+
|
|
39
|
+
// toFixed and toFormat keep the cents, such as '1234.50' and '1,234.50', where a number or
|
|
40
|
+
// prettifyValue drops the trailing zero
|
|
41
|
+
const totalForApi = total.toFixed(2);
|
|
42
|
+
totalCell.textContent = `$${total.toFormat(2)}`;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every jg-bignumber calculation, and `processValue`, rounds its result, so call one where the invoice rounds, and do the steps in between with BigNumber methods, such as `times` and `dividedBy`, which don't. `{ type: 'bignumber' }` returns a BigNumber instead of a number, so the next step starts from the decimal value. Results round to 2 decimal places with `ROUND_HALF_UP`: pass `roundingMode: 'ROUND_HALF_EVEN'` where an accounting rule asks for banker's rounding, or `decimalPlaces: 0` for a currency without cents. A string that isn't a number, such as `''` from an empty form field, throws `INVALID_VALUE`, so check input with `isNumber` before it reaches the math.
|
|
46
|
+
|
|
47
|
+
## A percentage change for a dashboard
|
|
48
|
+
|
|
49
|
+
Use this for a metric tile that compares a period with the one before, such as this month's revenue against last month's. `calculatePercentageChange` returns a signed percentage, and `prettifyValue` formats it with a sign and a suffix:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import {
|
|
53
|
+
calculatePercentageChange,
|
|
54
|
+
getBigNumber,
|
|
55
|
+
prettifyValue,
|
|
56
|
+
type IBigNumberValue,
|
|
57
|
+
} from 'jg-bignumber';
|
|
58
|
+
|
|
59
|
+
// how a metric moved, which picks the tile's arrow and color
|
|
60
|
+
type ITrend = 'up' | 'down' | 'flat';
|
|
61
|
+
|
|
62
|
+
type IChange = { label: string; trend: ITrend };
|
|
63
|
+
|
|
64
|
+
// the reader's revenue for the last two months, and the tile's change label
|
|
65
|
+
declare const revenue: { previous: string; current: string };
|
|
66
|
+
declare const changeLabel: HTMLElement;
|
|
67
|
+
|
|
68
|
+
// describes how a metric changed, or returns null when the previous value can't be the base
|
|
69
|
+
const describeChange = (previous: IBigNumberValue, current: IBigNumberValue): IChange | null => {
|
|
70
|
+
// a change from 0, or from a negative value, has no meaningful percentage, and
|
|
71
|
+
// calculatePercentageChange throws NEGATIVE_VALUE_NOT_ALLOWED for one
|
|
72
|
+
if (!getBigNumber(previous).isGreaterThan(0)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// rounded to one decimal before the trend is picked, so the arrow always agrees with the label
|
|
77
|
+
const change = calculatePercentageChange(previous, current, { decimalPlaces: 1 });
|
|
78
|
+
let trend: ITrend = 'flat';
|
|
79
|
+
if (change > 0) {
|
|
80
|
+
trend = 'up';
|
|
81
|
+
} else if (change < 0) {
|
|
82
|
+
trend = 'down';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// only an increase gets a plus sign, so an unchanged metric reads 0%, not +0%
|
|
86
|
+
const label = prettifyValue(change, {
|
|
87
|
+
format: { positiveSign: trend === 'up' ? '+' : '', suffix: '%' },
|
|
88
|
+
});
|
|
89
|
+
return { label, trend };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const change = describeChange(revenue.previous, revenue.current);
|
|
93
|
+
|
|
94
|
+
// revenue that starts from 0 has no percentage, so the tile says it's new
|
|
95
|
+
changeLabel.textContent = change?.label ?? 'New';
|
|
96
|
+
changeLabel.dataset['trend'] = change?.trend ?? 'flat';
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A change too small to show, such as 0.04%, rounds to 0 and reads as flat, never as an arrow next to 0%. The largest decrease is -100%, which `calculatePercentageChange` returns whenever the current value is 0 or less. `prettifyValue` groups thousands, as in `+1,234.6%`, and drops trailing zeros, so 12% and 12.5% can sit side by side.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jg-bignumber",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Exact decimal math with bignumber.js: rounding, formatting, and sum, mean, percentage, and exchange calculations.",
|
|
5
5
|
"author": "Jesus Graterol",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"CHANGELOG.md",
|
|
19
|
-
"API.md"
|
|
19
|
+
"API.md",
|
|
20
|
+
"RECIPES.md"
|
|
20
21
|
],
|
|
21
22
|
"sideEffects": false,
|
|
22
23
|
"engines": {
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
],
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"bignumber.js": "^11.1.5",
|
|
47
|
-
"jg-exception": "^1.
|
|
48
|
+
"jg-exception": "^1.2.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@jg-packages/eslint-config": "0.0.0",
|