@c9up/atom 0.1.5 → 0.1.8
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/README.md +60 -10
- package/dist/Decimal.d.ts +13 -1
- package/dist/Decimal.d.ts.map +1 -1
- package/dist/Decimal.js +312 -39
- package/dist/Decimal.js.map +1 -1
- package/dist/Money.d.ts +33 -0
- package/dist/Money.d.ts.map +1 -0
- package/dist/Money.js +128 -0
- package/dist/Money.js.map +1 -0
- package/dist/atlas.d.ts +22 -5
- package/dist/atlas.d.ts.map +1 -1
- package/dist/atlas.js +41 -0
- package/dist/atlas.js.map +1 -1
- package/dist/context.d.ts +15 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +46 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +53 -3
- package/dist/math.js.map +1 -1
- package/dist/native.d.ts +3 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +8 -2
- package/dist/native.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +3 -3
- package/scripts/bench.mjs +26 -0
- package/scripts/verify-napi.mjs +10 -1
- package/scripts/verify-wasm.mjs +56 -0
- package/src/Decimal.ts +403 -47
- package/src/Money.ts +179 -0
- package/src/atlas.ts +78 -2
- package/src/context.ts +67 -0
- package/src/index.ts +20 -5
- package/src/math.ts +56 -3
- package/src/native.ts +9 -2
- package/wasm/atom_engine_wasm.d.ts +0 -19
package/README.md
CHANGED
|
@@ -1,29 +1,79 @@
|
|
|
1
1
|
# @c9up/atom
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Exact decimal arithmetic, statistics, and currency-safe money values for the
|
|
4
|
+
Ream ecosystem. Rust NAPI/WASM is used when available; the TypeScript BigInt
|
|
5
|
+
engine is the fallback.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pnpm add @c9up/atom
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Decimal
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Decimal, decimal, sum } from '@c9up/atom'
|
|
17
|
+
|
|
18
|
+
decimal('0.1').plus('0.2').toString() // "0.3"
|
|
19
|
+
sum(['1.1', '2.2', '3.3']).toString() // "6.6"
|
|
20
|
+
|
|
21
|
+
Decimal.safeParse('12.34') // { success: true, value: Decimal }
|
|
22
|
+
Decimal.tryParse('bad') // null
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Prefer string or bigint inputs for exact user/business data. Unsafe JS integer
|
|
26
|
+
numbers are rejected.
|
|
27
|
+
|
|
28
|
+
## Money
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { money, Money } from '@c9up/atom'
|
|
32
|
+
|
|
33
|
+
const total = money('10.00', 'USD').allocate([1, 1, 1])
|
|
34
|
+
total.map((part) => part.toString()) // ["3.34 USD", "3.33 USD", "3.33 USD"]
|
|
35
|
+
|
|
36
|
+
Money.fromMinorUnits(1999n, 'EUR').format({ locale: 'fr-FR' })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`Money` keeps currency and scale together, and rejects operations across
|
|
40
|
+
different currencies.
|
|
41
|
+
|
|
42
|
+
## Context
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { configureAtomContext, decimal, withAtomContext } from '@c9up/atom'
|
|
46
|
+
|
|
47
|
+
configureAtomContext({ precision: 8, roundMode: 'trunc' })
|
|
48
|
+
withAtomContext({ precision: 2 }, () => decimal('1').div('8').toString())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Atlas
|
|
14
52
|
|
|
15
53
|
```ts
|
|
16
|
-
import {
|
|
54
|
+
import { Column } from '@c9up/atlas'
|
|
55
|
+
import { decimalColumn } from '@c9up/atom/atlas'
|
|
56
|
+
|
|
57
|
+
class Invoice {
|
|
58
|
+
@Column(decimalColumn({ scale: 2, nullable: false }))
|
|
59
|
+
total!: Decimal
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Scripts
|
|
17
64
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
65
|
+
```bash
|
|
66
|
+
pnpm test
|
|
67
|
+
pnpm test:napi
|
|
68
|
+
pnpm test:coverage
|
|
69
|
+
pnpm bench
|
|
70
|
+
pnpm build:wasm && node scripts/verify-wasm.mjs
|
|
21
71
|
```
|
|
22
72
|
|
|
23
73
|
## Entry points
|
|
24
74
|
|
|
25
75
|
- `@c9up/atom` — main API
|
|
26
|
-
- `@c9up/atom/atlas` — Atlas
|
|
76
|
+
- `@c9up/atom/atlas` — Atlas column helpers
|
|
27
77
|
|
|
28
78
|
## License
|
|
29
79
|
|
package/dist/Decimal.d.ts
CHANGED
|
@@ -33,10 +33,21 @@ export interface ToMinorUnitsOptions {
|
|
|
33
33
|
exact?: boolean;
|
|
34
34
|
mode?: RoundMode;
|
|
35
35
|
}
|
|
36
|
+
export type DecimalSafeParseResult = {
|
|
37
|
+
success: true;
|
|
38
|
+
value: Decimal;
|
|
39
|
+
} | {
|
|
40
|
+
success: false;
|
|
41
|
+
error: Error;
|
|
42
|
+
};
|
|
36
43
|
export declare class Decimal {
|
|
37
44
|
#private;
|
|
38
|
-
constructor(value: DecimalInput);
|
|
45
|
+
constructor(value: DecimalInput, scaleHint?: number);
|
|
39
46
|
static from(value: DecimalInput): Decimal;
|
|
47
|
+
static parse(value: DecimalInput): Decimal;
|
|
48
|
+
static tryParse(value: unknown): Decimal | null;
|
|
49
|
+
static safeParse(value: unknown): DecimalSafeParseResult;
|
|
50
|
+
static isDecimal(value: unknown): value is Decimal;
|
|
40
51
|
static zero(): Decimal;
|
|
41
52
|
static one(): Decimal;
|
|
42
53
|
static fromMinorUnits(value: string | number | bigint, scale: number): Decimal;
|
|
@@ -132,6 +143,7 @@ export declare class Decimal {
|
|
|
132
143
|
* Provided for interop with APIs that expect a primitive number.
|
|
133
144
|
*/
|
|
134
145
|
toNumber(): number;
|
|
146
|
+
private sqrtTruncInt;
|
|
135
147
|
/**
|
|
136
148
|
* Format the value as a localized string via `Intl.NumberFormat`.
|
|
137
149
|
*
|
package/dist/Decimal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Decimal.d.ts","sourceRoot":"","sources":["../src/Decimal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Decimal.d.ts","sourceRoot":"","sources":["../src/Decimal.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAC9D,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7E,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,MAAM,sBAAsB,GAC/B;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjC;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAKpC,qBAAa,OAAO;;gBAIP,KAAK,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM;IAYnD,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIzC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAI1C,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI;IAK/C,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB;IAcxD,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO;IAIlD,MAAM,CAAC,IAAI,IAAI,OAAO;IAItB,MAAM,CAAC,GAAG,IAAI,OAAO;IAIrB,MAAM,CAAC,cAAc,CACpB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAC/B,KAAK,EAAE,MAAM,GACX,OAAO;IAMV,MAAM,CAAC,WAAW,CACjB,KAAK,EAAE,MAAM,EACb,gBAAgB,CAAC,EAAE,IAAI,CAAC,eAAe,GAAG,WAAW,GACnD,OAAO;IAOV,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAOlC,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAOnC,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAOnC,GAAG,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO;IAW3D,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAOjC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO;IAWnD,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO;IAYxC,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IASpC,EAAE,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIhC,EAAE,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIhC,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIjC,EAAE,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIhC,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIjC,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIjC,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIjC,KAAK,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;IAWpD,OAAO,CACN,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,YAAY,EACjB,OAAO,GAAE,cAAmB,GAC1B,OAAO;IAaV,GAAG,IAAI,OAAO;IAId,GAAG,IAAI,OAAO;IAUd,MAAM,IAAI,OAAO;IAIjB,UAAU,IAAI,OAAO;IAIrB,UAAU,IAAI,OAAO;IAIrB,SAAS,IAAI,OAAO;IAIpB,KAAK,CAAC,KAAK,SAAI,GAAG,OAAO;IAIzB,KAAK,CAAC,KAAK,SAAI,GAAG,OAAO;IAIzB,IAAI,CAAC,KAAK,SAAI,GAAG,OAAO;IAIxB,KAAK,CAAC,KAAK,SAAI,EAAE,IAAI,GAAE,SAAqB,GAAG,OAAO;IAItD;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO;IAoBpE,OAAO,CAAC,KAAK,SAAI,EAAE,IAAI,GAAE,SAAmB,GAAG,OAAO;IAStD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,SAAmB,GAAG,MAAM;IAezD;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM;IAoBtE;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO;IAI9D;;;;;OAKG;IACH,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO;IAInE;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO;IAIpE;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,OAAO,EAAE;IAsD5D,OAAO,IAAI,aAAa;IAWxB,QAAQ,IAAI,MAAM;IAIlB,MAAM,IAAI,MAAM;IAIhB;;;;OAIG;IACH,QAAQ,IAAI,MAAM;IAIlB,OAAO,CAAC,YAAY;IASpB;;;;;;;;;;;;;OAaG;IACH,QAAQ,CACP,gBAAgB,CAAC,EAAE,IAAI,CAAC,eAAe,GAAG,WAAW,EACrD,OAAO,CAAC,EAAE,IAAI,CAAC,mBAAmB,GAChC,MAAM;CAaT;AAuQD;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,mBAAmB,IAAI,uBAAuB,CAAC;IAC/C,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC;CAC9E"}
|
package/dist/Decimal.js
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { defaultPrecision, defaultQuantizeMode, defaultRoundMode, } from "./context.js";
|
|
2
|
+
import { addTs, cmpTs, divTs, formatDecimal, modTs, mulTs, parseDecimal, pow10BigInt, powTs, sqrtTs, subTs, } from "./math.js";
|
|
3
|
+
import { tryNativeAtom } from "./native.js";
|
|
4
|
+
const MAX_SCALE = 10_000;
|
|
5
|
+
const MAX_EXPONENT_ABS = 100_000;
|
|
3
6
|
export class Decimal {
|
|
4
7
|
#value;
|
|
5
|
-
|
|
8
|
+
#scaleHint;
|
|
9
|
+
constructor(value, scaleHint) {
|
|
10
|
+
if (value instanceof Decimal) {
|
|
11
|
+
this.#value = value.#value;
|
|
12
|
+
this.#scaleHint = scaleHint ?? value.#scaleHint;
|
|
13
|
+
assertScale(this.#scaleHint);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
6
16
|
this.#value = normalizeInput(value);
|
|
17
|
+
this.#scaleHint = scaleHint ?? inferInputScale(value, this.#value);
|
|
18
|
+
assertScale(this.#scaleHint);
|
|
7
19
|
}
|
|
8
20
|
static from(value) {
|
|
9
21
|
return new Decimal(value);
|
|
10
22
|
}
|
|
23
|
+
static parse(value) {
|
|
24
|
+
return new Decimal(value);
|
|
25
|
+
}
|
|
26
|
+
static tryParse(value) {
|
|
27
|
+
const parsed = Decimal.safeParse(value);
|
|
28
|
+
return parsed.success ? parsed.value : null;
|
|
29
|
+
}
|
|
30
|
+
static safeParse(value) {
|
|
31
|
+
try {
|
|
32
|
+
if (!isDecimalInput(value)) {
|
|
33
|
+
throw new Error(`Invalid decimal input type: ${typeof value}`);
|
|
34
|
+
}
|
|
35
|
+
return { success: true, value: new Decimal(value) };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
static isDecimal(value) {
|
|
45
|
+
return value instanceof Decimal;
|
|
46
|
+
}
|
|
11
47
|
static zero() {
|
|
12
48
|
return new Decimal("0");
|
|
13
49
|
}
|
|
@@ -27,56 +63,63 @@ export class Decimal {
|
|
|
27
63
|
}
|
|
28
64
|
plus(other) {
|
|
29
65
|
const b = normalizeInput(other);
|
|
30
|
-
const
|
|
66
|
+
const native = tryNativeAtom();
|
|
67
|
+
const result = native ? native.add(this.#value, b) : addTs(this.#value, b);
|
|
31
68
|
return new Decimal(result);
|
|
32
69
|
}
|
|
33
70
|
minus(other) {
|
|
34
71
|
const b = normalizeInput(other);
|
|
35
|
-
const
|
|
72
|
+
const native = tryNativeAtom();
|
|
73
|
+
const result = native ? native.sub(this.#value, b) : subTs(this.#value, b);
|
|
36
74
|
return new Decimal(result);
|
|
37
75
|
}
|
|
38
76
|
times(other) {
|
|
39
77
|
const b = normalizeInput(other);
|
|
40
|
-
const
|
|
78
|
+
const native = tryNativeAtom();
|
|
79
|
+
const result = native ? native.mul(this.#value, b) : mulTs(this.#value, b);
|
|
41
80
|
return new Decimal(result);
|
|
42
81
|
}
|
|
43
82
|
div(other, options = {}) {
|
|
44
83
|
const b = normalizeInput(other);
|
|
45
|
-
const precision = options.precision ??
|
|
84
|
+
const precision = options.precision ?? defaultPrecision();
|
|
46
85
|
assertScale(precision);
|
|
47
|
-
const
|
|
86
|
+
const native = tryNativeAtom();
|
|
87
|
+
const result = native
|
|
88
|
+
? native.div(this.#value, b, precision)
|
|
89
|
+
: divTs(this.#value, b, precision);
|
|
48
90
|
return new Decimal(result);
|
|
49
91
|
}
|
|
50
92
|
mod(other) {
|
|
51
93
|
const b = normalizeInput(other);
|
|
52
|
-
const
|
|
94
|
+
const native = tryNativeAtom();
|
|
95
|
+
const result = native ? native.rem(this.#value, b) : modTs(this.#value, b);
|
|
53
96
|
return new Decimal(result);
|
|
54
97
|
}
|
|
55
98
|
pow(exp, options = {}) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
const precision = options.precision ?? 18;
|
|
99
|
+
assertExponent(exp);
|
|
100
|
+
const precision = options.precision ?? defaultPrecision();
|
|
60
101
|
assertScale(precision);
|
|
61
|
-
const
|
|
102
|
+
const native = tryNativeAtom();
|
|
103
|
+
const result = native
|
|
104
|
+
? native.pow(this.#value, exp, precision)
|
|
105
|
+
: powTs(this.#value, exp, precision);
|
|
62
106
|
return new Decimal(result);
|
|
63
107
|
}
|
|
64
108
|
sqrt(options = {}) {
|
|
65
|
-
const precision = options.precision ??
|
|
66
|
-
const mode = options.mode ??
|
|
109
|
+
const precision = options.precision ?? defaultPrecision();
|
|
110
|
+
const mode = options.mode ?? defaultRoundMode();
|
|
67
111
|
assertScale(precision);
|
|
68
112
|
if (mode === "trunc") {
|
|
69
|
-
|
|
70
|
-
return new Decimal(result);
|
|
113
|
+
return fromIntScale(this.sqrtTruncInt(precision), precision);
|
|
71
114
|
}
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const rounded = roundIntScale(parsed.int, precision + 1, precision, mode);
|
|
115
|
+
const truncated = this.sqrtTruncInt(precision);
|
|
116
|
+
const rounded = roundSqrtInt(this.#value, truncated, precision, mode);
|
|
75
117
|
return fromIntScale(rounded, precision);
|
|
76
118
|
}
|
|
77
119
|
cmp(other) {
|
|
78
120
|
const b = normalizeInput(other);
|
|
79
|
-
const
|
|
121
|
+
const native = tryNativeAtom();
|
|
122
|
+
const result = native ? native.cmp(this.#value, b) : cmpTs(this.#value, b);
|
|
80
123
|
if (result < 0)
|
|
81
124
|
return -1;
|
|
82
125
|
if (result > 0)
|
|
@@ -172,15 +215,19 @@ export class Decimal {
|
|
|
172
215
|
* new Decimal('1.025').quantize('0.05') // → Decimal('1.05')
|
|
173
216
|
*/
|
|
174
217
|
quantize(step, options = {}) {
|
|
175
|
-
const { mode =
|
|
218
|
+
const { mode = defaultQuantizeMode() } = options;
|
|
176
219
|
const stepValue = new Decimal(step);
|
|
177
220
|
if (!stepValue.gt(0)) {
|
|
178
221
|
throw new Error("Quantize step must be greater than zero");
|
|
179
222
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
223
|
+
if (options.precision !== undefined) {
|
|
224
|
+
assertScale(options.precision);
|
|
225
|
+
}
|
|
226
|
+
const thisParts = parseDecimal(this.#value);
|
|
227
|
+
const stepParts = parseDecimal(stepValue.#value);
|
|
228
|
+
const numerator = thisParts.int * pow10BigInt(stepParts.scale);
|
|
229
|
+
const denominator = stepParts.int * pow10BigInt(thisParts.scale);
|
|
230
|
+
const units = fromIntScale(roundRationalToInt(numerator, denominator, mode), 0);
|
|
184
231
|
return units.times(stepValue);
|
|
185
232
|
}
|
|
186
233
|
toScale(scale = 0, mode = "trunc") {
|
|
@@ -282,9 +329,9 @@ export class Decimal {
|
|
|
282
329
|
if (ratioTotal <= 0n) {
|
|
283
330
|
throw new Error("Allocate requires at least one positive ratio");
|
|
284
331
|
}
|
|
285
|
-
const parsed =
|
|
286
|
-
const sign = parsed.
|
|
287
|
-
const total = parsed.
|
|
332
|
+
const parsed = this.toParts();
|
|
333
|
+
const sign = parsed.value < 0n ? -1n : 1n;
|
|
334
|
+
const total = parsed.value < 0n ? -parsed.value : parsed.value;
|
|
288
335
|
const baseShares = [];
|
|
289
336
|
const remainders = [];
|
|
290
337
|
let consumed = 0n;
|
|
@@ -317,7 +364,13 @@ export class Decimal {
|
|
|
317
364
|
}
|
|
318
365
|
toParts() {
|
|
319
366
|
const parsed = parseDecimal(this.#value);
|
|
320
|
-
|
|
367
|
+
if (this.#scaleHint <= parsed.scale) {
|
|
368
|
+
return { value: parsed.int, scale: parsed.scale };
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
value: parsed.int * pow10BigInt(this.#scaleHint - parsed.scale),
|
|
372
|
+
scale: this.#scaleHint,
|
|
373
|
+
};
|
|
321
374
|
}
|
|
322
375
|
toString() {
|
|
323
376
|
return this.#value;
|
|
@@ -333,6 +386,14 @@ export class Decimal {
|
|
|
333
386
|
toNumber() {
|
|
334
387
|
return Number(this.#value);
|
|
335
388
|
}
|
|
389
|
+
sqrtTruncInt(precision) {
|
|
390
|
+
const native = tryNativeAtom();
|
|
391
|
+
const result = native
|
|
392
|
+
? native.sqrt(this.#value, precision)
|
|
393
|
+
: sqrtTs(this.#value, precision);
|
|
394
|
+
const parsed = parseDecimal(result);
|
|
395
|
+
return roundIntScale(parsed.int, parsed.scale, precision, "trunc");
|
|
396
|
+
}
|
|
336
397
|
/**
|
|
337
398
|
* Format the value as a localized string via `Intl.NumberFormat`.
|
|
338
399
|
*
|
|
@@ -369,10 +430,60 @@ function normalizeInput(value) {
|
|
|
369
430
|
if (!Number.isFinite(value)) {
|
|
370
431
|
throw new Error(`Invalid decimal: ${value}`);
|
|
371
432
|
}
|
|
372
|
-
return normalizeDecimalString(
|
|
433
|
+
return normalizeDecimalString(decimalStringFromNumber(value));
|
|
373
434
|
}
|
|
374
435
|
return normalizeDecimalString(value);
|
|
375
436
|
}
|
|
437
|
+
function isDecimalInput(value) {
|
|
438
|
+
return (value instanceof Decimal ||
|
|
439
|
+
typeof value === "string" ||
|
|
440
|
+
typeof value === "number" ||
|
|
441
|
+
typeof value === "bigint");
|
|
442
|
+
}
|
|
443
|
+
function inferInputScale(value, normalized) {
|
|
444
|
+
if (typeof value === "bigint")
|
|
445
|
+
return 0;
|
|
446
|
+
if (typeof value === "number") {
|
|
447
|
+
return parseDecimal(decimalStringFromNumber(value)).scale;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
return parseDecimal(value).scale;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return parseDecimal(normalized).scale;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function decimalStringFromNumber(value) {
|
|
457
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
458
|
+
throw new Error(`Unsafe integer decimal input: ${value}`);
|
|
459
|
+
}
|
|
460
|
+
const raw = String(value);
|
|
461
|
+
if (!/[eE]/.test(raw))
|
|
462
|
+
return raw;
|
|
463
|
+
const [coefficient, exponentRaw] = raw.toLowerCase().split("e");
|
|
464
|
+
const exponent = Number(exponentRaw);
|
|
465
|
+
if (!Number.isInteger(exponent)) {
|
|
466
|
+
throw new Error(`Invalid decimal: ${value}`);
|
|
467
|
+
}
|
|
468
|
+
const negative = coefficient.startsWith("-");
|
|
469
|
+
const unsigned = negative || coefficient.startsWith("+")
|
|
470
|
+
? coefficient.slice(1)
|
|
471
|
+
: coefficient;
|
|
472
|
+
const [wholeRaw, fracRaw = ""] = unsigned.split(".");
|
|
473
|
+
const digits = `${wholeRaw}${fracRaw}`;
|
|
474
|
+
const decimalIndex = wholeRaw.length + exponent;
|
|
475
|
+
let expanded;
|
|
476
|
+
if (decimalIndex <= 0) {
|
|
477
|
+
expanded = `0.${"0".repeat(-decimalIndex)}${digits}`;
|
|
478
|
+
}
|
|
479
|
+
else if (decimalIndex >= digits.length) {
|
|
480
|
+
expanded = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
expanded = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
|
|
484
|
+
}
|
|
485
|
+
return negative ? `-${expanded}` : expanded;
|
|
486
|
+
}
|
|
376
487
|
function normalizeDecimalString(input) {
|
|
377
488
|
const parsed = parseDecimal(input);
|
|
378
489
|
return formatDecimal(parsed.int, parsed.scale);
|
|
@@ -384,6 +495,9 @@ function parseIntegerInput(value) {
|
|
|
384
495
|
if (!Number.isInteger(value)) {
|
|
385
496
|
throw new Error(`Invalid integer: ${value}`);
|
|
386
497
|
}
|
|
498
|
+
if (!Number.isSafeInteger(value)) {
|
|
499
|
+
throw new Error(`Unsafe integer input: ${value}`);
|
|
500
|
+
}
|
|
387
501
|
return BigInt(value);
|
|
388
502
|
}
|
|
389
503
|
const s = value.trim();
|
|
@@ -393,13 +507,20 @@ function parseIntegerInput(value) {
|
|
|
393
507
|
return BigInt(s);
|
|
394
508
|
}
|
|
395
509
|
function fromIntScale(int, scale) {
|
|
396
|
-
return new Decimal(formatDecimal(int, scale));
|
|
510
|
+
return new Decimal(formatDecimal(int, scale), scale);
|
|
397
511
|
}
|
|
398
512
|
function assertScale(scale) {
|
|
399
|
-
if (!Number.isInteger(scale) || scale < 0) {
|
|
513
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
400
514
|
throw new Error(`Invalid scale: ${scale}`);
|
|
401
515
|
}
|
|
402
516
|
}
|
|
517
|
+
function assertExponent(exp) {
|
|
518
|
+
if (!Number.isInteger(exp) ||
|
|
519
|
+
exp < -MAX_EXPONENT_ABS ||
|
|
520
|
+
exp > MAX_EXPONENT_ABS) {
|
|
521
|
+
throw new Error(`Invalid exponent: ${exp}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
403
524
|
function roundIntScale(int, sourceScale, targetScale, mode) {
|
|
404
525
|
if (targetScale >= sourceScale) {
|
|
405
526
|
return int * pow10BigInt(targetScale - sourceScale);
|
|
@@ -439,6 +560,73 @@ function roundIntScale(int, sourceScale, targetScale, mode) {
|
|
|
439
560
|
throw new Error(`Unknown rounding mode: ${String(mode)}`);
|
|
440
561
|
}
|
|
441
562
|
}
|
|
563
|
+
function roundRationalToInt(numerator, denominator, mode) {
|
|
564
|
+
if (denominator <= 0n) {
|
|
565
|
+
throw new Error("Invalid rational denominator");
|
|
566
|
+
}
|
|
567
|
+
const q = numerator / denominator;
|
|
568
|
+
const r = numerator % denominator;
|
|
569
|
+
if (r === 0n)
|
|
570
|
+
return q;
|
|
571
|
+
const absR = r < 0n ? -r : r;
|
|
572
|
+
const sign = numerator < 0n ? -1n : 1n;
|
|
573
|
+
switch (mode) {
|
|
574
|
+
case "trunc":
|
|
575
|
+
return q;
|
|
576
|
+
case "floor":
|
|
577
|
+
return numerator < 0n ? q - 1n : q;
|
|
578
|
+
case "ceil":
|
|
579
|
+
return numerator > 0n ? q + 1n : q;
|
|
580
|
+
case "half-up":
|
|
581
|
+
return absR * 2n >= denominator ? q + sign : q;
|
|
582
|
+
case "half-even": {
|
|
583
|
+
const twice = absR * 2n;
|
|
584
|
+
if (twice < denominator)
|
|
585
|
+
return q;
|
|
586
|
+
if (twice > denominator)
|
|
587
|
+
return q + sign;
|
|
588
|
+
const isEven = (q < 0n ? -q : q) % 2n === 0n;
|
|
589
|
+
return isEven ? q : q + sign;
|
|
590
|
+
}
|
|
591
|
+
default:
|
|
592
|
+
throw new Error(`Unknown rounding mode: ${String(mode)}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function roundSqrtInt(value, truncated, precision, mode) {
|
|
596
|
+
const parsed = parseDecimal(value);
|
|
597
|
+
if (parsed.int < 0n) {
|
|
598
|
+
throw new Error("Cannot compute sqrt of a negative decimal");
|
|
599
|
+
}
|
|
600
|
+
const squareCmp = compareScaled(truncated * truncated, 2 * precision, parsed.int, parsed.scale);
|
|
601
|
+
if (mode === "floor")
|
|
602
|
+
return truncated;
|
|
603
|
+
if (mode === "ceil") {
|
|
604
|
+
return squareCmp === 0 ? truncated : truncated + 1n;
|
|
605
|
+
}
|
|
606
|
+
const thresholdCmp = compareScaled((2n * truncated + 1n) * (2n * truncated + 1n), 2 * precision, 4n * parsed.int, parsed.scale);
|
|
607
|
+
if (thresholdCmp < 0)
|
|
608
|
+
return truncated + 1n;
|
|
609
|
+
if (thresholdCmp > 0)
|
|
610
|
+
return truncated;
|
|
611
|
+
if (mode === "half-up")
|
|
612
|
+
return truncated + 1n;
|
|
613
|
+
return truncated % 2n === 0n ? truncated : truncated + 1n;
|
|
614
|
+
}
|
|
615
|
+
function compareScaled(leftInt, leftScale, rightInt, rightScale) {
|
|
616
|
+
let left = leftInt;
|
|
617
|
+
let right = rightInt;
|
|
618
|
+
if (leftScale > rightScale) {
|
|
619
|
+
right *= pow10BigInt(leftScale - rightScale);
|
|
620
|
+
}
|
|
621
|
+
else if (rightScale > leftScale) {
|
|
622
|
+
left *= pow10BigInt(rightScale - leftScale);
|
|
623
|
+
}
|
|
624
|
+
if (left < right)
|
|
625
|
+
return -1;
|
|
626
|
+
if (left > right)
|
|
627
|
+
return 1;
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
442
630
|
function isRosettaLike(arg) {
|
|
443
631
|
return (typeof arg === "object" &&
|
|
444
632
|
arg !== null &&
|
|
@@ -463,8 +651,6 @@ function normalizeViaRosetta(input, rosetta) {
|
|
|
463
651
|
plusSign: raw.plusSign || "+",
|
|
464
652
|
};
|
|
465
653
|
let normalized = trimmed.replace(/\s| | /g, "");
|
|
466
|
-
normalized = normalized.replace(new RegExp(escapeRegExp(data.group), "g"), "");
|
|
467
|
-
normalized = normalized.replace(new RegExp(escapeRegExp(data.decimal), "g"), ".");
|
|
468
654
|
if (data.minus !== "-") {
|
|
469
655
|
normalized = normalized.replace(new RegExp(escapeRegExp(data.minus), "g"), "-");
|
|
470
656
|
}
|
|
@@ -477,6 +663,9 @@ function normalizeViaRosetta(input, rosetta) {
|
|
|
477
663
|
if (/^\(.*\)$/.test(normalized)) {
|
|
478
664
|
normalized = `-${normalized.slice(1, -1)}`;
|
|
479
665
|
}
|
|
666
|
+
validateLocalizedSyntax(normalized, data.group, data.decimal);
|
|
667
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(data.group), "g"), "");
|
|
668
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(data.decimal), "g"), ".");
|
|
480
669
|
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
481
670
|
throw new Error(`Invalid localized decimal: ${input}`);
|
|
482
671
|
}
|
|
@@ -492,9 +681,8 @@ function normalizeLocaleNumber(input, locales) {
|
|
|
492
681
|
const group = parts.find((part) => part.type === "group")?.value ?? ",";
|
|
493
682
|
const decimal = parts.find((part) => part.type === "decimal")?.value ?? ".";
|
|
494
683
|
const minus = parts.find((part) => part.type === "minusSign")?.value ?? "-";
|
|
495
|
-
let normalized = trimmed
|
|
496
|
-
normalized = normalized.replace(
|
|
497
|
-
normalized = normalized.replace(new RegExp(escapeRegExp(decimal), "g"), ".");
|
|
684
|
+
let normalized = normalizeLocaleDigits(trimmed, locales);
|
|
685
|
+
normalized = normalized.replace(/\s|\u00A0|\u202F/g, "");
|
|
498
686
|
if (minus !== "-") {
|
|
499
687
|
normalized = normalized.replace(new RegExp(escapeRegExp(minus), "g"), "-");
|
|
500
688
|
}
|
|
@@ -502,11 +690,96 @@ function normalizeLocaleNumber(input, locales) {
|
|
|
502
690
|
if (/^\(.*\)$/.test(normalized)) {
|
|
503
691
|
normalized = `-${normalized.slice(1, -1)}`;
|
|
504
692
|
}
|
|
693
|
+
validateLocalizedSyntax(normalized, group, decimal, getLocaleGrouping(locales));
|
|
694
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(group), "g"), "");
|
|
695
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(decimal), "g"), ".");
|
|
505
696
|
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
506
697
|
throw new Error(`Invalid localized decimal: ${input}`);
|
|
507
698
|
}
|
|
508
699
|
return normalized;
|
|
509
700
|
}
|
|
701
|
+
function normalizeLocaleDigits(input, locales) {
|
|
702
|
+
const digitMap = getLocaleDigitMap(locales);
|
|
703
|
+
let out = "";
|
|
704
|
+
for (const char of input) {
|
|
705
|
+
out += digitMap.get(char) ?? char;
|
|
706
|
+
}
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
709
|
+
function getLocaleDigitMap(locales) {
|
|
710
|
+
const formatter = new Intl.NumberFormat(locales, { useGrouping: false });
|
|
711
|
+
const digits = formatter.format(9876543210);
|
|
712
|
+
const map = new Map();
|
|
713
|
+
let value = 9;
|
|
714
|
+
for (const char of digits) {
|
|
715
|
+
if (!map.has(char) && value >= 0) {
|
|
716
|
+
map.set(char, String(value));
|
|
717
|
+
value--;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return map;
|
|
721
|
+
}
|
|
722
|
+
function getLocaleGrouping(locales) {
|
|
723
|
+
const parts = new Intl.NumberFormat(locales)
|
|
724
|
+
.formatToParts(1234567890123)
|
|
725
|
+
.filter((part) => part.type === "integer" || part.type === "group");
|
|
726
|
+
const lengths = [];
|
|
727
|
+
let current = 0;
|
|
728
|
+
for (const part of parts) {
|
|
729
|
+
if (part.type === "integer") {
|
|
730
|
+
current += [...part.value].length;
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
lengths.push(current);
|
|
734
|
+
current = 0;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
lengths.push(current);
|
|
738
|
+
if (lengths.length < 2) {
|
|
739
|
+
return { primary: 3, secondary: 3 };
|
|
740
|
+
}
|
|
741
|
+
const primary = lengths[lengths.length - 1];
|
|
742
|
+
const secondary = lengths[lengths.length - 2] ?? primary;
|
|
743
|
+
return { primary, secondary };
|
|
744
|
+
}
|
|
745
|
+
function validateLocalizedSyntax(value, group, decimal, grouping = { primary: 3, secondary: 3 }) {
|
|
746
|
+
const decimalParts = value.split(decimal);
|
|
747
|
+
if (decimalParts.length > 2) {
|
|
748
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
749
|
+
}
|
|
750
|
+
let integer = decimalParts[0] ?? "";
|
|
751
|
+
const fraction = decimalParts[1];
|
|
752
|
+
if (integer.startsWith("+") || integer.startsWith("-")) {
|
|
753
|
+
integer = integer.slice(1);
|
|
754
|
+
}
|
|
755
|
+
if (!integer) {
|
|
756
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
757
|
+
}
|
|
758
|
+
if (fraction !== undefined && !/^\d+$/.test(fraction)) {
|
|
759
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
760
|
+
}
|
|
761
|
+
if (!integer.includes(group)) {
|
|
762
|
+
if (!/^\d+$/.test(integer)) {
|
|
763
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const groups = integer.split(group);
|
|
768
|
+
if (groups.some((part) => !/^\d+$/.test(part))) {
|
|
769
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
770
|
+
}
|
|
771
|
+
for (let i = groups.length - 1, distance = 0; i >= 0; i--, distance++) {
|
|
772
|
+
const size = distance === 0 ? grouping.primary : grouping.secondary;
|
|
773
|
+
if (i === 0) {
|
|
774
|
+
if (groups[i].length < 1 || groups[i].length > size) {
|
|
775
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
else if (groups[i].length !== size) {
|
|
779
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
510
783
|
function escapeRegExp(value) {
|
|
511
784
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
512
785
|
}
|