@c9up/atom 0.1.3 → 0.1.5
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/dist/Decimal.d.ts +167 -0
- package/dist/Decimal.d.ts.map +1 -0
- package/dist/Decimal.js +513 -0
- package/dist/Decimal.js.map +1 -0
- package/dist/atlas.d.ts +60 -0
- package/dist/atlas.d.ts.map +1 -0
- package/dist/atlas.js +79 -0
- package/dist/atlas.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +216 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +37 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +187 -0
- package/dist/math.js.map +1 -0
- package/dist/native.d.ts +25 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +80 -0
- package/dist/native.js.map +1 -0
- 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 +1 -1
- package/src/math.ts +3 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export type DecimalInput = string | number | bigint | Decimal;
|
|
2
|
+
export type RoundMode = "trunc" | "floor" | "ceil" | "half-up" | "half-even";
|
|
3
|
+
export interface DecimalScaled {
|
|
4
|
+
value: bigint;
|
|
5
|
+
scale: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DivOptions {
|
|
8
|
+
precision?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PowOptions {
|
|
11
|
+
precision?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SqrtOptions {
|
|
14
|
+
precision?: number;
|
|
15
|
+
mode?: RoundMode;
|
|
16
|
+
}
|
|
17
|
+
export interface BetweenOptions {
|
|
18
|
+
inclusive?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface QuantizeOptions {
|
|
21
|
+
mode?: RoundMode;
|
|
22
|
+
precision?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface MedianOptions {
|
|
25
|
+
precision?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface StddevOptions {
|
|
28
|
+
sample?: boolean;
|
|
29
|
+
precision?: number;
|
|
30
|
+
mode?: RoundMode;
|
|
31
|
+
}
|
|
32
|
+
export interface ToMinorUnitsOptions {
|
|
33
|
+
exact?: boolean;
|
|
34
|
+
mode?: RoundMode;
|
|
35
|
+
}
|
|
36
|
+
export declare class Decimal {
|
|
37
|
+
#private;
|
|
38
|
+
constructor(value: DecimalInput);
|
|
39
|
+
static from(value: DecimalInput): Decimal;
|
|
40
|
+
static zero(): Decimal;
|
|
41
|
+
static one(): Decimal;
|
|
42
|
+
static fromMinorUnits(value: string | number | bigint, scale: number): Decimal;
|
|
43
|
+
static parseLocale(value: string, localesOrRosetta?: Intl.LocalesArgument | RosettaLike): Decimal;
|
|
44
|
+
plus(other: DecimalInput): Decimal;
|
|
45
|
+
minus(other: DecimalInput): Decimal;
|
|
46
|
+
times(other: DecimalInput): Decimal;
|
|
47
|
+
div(other: DecimalInput, options?: DivOptions): Decimal;
|
|
48
|
+
mod(other: DecimalInput): Decimal;
|
|
49
|
+
pow(exp: number, options?: PowOptions): Decimal;
|
|
50
|
+
sqrt(options?: SqrtOptions): Decimal;
|
|
51
|
+
cmp(other: DecimalInput): -1 | 0 | 1;
|
|
52
|
+
eq(other: DecimalInput): boolean;
|
|
53
|
+
lt(other: DecimalInput): boolean;
|
|
54
|
+
lte(other: DecimalInput): boolean;
|
|
55
|
+
gt(other: DecimalInput): boolean;
|
|
56
|
+
gte(other: DecimalInput): boolean;
|
|
57
|
+
min(other: DecimalInput): Decimal;
|
|
58
|
+
max(other: DecimalInput): Decimal;
|
|
59
|
+
clamp(min: DecimalInput, max: DecimalInput): Decimal;
|
|
60
|
+
between(min: DecimalInput, max: DecimalInput, options?: BetweenOptions): boolean;
|
|
61
|
+
abs(): Decimal;
|
|
62
|
+
neg(): Decimal;
|
|
63
|
+
isZero(): boolean;
|
|
64
|
+
isPositive(): boolean;
|
|
65
|
+
isNegative(): boolean;
|
|
66
|
+
isInteger(): boolean;
|
|
67
|
+
trunc(scale?: number): Decimal;
|
|
68
|
+
floor(scale?: number): Decimal;
|
|
69
|
+
ceil(scale?: number): Decimal;
|
|
70
|
+
round(scale?: number, mode?: RoundMode): Decimal;
|
|
71
|
+
/**
|
|
72
|
+
* Snap the value to the nearest multiple of `step`. Useful for rounding
|
|
73
|
+
* prices to the nearest cent (`.quantize('0.01')`), the nearest 5-cent
|
|
74
|
+
* increment, or any custom unit. The default rounding mode is `'half-up'`;
|
|
75
|
+
* pass `{ mode: 'half-even' }` for banker's rounding.
|
|
76
|
+
*
|
|
77
|
+
* new Decimal('1.234').quantize('0.01') // → Decimal('1.23')
|
|
78
|
+
* new Decimal('1.025').quantize('0.05') // → Decimal('1.05')
|
|
79
|
+
*/
|
|
80
|
+
quantize(step: DecimalInput, options?: QuantizeOptions): Decimal;
|
|
81
|
+
toScale(scale?: number, mode?: RoundMode): Decimal;
|
|
82
|
+
toFixed(scale: number, mode?: RoundMode): string;
|
|
83
|
+
/**
|
|
84
|
+
* Convert to a minor-unit `bigint` representation — typically used for
|
|
85
|
+
* persisting prices to a database as integer cents (`scale: 2`).
|
|
86
|
+
*
|
|
87
|
+
* - `exact: true` (default) throws if the conversion would lose precision
|
|
88
|
+
* (e.g. `'1.234'.toMinorUnits(2)` errors because `0.004` can't be
|
|
89
|
+
* represented at scale 2 without rounding).
|
|
90
|
+
* - `exact: false` rounds using the requested `mode` (default `'trunc'`).
|
|
91
|
+
*
|
|
92
|
+
* new Decimal('19.99').toMinorUnits(2) // → 1999n
|
|
93
|
+
* new Decimal('1.234').toMinorUnits(2, { exact: false }) // → 123n (truncated)
|
|
94
|
+
*/
|
|
95
|
+
toMinorUnits(scale: number, options?: ToMinorUnitsOptions): bigint;
|
|
96
|
+
/**
|
|
97
|
+
* Compute `this * rate / 100` — the percentage portion of the value.
|
|
98
|
+
*
|
|
99
|
+
* new Decimal('200').percent('15') // → Decimal('30')
|
|
100
|
+
*/
|
|
101
|
+
percent(rate: DecimalInput, options?: DivOptions): Decimal;
|
|
102
|
+
/**
|
|
103
|
+
* Add a percentage to the value: `this + (this * rate / 100)`. Handy for
|
|
104
|
+
* tax/markup calculations.
|
|
105
|
+
*
|
|
106
|
+
* new Decimal('100').applyPercent('20') // → Decimal('120')
|
|
107
|
+
*/
|
|
108
|
+
applyPercent(rate: DecimalInput, options?: DivOptions): Decimal;
|
|
109
|
+
/**
|
|
110
|
+
* Express this value as a percentage of `total`: `this / total * 100`.
|
|
111
|
+
*
|
|
112
|
+
* new Decimal('30').percentageOf('200') // → Decimal('15')
|
|
113
|
+
*/
|
|
114
|
+
percentageOf(total: DecimalInput, options?: DivOptions): Decimal;
|
|
115
|
+
/**
|
|
116
|
+
* Distribute the value across N buckets according to integer ratios with
|
|
117
|
+
* **zero rounding loss**: the sum of the returned shares equals the
|
|
118
|
+
* original value exactly. Used for splitting money: `'10.00'.allocate([1, 1, 1])`
|
|
119
|
+
* returns `['3.34', '3.33', '3.33']`, not three `'3.33'` (which would
|
|
120
|
+
* lose a cent).
|
|
121
|
+
*
|
|
122
|
+
* The remainder pennies are distributed largest-remainder-first, with
|
|
123
|
+
* stable input order as the tiebreaker.
|
|
124
|
+
*/
|
|
125
|
+
allocate(ratios: Array<string | number | bigint>): Decimal[];
|
|
126
|
+
toParts(): DecimalScaled;
|
|
127
|
+
toString(): string;
|
|
128
|
+
toJSON(): string;
|
|
129
|
+
/**
|
|
130
|
+
* Convert to a JavaScript `number` — **lossy** for values with more than
|
|
131
|
+
* 15-16 significant digits. Use `toString()` / `toJSON()` for exact output.
|
|
132
|
+
* Provided for interop with APIs that expect a primitive number.
|
|
133
|
+
*/
|
|
134
|
+
toNumber(): number;
|
|
135
|
+
/**
|
|
136
|
+
* Format the value as a localized string via `Intl.NumberFormat`.
|
|
137
|
+
*
|
|
138
|
+
* Unlike `toNumber()`, this path is **exact** — we route through the
|
|
139
|
+
* string-accepting overload of `Intl.NumberFormat.format` (ECMA-402
|
|
140
|
+
* stage-4, supported by every V8 since Node 20). A `Decimal` of
|
|
141
|
+
* `'9999999999999999.99'` formats correctly instead of rounding to
|
|
142
|
+
* `10000000000000000`, which is the whole reason Atom exists.
|
|
143
|
+
*
|
|
144
|
+
* For pure integers without a decimal point, we hand the value to
|
|
145
|
+
* `format` as a BigInt (which has been in the type system since ES2020).
|
|
146
|
+
* For fractional values, we use the runtime's string support via a
|
|
147
|
+
* typed extension interface — no `any` escape hatch.
|
|
148
|
+
*/
|
|
149
|
+
toLocale(localesOrRosetta?: Intl.LocalesArgument | RosettaLike, options?: Intl.NumberFormatOptions): string;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Structural type matching `@c9up/rosetta`'s `Rosetta` and
|
|
153
|
+
* `RosettaLocale` surfaces. Atom never imports the Rosetta
|
|
154
|
+
* package directly — duck-typing keeps the integration optional
|
|
155
|
+
* and avoids a hard cross-package dependency.
|
|
156
|
+
*/
|
|
157
|
+
export interface RosettaNumberFormatData {
|
|
158
|
+
decimal: string;
|
|
159
|
+
group: string;
|
|
160
|
+
minus: string;
|
|
161
|
+
plusSign: string;
|
|
162
|
+
}
|
|
163
|
+
export interface RosettaLike {
|
|
164
|
+
getNumberFormatData(): RosettaNumberFormatData;
|
|
165
|
+
formatNumberString(value: string, options?: Intl.NumberFormatOptions): string;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=Decimal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Decimal.d.ts","sourceRoot":"","sources":["../src/Decimal.ts"],"names":[],"mappings":"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"}
|
package/dist/Decimal.js
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { formatDecimal, parseDecimal, pow10BigInt } from "./math.js";
|
|
2
|
+
import { nativeAtom } from "./native.js";
|
|
3
|
+
export class Decimal {
|
|
4
|
+
#value;
|
|
5
|
+
constructor(value) {
|
|
6
|
+
this.#value = normalizeInput(value);
|
|
7
|
+
}
|
|
8
|
+
static from(value) {
|
|
9
|
+
return new Decimal(value);
|
|
10
|
+
}
|
|
11
|
+
static zero() {
|
|
12
|
+
return new Decimal("0");
|
|
13
|
+
}
|
|
14
|
+
static one() {
|
|
15
|
+
return new Decimal("1");
|
|
16
|
+
}
|
|
17
|
+
static fromMinorUnits(value, scale) {
|
|
18
|
+
assertScale(scale);
|
|
19
|
+
const minor = parseIntegerInput(value);
|
|
20
|
+
return fromIntScale(minor, scale);
|
|
21
|
+
}
|
|
22
|
+
static parseLocale(value, localesOrRosetta) {
|
|
23
|
+
if (isRosettaLike(localesOrRosetta)) {
|
|
24
|
+
return new Decimal(normalizeViaRosetta(value, localesOrRosetta));
|
|
25
|
+
}
|
|
26
|
+
return new Decimal(normalizeLocaleNumber(value, localesOrRosetta));
|
|
27
|
+
}
|
|
28
|
+
plus(other) {
|
|
29
|
+
const b = normalizeInput(other);
|
|
30
|
+
const result = nativeAtom().add(this.#value, b);
|
|
31
|
+
return new Decimal(result);
|
|
32
|
+
}
|
|
33
|
+
minus(other) {
|
|
34
|
+
const b = normalizeInput(other);
|
|
35
|
+
const result = nativeAtom().sub(this.#value, b);
|
|
36
|
+
return new Decimal(result);
|
|
37
|
+
}
|
|
38
|
+
times(other) {
|
|
39
|
+
const b = normalizeInput(other);
|
|
40
|
+
const result = nativeAtom().mul(this.#value, b);
|
|
41
|
+
return new Decimal(result);
|
|
42
|
+
}
|
|
43
|
+
div(other, options = {}) {
|
|
44
|
+
const b = normalizeInput(other);
|
|
45
|
+
const precision = options.precision ?? 18;
|
|
46
|
+
assertScale(precision);
|
|
47
|
+
const result = nativeAtom().div(this.#value, b, precision);
|
|
48
|
+
return new Decimal(result);
|
|
49
|
+
}
|
|
50
|
+
mod(other) {
|
|
51
|
+
const b = normalizeInput(other);
|
|
52
|
+
const result = nativeAtom().rem(this.#value, b);
|
|
53
|
+
return new Decimal(result);
|
|
54
|
+
}
|
|
55
|
+
pow(exp, options = {}) {
|
|
56
|
+
if (!Number.isInteger(exp)) {
|
|
57
|
+
throw new Error(`Invalid exponent: ${exp}`);
|
|
58
|
+
}
|
|
59
|
+
const precision = options.precision ?? 18;
|
|
60
|
+
assertScale(precision);
|
|
61
|
+
const result = nativeAtom().pow(this.#value, exp, precision);
|
|
62
|
+
return new Decimal(result);
|
|
63
|
+
}
|
|
64
|
+
sqrt(options = {}) {
|
|
65
|
+
const precision = options.precision ?? 18;
|
|
66
|
+
const mode = options.mode ?? "trunc";
|
|
67
|
+
assertScale(precision);
|
|
68
|
+
if (mode === "trunc") {
|
|
69
|
+
const result = nativeAtom().sqrt(this.#value, precision);
|
|
70
|
+
return new Decimal(result);
|
|
71
|
+
}
|
|
72
|
+
const withExtra = nativeAtom().sqrt(this.#value, precision + 1);
|
|
73
|
+
const parsed = parseDecimal(withExtra);
|
|
74
|
+
const rounded = roundIntScale(parsed.int, precision + 1, precision, mode);
|
|
75
|
+
return fromIntScale(rounded, precision);
|
|
76
|
+
}
|
|
77
|
+
cmp(other) {
|
|
78
|
+
const b = normalizeInput(other);
|
|
79
|
+
const result = nativeAtom().cmp(this.#value, b);
|
|
80
|
+
if (result < 0)
|
|
81
|
+
return -1;
|
|
82
|
+
if (result > 0)
|
|
83
|
+
return 1;
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
eq(other) {
|
|
87
|
+
return this.cmp(other) === 0;
|
|
88
|
+
}
|
|
89
|
+
lt(other) {
|
|
90
|
+
return this.cmp(other) < 0;
|
|
91
|
+
}
|
|
92
|
+
lte(other) {
|
|
93
|
+
return this.cmp(other) <= 0;
|
|
94
|
+
}
|
|
95
|
+
gt(other) {
|
|
96
|
+
return this.cmp(other) > 0;
|
|
97
|
+
}
|
|
98
|
+
gte(other) {
|
|
99
|
+
return this.cmp(other) >= 0;
|
|
100
|
+
}
|
|
101
|
+
min(other) {
|
|
102
|
+
return this.lte(other) ? this : new Decimal(other);
|
|
103
|
+
}
|
|
104
|
+
max(other) {
|
|
105
|
+
return this.gte(other) ? this : new Decimal(other);
|
|
106
|
+
}
|
|
107
|
+
clamp(min, max) {
|
|
108
|
+
const minValue = new Decimal(min);
|
|
109
|
+
const maxValue = new Decimal(max);
|
|
110
|
+
if (minValue.gt(maxValue)) {
|
|
111
|
+
throw new Error("Invalid clamp range: min is greater than max");
|
|
112
|
+
}
|
|
113
|
+
if (this.lt(minValue))
|
|
114
|
+
return minValue;
|
|
115
|
+
if (this.gt(maxValue))
|
|
116
|
+
return maxValue;
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
between(min, max, options = {}) {
|
|
120
|
+
const { inclusive = true } = options;
|
|
121
|
+
const minValue = new Decimal(min);
|
|
122
|
+
const maxValue = new Decimal(max);
|
|
123
|
+
if (minValue.gt(maxValue)) {
|
|
124
|
+
throw new Error("Invalid between range: min is greater than max");
|
|
125
|
+
}
|
|
126
|
+
if (inclusive) {
|
|
127
|
+
return this.gte(minValue) && this.lte(maxValue);
|
|
128
|
+
}
|
|
129
|
+
return this.gt(minValue) && this.lt(maxValue);
|
|
130
|
+
}
|
|
131
|
+
abs() {
|
|
132
|
+
return this.isNegative() ? this.neg() : this;
|
|
133
|
+
}
|
|
134
|
+
neg() {
|
|
135
|
+
return this.isZero()
|
|
136
|
+
? this
|
|
137
|
+
: new Decimal(this.#value.startsWith("-")
|
|
138
|
+
? this.#value.slice(1)
|
|
139
|
+
: `-${this.#value}`);
|
|
140
|
+
}
|
|
141
|
+
isZero() {
|
|
142
|
+
return this.#value === "0";
|
|
143
|
+
}
|
|
144
|
+
isPositive() {
|
|
145
|
+
return this.#value !== "0" && !this.#value.startsWith("-");
|
|
146
|
+
}
|
|
147
|
+
isNegative() {
|
|
148
|
+
return this.#value.startsWith("-");
|
|
149
|
+
}
|
|
150
|
+
isInteger() {
|
|
151
|
+
return parseDecimal(this.#value).scale === 0;
|
|
152
|
+
}
|
|
153
|
+
trunc(scale = 0) {
|
|
154
|
+
return this.toScale(scale, "trunc");
|
|
155
|
+
}
|
|
156
|
+
floor(scale = 0) {
|
|
157
|
+
return this.toScale(scale, "floor");
|
|
158
|
+
}
|
|
159
|
+
ceil(scale = 0) {
|
|
160
|
+
return this.toScale(scale, "ceil");
|
|
161
|
+
}
|
|
162
|
+
round(scale = 0, mode = "half-up") {
|
|
163
|
+
return this.toScale(scale, mode);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Snap the value to the nearest multiple of `step`. Useful for rounding
|
|
167
|
+
* prices to the nearest cent (`.quantize('0.01')`), the nearest 5-cent
|
|
168
|
+
* increment, or any custom unit. The default rounding mode is `'half-up'`;
|
|
169
|
+
* pass `{ mode: 'half-even' }` for banker's rounding.
|
|
170
|
+
*
|
|
171
|
+
* new Decimal('1.234').quantize('0.01') // → Decimal('1.23')
|
|
172
|
+
* new Decimal('1.025').quantize('0.05') // → Decimal('1.05')
|
|
173
|
+
*/
|
|
174
|
+
quantize(step, options = {}) {
|
|
175
|
+
const { mode = "half-up" } = options;
|
|
176
|
+
const stepValue = new Decimal(step);
|
|
177
|
+
if (!stepValue.gt(0)) {
|
|
178
|
+
throw new Error("Quantize step must be greater than zero");
|
|
179
|
+
}
|
|
180
|
+
const thisScale = this.toParts().scale;
|
|
181
|
+
const stepScale = stepValue.toParts().scale;
|
|
182
|
+
const precision = options.precision ?? Math.max(18, thisScale + stepScale + 6);
|
|
183
|
+
const units = this.div(stepValue, { precision }).round(0, mode);
|
|
184
|
+
return units.times(stepValue);
|
|
185
|
+
}
|
|
186
|
+
toScale(scale = 0, mode = "trunc") {
|
|
187
|
+
assertScale(scale);
|
|
188
|
+
const parsed = parseDecimal(this.#value);
|
|
189
|
+
return fromIntScale(roundIntScale(parsed.int, parsed.scale, scale, mode), scale);
|
|
190
|
+
}
|
|
191
|
+
toFixed(scale, mode = "trunc") {
|
|
192
|
+
assertScale(scale);
|
|
193
|
+
const parsed = parseDecimal(this.#value);
|
|
194
|
+
const roundedInt = roundIntScale(parsed.int, parsed.scale, scale, mode);
|
|
195
|
+
const negative = roundedInt < 0n;
|
|
196
|
+
const raw = (negative ? -roundedInt : roundedInt)
|
|
197
|
+
.toString()
|
|
198
|
+
.padStart(scale + 1, "0");
|
|
199
|
+
if (scale === 0)
|
|
200
|
+
return negative ? `-${raw}` : raw;
|
|
201
|
+
const whole = raw.slice(0, raw.length - scale);
|
|
202
|
+
const frac = raw.slice(raw.length - scale);
|
|
203
|
+
const out = `${whole}.${frac}`;
|
|
204
|
+
return negative ? `-${out}` : out;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Convert to a minor-unit `bigint` representation — typically used for
|
|
208
|
+
* persisting prices to a database as integer cents (`scale: 2`).
|
|
209
|
+
*
|
|
210
|
+
* - `exact: true` (default) throws if the conversion would lose precision
|
|
211
|
+
* (e.g. `'1.234'.toMinorUnits(2)` errors because `0.004` can't be
|
|
212
|
+
* represented at scale 2 without rounding).
|
|
213
|
+
* - `exact: false` rounds using the requested `mode` (default `'trunc'`).
|
|
214
|
+
*
|
|
215
|
+
* new Decimal('19.99').toMinorUnits(2) // → 1999n
|
|
216
|
+
* new Decimal('1.234').toMinorUnits(2, { exact: false }) // → 123n (truncated)
|
|
217
|
+
*/
|
|
218
|
+
toMinorUnits(scale, options = {}) {
|
|
219
|
+
assertScale(scale);
|
|
220
|
+
const { exact = true, mode = "trunc" } = options;
|
|
221
|
+
const parsed = parseDecimal(this.#value);
|
|
222
|
+
if (parsed.scale === scale)
|
|
223
|
+
return parsed.int;
|
|
224
|
+
if (parsed.scale < scale) {
|
|
225
|
+
return parsed.int * pow10BigInt(scale - parsed.scale);
|
|
226
|
+
}
|
|
227
|
+
const drop = parsed.scale - scale;
|
|
228
|
+
const factor = pow10BigInt(drop);
|
|
229
|
+
const remainder = parsed.int % factor;
|
|
230
|
+
if (exact && remainder !== 0n) {
|
|
231
|
+
throw new Error(`Cannot convert ${this.#value} to minor units at scale ${scale} without precision loss`);
|
|
232
|
+
}
|
|
233
|
+
return roundIntScale(parsed.int, parsed.scale, scale, mode);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Compute `this * rate / 100` — the percentage portion of the value.
|
|
237
|
+
*
|
|
238
|
+
* new Decimal('200').percent('15') // → Decimal('30')
|
|
239
|
+
*/
|
|
240
|
+
percent(rate, options = {}) {
|
|
241
|
+
return this.times(rate).div("100", options);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Add a percentage to the value: `this + (this * rate / 100)`. Handy for
|
|
245
|
+
* tax/markup calculations.
|
|
246
|
+
*
|
|
247
|
+
* new Decimal('100').applyPercent('20') // → Decimal('120')
|
|
248
|
+
*/
|
|
249
|
+
applyPercent(rate, options = {}) {
|
|
250
|
+
return this.plus(this.percent(rate, options));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Express this value as a percentage of `total`: `this / total * 100`.
|
|
254
|
+
*
|
|
255
|
+
* new Decimal('30').percentageOf('200') // → Decimal('15')
|
|
256
|
+
*/
|
|
257
|
+
percentageOf(total, options = {}) {
|
|
258
|
+
return this.div(total, options).times("100");
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Distribute the value across N buckets according to integer ratios with
|
|
262
|
+
* **zero rounding loss**: the sum of the returned shares equals the
|
|
263
|
+
* original value exactly. Used for splitting money: `'10.00'.allocate([1, 1, 1])`
|
|
264
|
+
* returns `['3.34', '3.33', '3.33']`, not three `'3.33'` (which would
|
|
265
|
+
* lose a cent).
|
|
266
|
+
*
|
|
267
|
+
* The remainder pennies are distributed largest-remainder-first, with
|
|
268
|
+
* stable input order as the tiebreaker.
|
|
269
|
+
*/
|
|
270
|
+
allocate(ratios) {
|
|
271
|
+
if (ratios.length === 0) {
|
|
272
|
+
throw new Error("Allocate requires at least one ratio");
|
|
273
|
+
}
|
|
274
|
+
const normalized = ratios.map((ratio) => {
|
|
275
|
+
const value = parseIntegerInput(ratio);
|
|
276
|
+
if (value < 0n) {
|
|
277
|
+
throw new Error(`Allocate ratios must be >= 0, got ${ratio}`);
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
280
|
+
});
|
|
281
|
+
const ratioTotal = normalized.reduce((acc, value) => acc + value, 0n);
|
|
282
|
+
if (ratioTotal <= 0n) {
|
|
283
|
+
throw new Error("Allocate requires at least one positive ratio");
|
|
284
|
+
}
|
|
285
|
+
const parsed = parseDecimal(this.#value);
|
|
286
|
+
const sign = parsed.int < 0n ? -1n : 1n;
|
|
287
|
+
const total = parsed.int < 0n ? -parsed.int : parsed.int;
|
|
288
|
+
const baseShares = [];
|
|
289
|
+
const remainders = [];
|
|
290
|
+
let consumed = 0n;
|
|
291
|
+
for (let index = 0; index < normalized.length; index++) {
|
|
292
|
+
const ratio = normalized[index];
|
|
293
|
+
const weighted = total * ratio;
|
|
294
|
+
const share = weighted / ratioTotal;
|
|
295
|
+
const remainder = weighted % ratioTotal;
|
|
296
|
+
baseShares.push(share);
|
|
297
|
+
remainders.push({ index, remainder });
|
|
298
|
+
consumed += share;
|
|
299
|
+
}
|
|
300
|
+
let left = total - consumed;
|
|
301
|
+
remainders.sort((a, b) => {
|
|
302
|
+
if (a.remainder > b.remainder)
|
|
303
|
+
return -1;
|
|
304
|
+
if (a.remainder < b.remainder)
|
|
305
|
+
return 1;
|
|
306
|
+
return a.index - b.index;
|
|
307
|
+
});
|
|
308
|
+
let pointer = 0;
|
|
309
|
+
while (left > 0n) {
|
|
310
|
+
baseShares[remainders[pointer].index] += 1n;
|
|
311
|
+
left -= 1n;
|
|
312
|
+
pointer++;
|
|
313
|
+
if (pointer >= remainders.length)
|
|
314
|
+
pointer = 0;
|
|
315
|
+
}
|
|
316
|
+
return baseShares.map((share) => fromIntScale(share * sign, parsed.scale));
|
|
317
|
+
}
|
|
318
|
+
toParts() {
|
|
319
|
+
const parsed = parseDecimal(this.#value);
|
|
320
|
+
return { value: parsed.int, scale: parsed.scale };
|
|
321
|
+
}
|
|
322
|
+
toString() {
|
|
323
|
+
return this.#value;
|
|
324
|
+
}
|
|
325
|
+
toJSON() {
|
|
326
|
+
return this.#value;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Convert to a JavaScript `number` — **lossy** for values with more than
|
|
330
|
+
* 15-16 significant digits. Use `toString()` / `toJSON()` for exact output.
|
|
331
|
+
* Provided for interop with APIs that expect a primitive number.
|
|
332
|
+
*/
|
|
333
|
+
toNumber() {
|
|
334
|
+
return Number(this.#value);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Format the value as a localized string via `Intl.NumberFormat`.
|
|
338
|
+
*
|
|
339
|
+
* Unlike `toNumber()`, this path is **exact** — we route through the
|
|
340
|
+
* string-accepting overload of `Intl.NumberFormat.format` (ECMA-402
|
|
341
|
+
* stage-4, supported by every V8 since Node 20). A `Decimal` of
|
|
342
|
+
* `'9999999999999999.99'` formats correctly instead of rounding to
|
|
343
|
+
* `10000000000000000`, which is the whole reason Atom exists.
|
|
344
|
+
*
|
|
345
|
+
* For pure integers without a decimal point, we hand the value to
|
|
346
|
+
* `format` as a BigInt (which has been in the type system since ES2020).
|
|
347
|
+
* For fractional values, we use the runtime's string support via a
|
|
348
|
+
* typed extension interface — no `any` escape hatch.
|
|
349
|
+
*/
|
|
350
|
+
toLocale(localesOrRosetta, options) {
|
|
351
|
+
if (isRosettaLike(localesOrRosetta)) {
|
|
352
|
+
return localesOrRosetta.formatNumberString(this.#value, options);
|
|
353
|
+
}
|
|
354
|
+
const formatter = new Intl.NumberFormat(localesOrRosetta, options);
|
|
355
|
+
if (this.isInteger()) {
|
|
356
|
+
return formatter.format(BigInt(this.#value));
|
|
357
|
+
}
|
|
358
|
+
return formatter.format(this.#value);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function normalizeInput(value) {
|
|
362
|
+
if (value instanceof Decimal) {
|
|
363
|
+
return value.toString();
|
|
364
|
+
}
|
|
365
|
+
if (typeof value === "bigint") {
|
|
366
|
+
return value.toString();
|
|
367
|
+
}
|
|
368
|
+
if (typeof value === "number") {
|
|
369
|
+
if (!Number.isFinite(value)) {
|
|
370
|
+
throw new Error(`Invalid decimal: ${value}`);
|
|
371
|
+
}
|
|
372
|
+
return normalizeDecimalString(String(value));
|
|
373
|
+
}
|
|
374
|
+
return normalizeDecimalString(value);
|
|
375
|
+
}
|
|
376
|
+
function normalizeDecimalString(input) {
|
|
377
|
+
const parsed = parseDecimal(input);
|
|
378
|
+
return formatDecimal(parsed.int, parsed.scale);
|
|
379
|
+
}
|
|
380
|
+
function parseIntegerInput(value) {
|
|
381
|
+
if (typeof value === "bigint")
|
|
382
|
+
return value;
|
|
383
|
+
if (typeof value === "number") {
|
|
384
|
+
if (!Number.isInteger(value)) {
|
|
385
|
+
throw new Error(`Invalid integer: ${value}`);
|
|
386
|
+
}
|
|
387
|
+
return BigInt(value);
|
|
388
|
+
}
|
|
389
|
+
const s = value.trim();
|
|
390
|
+
if (!/^[+-]?\d+$/.test(s)) {
|
|
391
|
+
throw new Error(`Invalid integer: ${value}`);
|
|
392
|
+
}
|
|
393
|
+
return BigInt(s);
|
|
394
|
+
}
|
|
395
|
+
function fromIntScale(int, scale) {
|
|
396
|
+
return new Decimal(formatDecimal(int, scale));
|
|
397
|
+
}
|
|
398
|
+
function assertScale(scale) {
|
|
399
|
+
if (!Number.isInteger(scale) || scale < 0) {
|
|
400
|
+
throw new Error(`Invalid scale: ${scale}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function roundIntScale(int, sourceScale, targetScale, mode) {
|
|
404
|
+
if (targetScale >= sourceScale) {
|
|
405
|
+
return int * pow10BigInt(targetScale - sourceScale);
|
|
406
|
+
}
|
|
407
|
+
const drop = sourceScale - targetScale;
|
|
408
|
+
const factor = pow10BigInt(drop);
|
|
409
|
+
const q = int / factor;
|
|
410
|
+
const r = int % factor;
|
|
411
|
+
if (r === 0n)
|
|
412
|
+
return q;
|
|
413
|
+
const absR = r < 0n ? -r : r;
|
|
414
|
+
const sign = int < 0n ? -1n : 1n;
|
|
415
|
+
switch (mode) {
|
|
416
|
+
case "trunc":
|
|
417
|
+
return q;
|
|
418
|
+
case "floor":
|
|
419
|
+
return int < 0n ? q - 1n : q;
|
|
420
|
+
case "ceil":
|
|
421
|
+
return int > 0n ? q + 1n : q;
|
|
422
|
+
case "half-up":
|
|
423
|
+
return absR * 2n >= factor ? q + sign : q;
|
|
424
|
+
case "half-even": {
|
|
425
|
+
const twice = absR * 2n;
|
|
426
|
+
if (twice < factor)
|
|
427
|
+
return q;
|
|
428
|
+
if (twice > factor)
|
|
429
|
+
return q + sign;
|
|
430
|
+
const isEven = (q < 0n ? -q : q) % 2n === 0n;
|
|
431
|
+
return isEven ? q : q + sign;
|
|
432
|
+
}
|
|
433
|
+
default:
|
|
434
|
+
// Defensive guard — the TypeScript type system already restricts `mode`
|
|
435
|
+
// to the `RoundMode` union, so this branch is unreachable under normal
|
|
436
|
+
// usage. We throw instead of silently returning the truncated result
|
|
437
|
+
// because a silent fallback would hide a bug: an upstream cast past the
|
|
438
|
+
// type system (`as RoundMode`) would lose precision without any signal.
|
|
439
|
+
throw new Error(`Unknown rounding mode: ${String(mode)}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function isRosettaLike(arg) {
|
|
443
|
+
return (typeof arg === "object" &&
|
|
444
|
+
arg !== null &&
|
|
445
|
+
"getNumberFormatData" in arg &&
|
|
446
|
+
typeof arg.getNumberFormatData === "function" &&
|
|
447
|
+
"formatNumberString" in arg &&
|
|
448
|
+
typeof arg.formatNumberString === "function");
|
|
449
|
+
}
|
|
450
|
+
function normalizeViaRosetta(input, rosetta) {
|
|
451
|
+
const trimmed = input.trim();
|
|
452
|
+
if (!trimmed) {
|
|
453
|
+
throw new Error("Invalid localized decimal: empty string");
|
|
454
|
+
}
|
|
455
|
+
const raw = rosetta.getNumberFormatData();
|
|
456
|
+
// Guard: a malformed `RosettaLike` returning empty separators
|
|
457
|
+
// would produce regexes matching every position. Fall back to
|
|
458
|
+
// ASCII defaults rather than corrupting the input.
|
|
459
|
+
const data = {
|
|
460
|
+
decimal: raw.decimal || ".",
|
|
461
|
+
group: raw.group || ",",
|
|
462
|
+
minus: raw.minus || "-",
|
|
463
|
+
plusSign: raw.plusSign || "+",
|
|
464
|
+
};
|
|
465
|
+
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
|
+
if (data.minus !== "-") {
|
|
469
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(data.minus), "g"), "-");
|
|
470
|
+
}
|
|
471
|
+
normalized = normalized.replace(/[−﹣-]/g, "-");
|
|
472
|
+
// Substitute the locale's plus sign (e.g., U+FF0B `+`,
|
|
473
|
+
// U+FB29 `﬩`) to ASCII so the strict validator accepts it.
|
|
474
|
+
if (data.plusSign !== "+") {
|
|
475
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(data.plusSign), "g"), "+");
|
|
476
|
+
}
|
|
477
|
+
if (/^\(.*\)$/.test(normalized)) {
|
|
478
|
+
normalized = `-${normalized.slice(1, -1)}`;
|
|
479
|
+
}
|
|
480
|
+
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
481
|
+
throw new Error(`Invalid localized decimal: ${input}`);
|
|
482
|
+
}
|
|
483
|
+
return normalized;
|
|
484
|
+
}
|
|
485
|
+
function normalizeLocaleNumber(input, locales) {
|
|
486
|
+
const trimmed = input.trim();
|
|
487
|
+
if (!trimmed) {
|
|
488
|
+
throw new Error("Invalid localized decimal: empty string");
|
|
489
|
+
}
|
|
490
|
+
const formatter = new Intl.NumberFormat(locales);
|
|
491
|
+
const parts = formatter.formatToParts(-12345.6);
|
|
492
|
+
const group = parts.find((part) => part.type === "group")?.value ?? ",";
|
|
493
|
+
const decimal = parts.find((part) => part.type === "decimal")?.value ?? ".";
|
|
494
|
+
const minus = parts.find((part) => part.type === "minusSign")?.value ?? "-";
|
|
495
|
+
let normalized = trimmed.replace(/\s|\u00A0|\u202F/g, "");
|
|
496
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(group), "g"), "");
|
|
497
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(decimal), "g"), ".");
|
|
498
|
+
if (minus !== "-") {
|
|
499
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(minus), "g"), "-");
|
|
500
|
+
}
|
|
501
|
+
normalized = normalized.replace(/[−﹣-]/g, "-");
|
|
502
|
+
if (/^\(.*\)$/.test(normalized)) {
|
|
503
|
+
normalized = `-${normalized.slice(1, -1)}`;
|
|
504
|
+
}
|
|
505
|
+
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
506
|
+
throw new Error(`Invalid localized decimal: ${input}`);
|
|
507
|
+
}
|
|
508
|
+
return normalized;
|
|
509
|
+
}
|
|
510
|
+
function escapeRegExp(value) {
|
|
511
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
512
|
+
}
|
|
513
|
+
//# sourceMappingURL=Decimal.js.map
|