@c9up/atom 0.1.6 → 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 +2 -1
- package/scripts/bench.mjs +26 -0
- package/scripts/verify-napi.mjs +10 -1
- package/scripts/verify-wasm.mjs +32 -3
- 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/src/Money.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { defaultQuantizeMode } from "./context.js";
|
|
2
|
+
import { Decimal, type DecimalInput, type RoundMode } from "./Decimal.js";
|
|
3
|
+
|
|
4
|
+
export interface MoneyOptions {
|
|
5
|
+
scale?: number;
|
|
6
|
+
exact?: boolean;
|
|
7
|
+
mode?: RoundMode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MoneyFormatOptions
|
|
11
|
+
extends Omit<Intl.NumberFormatOptions, "style" | "currency"> {
|
|
12
|
+
locale?: Intl.LocalesArgument;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ISO_MINOR_UNITS: Record<string, number> = Object.freeze({
|
|
16
|
+
BHD: 3,
|
|
17
|
+
CLF: 4,
|
|
18
|
+
CLP: 0,
|
|
19
|
+
DJF: 0,
|
|
20
|
+
EUR: 2,
|
|
21
|
+
GBP: 2,
|
|
22
|
+
JPY: 0,
|
|
23
|
+
KMF: 0,
|
|
24
|
+
KRW: 0,
|
|
25
|
+
KWD: 3,
|
|
26
|
+
LYD: 3,
|
|
27
|
+
OMR: 3,
|
|
28
|
+
PYG: 0,
|
|
29
|
+
TND: 3,
|
|
30
|
+
USD: 2,
|
|
31
|
+
VND: 0,
|
|
32
|
+
XAF: 0,
|
|
33
|
+
XOF: 0,
|
|
34
|
+
XPF: 0,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export class Money {
|
|
38
|
+
#amount: Decimal;
|
|
39
|
+
#currency: string;
|
|
40
|
+
#scale: number;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
amount: DecimalInput,
|
|
44
|
+
currency: string,
|
|
45
|
+
options: MoneyOptions = {},
|
|
46
|
+
) {
|
|
47
|
+
this.#currency = normalizeCurrency(currency);
|
|
48
|
+
this.#scale = options.scale ?? ISO_MINOR_UNITS[this.#currency] ?? 2;
|
|
49
|
+
const mode = options.mode ?? defaultQuantizeMode();
|
|
50
|
+
const exact = options.exact ?? true;
|
|
51
|
+
const decimal = new Decimal(amount);
|
|
52
|
+
this.#amount = exact
|
|
53
|
+
? Decimal.fromMinorUnits(decimal.toMinorUnits(this.#scale), this.#scale)
|
|
54
|
+
: decimal.toScale(this.#scale, mode);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static fromMajor(
|
|
58
|
+
amount: DecimalInput,
|
|
59
|
+
currency: string,
|
|
60
|
+
options: MoneyOptions = {},
|
|
61
|
+
): Money {
|
|
62
|
+
return new Money(amount, currency, options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static fromMinorUnits(
|
|
66
|
+
minorUnits: string | number | bigint,
|
|
67
|
+
currency: string,
|
|
68
|
+
options: Omit<MoneyOptions, "exact"> = {},
|
|
69
|
+
): Money {
|
|
70
|
+
const normalized = normalizeCurrency(currency);
|
|
71
|
+
const scale = options.scale ?? ISO_MINOR_UNITS[normalized] ?? 2;
|
|
72
|
+
return new Money(Decimal.fromMinorUnits(minorUnits, scale), normalized, {
|
|
73
|
+
...options,
|
|
74
|
+
scale,
|
|
75
|
+
exact: true,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get amount(): Decimal {
|
|
80
|
+
return this.#amount;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get currency(): string {
|
|
84
|
+
return this.#currency;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get scale(): number {
|
|
88
|
+
return this.#scale;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
plus(other: Money): Money {
|
|
92
|
+
this.assertSameCurrency(other);
|
|
93
|
+
return new Money(this.#amount.plus(other.#amount), this.#currency, {
|
|
94
|
+
scale: this.#scale,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
minus(other: Money): Money {
|
|
99
|
+
this.assertSameCurrency(other);
|
|
100
|
+
return new Money(this.#amount.minus(other.#amount), this.#currency, {
|
|
101
|
+
scale: this.#scale,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
times(multiplier: DecimalInput, options: MoneyOptions = {}): Money {
|
|
106
|
+
return new Money(this.#amount.times(multiplier), this.#currency, {
|
|
107
|
+
scale: this.#scale,
|
|
108
|
+
exact: false,
|
|
109
|
+
mode: options.mode,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
div(divisor: DecimalInput, options: MoneyOptions = {}): Money {
|
|
114
|
+
return new Money(this.#amount.div(divisor), this.#currency, {
|
|
115
|
+
scale: this.#scale,
|
|
116
|
+
exact: false,
|
|
117
|
+
mode: options.mode,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
allocate(ratios: Array<string | number | bigint>): Money[] {
|
|
122
|
+
return this.#amount.allocate(ratios).map(
|
|
123
|
+
(part) =>
|
|
124
|
+
new Money(part, this.#currency, {
|
|
125
|
+
scale: this.#scale,
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
toMinorUnits(): bigint {
|
|
131
|
+
return this.#amount.toMinorUnits(this.#scale);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
format(options: MoneyFormatOptions = {}): string {
|
|
135
|
+
const { locale, ...intlOptions } = options;
|
|
136
|
+
return this.#amount.toLocale(locale, {
|
|
137
|
+
style: "currency",
|
|
138
|
+
currency: this.#currency,
|
|
139
|
+
minimumFractionDigits: this.#scale,
|
|
140
|
+
maximumFractionDigits: this.#scale,
|
|
141
|
+
...intlOptions,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
toString(): string {
|
|
146
|
+
return `${this.#amount.toFixed(this.#scale)} ${this.#currency}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
toJSON(): { amount: string; currency: string } {
|
|
150
|
+
return {
|
|
151
|
+
amount: this.#amount.toFixed(this.#scale),
|
|
152
|
+
currency: this.#currency,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private assertSameCurrency(other: Money): void {
|
|
157
|
+
if (this.#currency !== other.#currency || this.#scale !== other.#scale) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Currency mismatch: ${this.#currency}/${this.#scale} !== ${other.#currency}/${other.#scale}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function money(
|
|
166
|
+
amount: DecimalInput,
|
|
167
|
+
currency: string,
|
|
168
|
+
options: MoneyOptions = {},
|
|
169
|
+
): Money {
|
|
170
|
+
return new Money(amount, currency, options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeCurrency(currency: string): string {
|
|
174
|
+
const normalized = currency.trim().toUpperCase();
|
|
175
|
+
if (!/^[A-Z]{3}$/.test(normalized)) {
|
|
176
|
+
throw new Error(`Invalid currency code: ${currency}`);
|
|
177
|
+
}
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
package/src/atlas.ts
CHANGED
|
@@ -23,7 +23,29 @@
|
|
|
23
23
|
* @implements Story 35.10
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { Decimal } from "./Decimal.js";
|
|
26
|
+
import { Decimal, type RoundMode } from "./Decimal.js";
|
|
27
|
+
|
|
28
|
+
export interface DecimalColumnOptions {
|
|
29
|
+
scale?: number;
|
|
30
|
+
exact?: boolean;
|
|
31
|
+
mode?: RoundMode;
|
|
32
|
+
nullable?: boolean;
|
|
33
|
+
columnType?: "decimal" | "numeric";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DecimalAtlasAdapter {
|
|
37
|
+
consume(raw: unknown): Decimal | null;
|
|
38
|
+
prepare(value: unknown): string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DecimalAtlasColumn extends DecimalAtlasAdapter {
|
|
42
|
+
meta: {
|
|
43
|
+
atomDecimal: true;
|
|
44
|
+
columnType: "decimal" | "numeric";
|
|
45
|
+
scale?: number;
|
|
46
|
+
nullable: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
27
49
|
|
|
28
50
|
/**
|
|
29
51
|
* Atlas adapter for postgres `numeric` / `decimal` (and equivalent on mysql /
|
|
@@ -55,7 +77,7 @@ import { Decimal } from "./Decimal.js";
|
|
|
55
77
|
* plugin cannot monkey-patch `prepare` / `consume` at runtime and silently
|
|
56
78
|
* corrupt every repository sharing this import.
|
|
57
79
|
*/
|
|
58
|
-
export const decimalAtlasAdapter = Object.freeze({
|
|
80
|
+
export const decimalAtlasAdapter: DecimalAtlasAdapter = Object.freeze({
|
|
59
81
|
consume(raw: unknown): Decimal | null {
|
|
60
82
|
if (raw === null || raw === undefined) return null;
|
|
61
83
|
if (raw instanceof Decimal) return raw;
|
|
@@ -81,3 +103,57 @@ export const decimalAtlasAdapter = Object.freeze({
|
|
|
81
103
|
return value.toString();
|
|
82
104
|
},
|
|
83
105
|
});
|
|
106
|
+
|
|
107
|
+
export function decimalColumn(
|
|
108
|
+
options: DecimalColumnOptions = {},
|
|
109
|
+
): DecimalAtlasColumn {
|
|
110
|
+
const columnType = options.columnType ?? "decimal";
|
|
111
|
+
const nullable = options.nullable ?? true;
|
|
112
|
+
const adapter: DecimalAtlasColumn = {
|
|
113
|
+
meta: {
|
|
114
|
+
atomDecimal: true,
|
|
115
|
+
columnType,
|
|
116
|
+
scale: options.scale,
|
|
117
|
+
nullable,
|
|
118
|
+
},
|
|
119
|
+
consume(raw: unknown): Decimal | null {
|
|
120
|
+
const value = decimalAtlasAdapter.consume(raw);
|
|
121
|
+
if (value === null) {
|
|
122
|
+
if (!nullable) {
|
|
123
|
+
throw new TypeError(
|
|
124
|
+
"decimalColumn.consume: non-nullable column got null",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return normalizeColumnDecimal(value, options);
|
|
130
|
+
},
|
|
131
|
+
prepare(value: unknown): string | null {
|
|
132
|
+
const prepared = decimalAtlasAdapter.prepare(value);
|
|
133
|
+
if (prepared === null) {
|
|
134
|
+
if (!nullable) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
"decimalColumn.prepare: non-nullable column got null",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return normalizeColumnDecimal(new Decimal(prepared), options).toString();
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
return Object.freeze(adapter);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeColumnDecimal(
|
|
148
|
+
value: Decimal,
|
|
149
|
+
options: DecimalColumnOptions,
|
|
150
|
+
): Decimal {
|
|
151
|
+
if (options.scale === undefined) return value;
|
|
152
|
+
if (options.exact ?? true) {
|
|
153
|
+
return Decimal.fromMinorUnits(
|
|
154
|
+
value.toMinorUnits(options.scale),
|
|
155
|
+
options.scale,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return value.toScale(options.scale, options.mode ?? "half-up");
|
|
159
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RoundMode } from "./Decimal.js";
|
|
2
|
+
|
|
3
|
+
export interface AtomContext {
|
|
4
|
+
precision: number;
|
|
5
|
+
roundMode: RoundMode;
|
|
6
|
+
quantizeMode: RoundMode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type AtomContextOptions = Partial<AtomContext>;
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONTEXT: AtomContext = Object.freeze({
|
|
12
|
+
precision: 18,
|
|
13
|
+
roundMode: "trunc",
|
|
14
|
+
quantizeMode: "half-up",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let currentContext: AtomContext = DEFAULT_CONTEXT;
|
|
18
|
+
|
|
19
|
+
export function getAtomContext(): AtomContext {
|
|
20
|
+
return { ...currentContext };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function configureAtomContext(options: AtomContextOptions): AtomContext {
|
|
24
|
+
currentContext = normalizeContext({ ...currentContext, ...options });
|
|
25
|
+
return getAtomContext();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resetAtomContext(): AtomContext {
|
|
29
|
+
currentContext = DEFAULT_CONTEXT;
|
|
30
|
+
return getAtomContext();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function withAtomContext<T>(
|
|
34
|
+
options: AtomContextOptions,
|
|
35
|
+
callback: () => T,
|
|
36
|
+
): T {
|
|
37
|
+
const previous = currentContext;
|
|
38
|
+
currentContext = normalizeContext({ ...currentContext, ...options });
|
|
39
|
+
try {
|
|
40
|
+
return callback();
|
|
41
|
+
} finally {
|
|
42
|
+
currentContext = previous;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function defaultPrecision(): number {
|
|
47
|
+
return currentContext.precision;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function defaultRoundMode(): RoundMode {
|
|
51
|
+
return currentContext.roundMode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defaultQuantizeMode(): RoundMode {
|
|
55
|
+
return currentContext.quantizeMode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeContext(context: AtomContext): AtomContext {
|
|
59
|
+
assertContextPrecision(context.precision);
|
|
60
|
+
return Object.freeze({ ...context });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertContextPrecision(precision: number): void {
|
|
64
|
+
if (!Number.isInteger(precision) || precision < 0 || precision > 10_000) {
|
|
65
|
+
throw new Error(`Invalid Atom context precision: ${precision}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @c9up/atom — exact decimal arithmetic.
|
|
3
|
-
* The Rust engine is
|
|
3
|
+
* The Rust engine is preferred (NAPI in Node, WASM in browser), with a
|
|
4
|
+
* pure TypeScript BigInt fallback for unsupported platforms.
|
|
4
5
|
*/
|
|
5
6
|
|
|
7
|
+
export type { AtomContext, AtomContextOptions } from "./context.js";
|
|
8
|
+
export {
|
|
9
|
+
configureAtomContext,
|
|
10
|
+
getAtomContext,
|
|
11
|
+
resetAtomContext,
|
|
12
|
+
withAtomContext,
|
|
13
|
+
} from "./context.js";
|
|
6
14
|
export type {
|
|
7
15
|
BetweenOptions,
|
|
8
16
|
DecimalInput,
|
|
17
|
+
DecimalSafeParseResult,
|
|
9
18
|
DecimalScaled,
|
|
10
19
|
DivOptions,
|
|
11
20
|
MedianOptions,
|
|
@@ -17,10 +26,14 @@ export type {
|
|
|
17
26
|
ToMinorUnitsOptions,
|
|
18
27
|
} from "./Decimal.js";
|
|
19
28
|
export { Decimal } from "./Decimal.js";
|
|
29
|
+
export type { MoneyFormatOptions, MoneyOptions } from "./Money.js";
|
|
30
|
+
export { Money, money } from "./Money.js";
|
|
20
31
|
export { isNativeAvailable } from "./native.js";
|
|
21
32
|
|
|
33
|
+
import { defaultPrecision, defaultRoundMode } from "./context.js";
|
|
22
34
|
import type { MedianOptions, StddevOptions } from "./Decimal.js";
|
|
23
35
|
import { Decimal, type DecimalInput } from "./Decimal.js";
|
|
36
|
+
import { money } from "./Money.js";
|
|
24
37
|
|
|
25
38
|
export function decimal(value: DecimalInput): Decimal {
|
|
26
39
|
return Decimal.from(value);
|
|
@@ -98,7 +111,7 @@ function medianImpl(
|
|
|
98
111
|
list.sort((a, b) => a.cmp(b));
|
|
99
112
|
const mid = Math.floor(list.length / 2);
|
|
100
113
|
if (list.length % 2 === 1) return list[mid];
|
|
101
|
-
const precision = options.precision ??
|
|
114
|
+
const precision = options.precision ?? defaultPrecision();
|
|
102
115
|
return list[mid - 1].plus(list[mid]).div("2", { precision });
|
|
103
116
|
}
|
|
104
117
|
|
|
@@ -109,7 +122,7 @@ function modeImpl(values: Iterable<DecimalInput>): Decimal[] {
|
|
|
109
122
|
frequencies.set(key, (frequencies.get(key) ?? 0) + 1);
|
|
110
123
|
}
|
|
111
124
|
if (frequencies.size === 0) {
|
|
112
|
-
|
|
125
|
+
return [];
|
|
113
126
|
}
|
|
114
127
|
let maxCount = 0;
|
|
115
128
|
for (const count of frequencies.values()) {
|
|
@@ -131,8 +144,8 @@ function stddevImpl(
|
|
|
131
144
|
throw new Error("Atom.stddev requires at least one value");
|
|
132
145
|
}
|
|
133
146
|
const sample = options.sample ?? false;
|
|
134
|
-
const precision = options.precision ??
|
|
135
|
-
const mode = options.mode ??
|
|
147
|
+
const precision = options.precision ?? defaultPrecision();
|
|
148
|
+
const mode = options.mode ?? defaultRoundMode();
|
|
136
149
|
const divisor = sample ? list.length - 1 : list.length;
|
|
137
150
|
if (divisor <= 0) {
|
|
138
151
|
throw new Error("Atom.stddev sample mode requires at least two values");
|
|
@@ -256,6 +269,8 @@ export const Atom = {
|
|
|
256
269
|
/** Parse a locale-formatted decimal string (e.g. `'1.234,56'` in `fr-FR`). */
|
|
257
270
|
parseLocale: (value: string, locales?: Intl.LocalesArgument) =>
|
|
258
271
|
Decimal.parseLocale(value, locales),
|
|
272
|
+
/** Construct a currency-bound `Money` value. */
|
|
273
|
+
money,
|
|
259
274
|
};
|
|
260
275
|
|
|
261
276
|
export const sum = sumFn;
|
package/src/math.ts
CHANGED
|
@@ -3,11 +3,61 @@ export interface ParsedDecimal {
|
|
|
3
3
|
scale: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* How far an exponent may move the decimal point.
|
|
8
|
+
*
|
|
9
|
+
* An expression like `1E1000000` is syntactically fine and would expand to a
|
|
10
|
+
* megabyte of zeroes before anything could reject it. The bound is far past any
|
|
11
|
+
* real monetary or measurement scale, and refuses the pathological input as
|
|
12
|
+
* input rather than as an out-of-memory later.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_EXPONENT = 10_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Expand scientific notation into plain decimal text.
|
|
18
|
+
*
|
|
19
|
+
* `1E-10` is what `JSON.stringify` emits for a small number and what many APIs
|
|
20
|
+
* return, so a value that survives as a `number` has to survive as the string
|
|
21
|
+
* carrying it too — otherwise the same amount parses or throws depending on
|
|
22
|
+
* which side of a JSON boundary it arrived from.
|
|
23
|
+
*/
|
|
24
|
+
function expandScientific(input: string): string {
|
|
25
|
+
const matched = /^([+-]?)(\d*)(?:\.(\d*))?[eE]([+-]?\d+)$/.exec(input);
|
|
26
|
+
if (!matched) {
|
|
27
|
+
throw new Error(`Invalid decimal: ${input}`);
|
|
28
|
+
}
|
|
29
|
+
const [, sign, wholeRaw = "", fracRaw = "", exponentRaw = "0"] = matched;
|
|
30
|
+
if (wholeRaw.length === 0 && fracRaw.length === 0) {
|
|
31
|
+
throw new Error(`Invalid decimal: ${input}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const exponent = Number(exponentRaw);
|
|
35
|
+
if (!Number.isInteger(exponent) || Math.abs(exponent) > MAX_EXPONENT) {
|
|
36
|
+
throw new Error(`Invalid decimal: ${input}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const digits = `${wholeRaw}${fracRaw}`;
|
|
40
|
+
const pointIndex = wholeRaw.length + exponent;
|
|
41
|
+
|
|
42
|
+
let expanded: string;
|
|
43
|
+
if (pointIndex <= 0) {
|
|
44
|
+
expanded = `0.${"0".repeat(-pointIndex)}${digits}`;
|
|
45
|
+
} else if (pointIndex >= digits.length) {
|
|
46
|
+
expanded = `${digits}${"0".repeat(pointIndex - digits.length)}`;
|
|
47
|
+
} else {
|
|
48
|
+
expanded = `${digits.slice(0, pointIndex)}.${digits.slice(pointIndex)}`;
|
|
49
|
+
}
|
|
50
|
+
return sign === "-" ? `-${expanded}` : expanded;
|
|
51
|
+
}
|
|
52
|
+
|
|
6
53
|
export function parseDecimal(input: string): ParsedDecimal {
|
|
7
|
-
|
|
54
|
+
let s = input.trim();
|
|
8
55
|
if (!s) {
|
|
9
56
|
throw new Error("Invalid decimal: empty string");
|
|
10
57
|
}
|
|
58
|
+
if (/[eE]/.test(s)) {
|
|
59
|
+
s = expandScientific(s);
|
|
60
|
+
}
|
|
11
61
|
|
|
12
62
|
let sign = 1n;
|
|
13
63
|
let body = s;
|
|
@@ -27,6 +77,9 @@ export function parseDecimal(input: string): ParsedDecimal {
|
|
|
27
77
|
if (!/^\d*$/.test(whole) || !/^\d*$/.test(frac)) {
|
|
28
78
|
throw new Error(`Invalid decimal: ${input}`);
|
|
29
79
|
}
|
|
80
|
+
if (whole.length + frac.length === 0) {
|
|
81
|
+
throw new Error(`Invalid decimal: ${input}`);
|
|
82
|
+
}
|
|
30
83
|
|
|
31
84
|
const digits = `${whole}${frac}` || "0";
|
|
32
85
|
const int = BigInt(digits) * sign;
|
|
@@ -154,11 +207,11 @@ export function powTs(a: string, exp: number, precision: number): string {
|
|
|
154
207
|
let e = exp;
|
|
155
208
|
|
|
156
209
|
while (e > 0) {
|
|
157
|
-
if (e
|
|
210
|
+
if (e % 2 === 1) {
|
|
158
211
|
resultInt *= currentInt;
|
|
159
212
|
resultScale += currentScale;
|
|
160
213
|
}
|
|
161
|
-
e
|
|
214
|
+
e = Math.floor(e / 2);
|
|
162
215
|
if (e > 0) {
|
|
163
216
|
currentInt *= currentInt;
|
|
164
217
|
currentScale *= 2;
|
package/src/native.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* - **Browser**: loads the `.wasm` binary via the wasm-pack JS glue (async init on
|
|
6
6
|
* first module import via top-level await, then sync function calls)
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
8
|
+
* The Decimal facade prefers this engine when available and falls back to the
|
|
9
|
+
* pure TypeScript BigInt implementation when unavailable.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export interface NativeAtom {
|
|
@@ -94,6 +94,13 @@ export function nativeAtom(): NativeAtom {
|
|
|
94
94
|
return native;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export function tryNativeAtom(): NativeAtom | undefined {
|
|
98
|
+
if (overrideNative !== undefined) {
|
|
99
|
+
return overrideNative ?? undefined;
|
|
100
|
+
}
|
|
101
|
+
return native;
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
// Test override (unchanged from before)
|
|
98
105
|
let overrideNative: NativeAtom | null | undefined;
|
|
99
106
|
|