@onrail-xyz/amount 1.0.0
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/LICENSE +201 -0
- package/README.md +617 -0
- package/dist/aggregating.d.ts +31 -0
- package/dist/aggregating.d.ts.map +1 -0
- package/dist/aggregating.js +31 -0
- package/dist/allocating.d.ts +9 -0
- package/dist/allocating.d.ts.map +1 -0
- package/dist/allocating.js +31 -0
- package/dist/amount.d.ts +68 -0
- package/dist/amount.d.ts.map +1 -0
- package/dist/amount.js +184 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +226 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/jsonCodecs.d.ts +9 -0
- package/dist/jsonCodecs.d.ts.map +1 -0
- package/dist/jsonCodecs.js +46 -0
- package/dist/kind.d.ts +126 -0
- package/dist/kind.d.ts.map +1 -0
- package/dist/kind.js +118 -0
- package/dist/rate.d.ts +58 -0
- package/dist/rate.d.ts.map +1 -0
- package/dist/rate.js +178 -0
- package/dist/rational.d.ts +49 -0
- package/dist/rational.d.ts.map +1 -0
- package/dist/rational.js +378 -0
- package/dist/segmenting.d.ts +15 -0
- package/dist/segmenting.d.ts.map +1 -0
- package/dist/segmenting.js +97 -0
- package/dist/unitSpecs.d.ts +47 -0
- package/dist/unitSpecs.d.ts.map +1 -0
- package/dist/unitSpecs.js +8 -0
- package/package.json +56 -0
- package/src/aggregating.ts +160 -0
- package/src/allocating.ts +70 -0
- package/src/amount.ts +291 -0
- package/src/format.ts +330 -0
- package/src/index.ts +8 -0
- package/src/jsonCodecs.ts +63 -0
- package/src/kind.ts +442 -0
- package/src/rate.ts +316 -0
- package/src/rational.ts +471 -0
- package/src/segmenting.ts +154 -0
- package/src/unitSpecs.ts +37 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { RoArray, RoTuple, HeadTail } from "@onrail-xyz/utils";
|
|
2
|
+
import { type Rationalish, Rational } from "./rational.js";
|
|
3
|
+
import type { Kind } from "./kind.js";
|
|
4
|
+
import { type Amount, isAmount } from "./amount.js";
|
|
5
|
+
import { type Rate, _Rate, isRate } from "./rate.js";
|
|
6
|
+
|
|
7
|
+
//An aggregate of amounts is an amount of the kind every operand has, or a kind mismatch at
|
|
8
|
+
// runtime; the intersection says so exactly. Two distinct kinds intersect to never (their names
|
|
9
|
+
// are disjoint literals), and an Amount of never is never: the type of a call that cannot return.
|
|
10
|
+
//only a fixed tuple's positions witness a kind: a plain array may be empty at runtime, which
|
|
11
|
+
// returns the first operand unchanged, so it narrows nothing (nor, conservatively, does an
|
|
12
|
+
// open-ended tuple)
|
|
13
|
+
//the tuple test is against the bare RoTuple: an element constraint in the extends type would
|
|
14
|
+
// leave `[K] extends RoTuple<Kind>` undecided at an open K, since tsc settles a definitely-true
|
|
15
|
+
// check with K's own constraint stripped, and the fold below would stay deferred
|
|
16
|
+
export type CommonKind<Ks extends RoArray<Kind>> =
|
|
17
|
+
Ks extends RoTuple
|
|
18
|
+
? Ks extends HeadTail<Ks, infer H, infer T>
|
|
19
|
+
? H & CommonKind<T>
|
|
20
|
+
: unknown
|
|
21
|
+
: unknown;
|
|
22
|
+
|
|
23
|
+
//kinds are inferred through the mapped tuple, so the result is exact even at an open K, where
|
|
24
|
+
// a kind extracted from an Amount<K> would stay a deferred conditional
|
|
25
|
+
type AmountsOf<Ks extends RoArray<Kind>> = { readonly [I in keyof Ks]: Amount<Ks[I]> };
|
|
26
|
+
|
|
27
|
+
//Rates carry two kinds and a pair tuple does not infer, so their kinds are read back from the
|
|
28
|
+
// inferred rate tuple, one side at a time; assignable to Rate<NK, DK> at an open kind, exact at
|
|
29
|
+
// a concrete one
|
|
30
|
+
type SideOf<R, S extends 0 | 1> = R extends _Rate<infer NK, infer DK> ? [NK, DK][S] : never;
|
|
31
|
+
type SidesOf<Rs extends RoArray, S extends 0 | 1> = { readonly [I in keyof Rs]: SideOf<Rs[I], S> };
|
|
32
|
+
|
|
33
|
+
type CommonRate<NK extends Kind, DK extends Kind, Rs extends RoArray> =
|
|
34
|
+
Rate<NK & CommonKind<SidesOf<Rs, 0>>, DK & CommonKind<SidesOf<Rs, 1>>>;
|
|
35
|
+
|
|
36
|
+
export function min<K extends Kind, Ks extends RoArray<Kind>>(
|
|
37
|
+
first: Amount<K>,
|
|
38
|
+
...rest: AmountsOf<Ks>
|
|
39
|
+
): Amount<K & CommonKind<Ks>>;
|
|
40
|
+
export function min<NK extends Kind, DK extends Kind, Rs extends RoArray<Rate<Kind, Kind>>>(
|
|
41
|
+
first: Rate<NK, DK>,
|
|
42
|
+
...rest: Rs
|
|
43
|
+
): CommonRate<NK, DK, Rs>;
|
|
44
|
+
export function min(
|
|
45
|
+
first: Rationalish,
|
|
46
|
+
...rest: RoArray<Rationalish>
|
|
47
|
+
): Rational;
|
|
48
|
+
export function min(
|
|
49
|
+
first: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
50
|
+
...rest: RoArray<Amount<Kind> | Rate<Kind, Kind> | Rationalish>
|
|
51
|
+
): Amount<Kind> | Rate<Kind, Kind> | Rational {
|
|
52
|
+
if (isAmount(first) || isRate(first))
|
|
53
|
+
return rest.reduce((acc, value) => (acc as any).le(value) ? acc : value, first) as any;
|
|
54
|
+
|
|
55
|
+
return (rest as RoArray<Rationalish>).reduce<Rational>(
|
|
56
|
+
(acc, value) => acc.le(value) ? acc : Rational.from(value), Rational.from(first));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function max<K extends Kind, Ks extends RoArray<Kind>>(
|
|
60
|
+
first: Amount<K>,
|
|
61
|
+
...rest: AmountsOf<Ks>
|
|
62
|
+
): Amount<K & CommonKind<Ks>>;
|
|
63
|
+
export function max<NK extends Kind, DK extends Kind, Rs extends RoArray<Rate<Kind, Kind>>>(
|
|
64
|
+
first: Rate<NK, DK>,
|
|
65
|
+
...rest: Rs
|
|
66
|
+
): CommonRate<NK, DK, Rs>;
|
|
67
|
+
export function max(
|
|
68
|
+
first: Rationalish,
|
|
69
|
+
...rest: RoArray<Rationalish>
|
|
70
|
+
): Rational;
|
|
71
|
+
export function max(
|
|
72
|
+
first: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
73
|
+
...rest: RoArray<Amount<Kind> | Rate<Kind, Kind> | Rationalish>
|
|
74
|
+
): Amount<Kind> | Rate<Kind, Kind> | Rational {
|
|
75
|
+
if (isAmount(first) || isRate(first))
|
|
76
|
+
return rest.reduce((acc, value) => (acc as any).ge(value) ? acc : value, first) as any;
|
|
77
|
+
|
|
78
|
+
return (rest as RoArray<Rationalish>).reduce<Rational>(
|
|
79
|
+
(acc, value) => acc.ge(value) ? acc : Rational.from(value), Rational.from(first));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function sum<K extends Kind, Ks extends RoArray<Kind>>(
|
|
83
|
+
first: Amount<K>,
|
|
84
|
+
...rest: AmountsOf<Ks>
|
|
85
|
+
): Amount<K & CommonKind<Ks>>;
|
|
86
|
+
export function sum<NK extends Kind, DK extends Kind, Rs extends RoArray<Rate<Kind, Kind>>>(
|
|
87
|
+
first: Rate<NK, DK>,
|
|
88
|
+
...rest: Rs
|
|
89
|
+
): CommonRate<NK, DK, Rs>;
|
|
90
|
+
export function sum(
|
|
91
|
+
first: Rationalish,
|
|
92
|
+
...rest: RoArray<Rationalish>
|
|
93
|
+
): Rational;
|
|
94
|
+
export function sum(
|
|
95
|
+
first: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
96
|
+
...rest: RoArray<Amount<Kind> | Rate<Kind, Kind> | Rationalish>
|
|
97
|
+
): Amount<Kind> | Rate<Kind, Kind> | Rational {
|
|
98
|
+
if (typeof first === "number" || typeof first === "bigint")
|
|
99
|
+
first = Rational.from(first);
|
|
100
|
+
|
|
101
|
+
return rest.reduce((acc, value) => (acc as any).add(value), first) as any;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//three-way comparison, usable directly as an Array.prototype.sort comparator. Its result
|
|
105
|
+
// carries no kind, so nothing can express "the kind both share": the first operand's set of
|
|
106
|
+
// kinds is authoritative and the second must fall within it
|
|
107
|
+
export function compare<K extends Kind>(
|
|
108
|
+
a: Amount<K>,
|
|
109
|
+
b: NoInfer<Amount<K>>,
|
|
110
|
+
): -1 | 0 | 1;
|
|
111
|
+
export function compare<NK extends Kind, DK extends Kind>(
|
|
112
|
+
a: Rate<NK, DK>,
|
|
113
|
+
b: NoInfer<Rate<NK, DK>>,
|
|
114
|
+
): -1 | 0 | 1;
|
|
115
|
+
export function compare(
|
|
116
|
+
a: Rationalish,
|
|
117
|
+
b: Rationalish,
|
|
118
|
+
): -1 | 0 | 1;
|
|
119
|
+
export function compare(
|
|
120
|
+
a: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
121
|
+
b: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
122
|
+
): -1 | 0 | 1 {
|
|
123
|
+
if (typeof a === "number" || typeof a === "bigint")
|
|
124
|
+
a = Rational.from(a);
|
|
125
|
+
|
|
126
|
+
if (a instanceof Rational)
|
|
127
|
+
return a.cmp(b as Rationalish);
|
|
128
|
+
|
|
129
|
+
//no subtraction: a sort calls this n·log n times, and an amount's or rate's own gt/lt check
|
|
130
|
+
// the kinds
|
|
131
|
+
const ordered = a as { gt(other: unknown): boolean, lt(other: unknown): boolean };
|
|
132
|
+
return ordered.gt(b) ? 1 : ordered.lt(b) ? -1 : 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function clamp<K extends Kind, KL extends Kind, KH extends Kind>(
|
|
136
|
+
x: Amount<K>,
|
|
137
|
+
lo: Amount<KL>,
|
|
138
|
+
hi: Amount<KH>,
|
|
139
|
+
): Amount<K & KL & KH>;
|
|
140
|
+
export function clamp<
|
|
141
|
+
NK extends Kind, DK extends Kind,
|
|
142
|
+
NL extends Kind, DL extends Kind,
|
|
143
|
+
NH extends Kind, DH extends Kind,
|
|
144
|
+
>(
|
|
145
|
+
x: Rate<NK, DK>,
|
|
146
|
+
lo: Rate<NL, DL>,
|
|
147
|
+
hi: Rate<NH, DH>,
|
|
148
|
+
): Rate<NK & NL & NH, DK & DL & DH>;
|
|
149
|
+
export function clamp(
|
|
150
|
+
x: Rationalish,
|
|
151
|
+
lo: Rationalish,
|
|
152
|
+
hi: Rationalish,
|
|
153
|
+
): Rational;
|
|
154
|
+
export function clamp(
|
|
155
|
+
x: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
156
|
+
lo: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
157
|
+
hi: Amount<Kind> | Rate<Kind, Kind> | Rationalish,
|
|
158
|
+
): Amount<Kind> | Rate<Kind, Kind> | Rational {
|
|
159
|
+
return min(max(x as any, lo as any), hi as any);
|
|
160
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { OfLength, RoArray, RoNeTuple } from "@onrail-xyz/utils";
|
|
2
|
+
import { isArray } from "@onrail-xyz/utils";
|
|
3
|
+
import type { Rationalish } from "./rational.js";
|
|
4
|
+
import { Rational } from "./rational.js";
|
|
5
|
+
import type { Kind, KindWithAtomic, SymbolsOf } from "./kind.js";
|
|
6
|
+
import type { Amount } from "./amount.js";
|
|
7
|
+
|
|
8
|
+
//a literal count or weights tuple yields a length-typed tuple; a runtime length degrades to a
|
|
9
|
+
// plain array
|
|
10
|
+
type Allocation<K extends Kind, P extends number | RoArray<Rationalish>> =
|
|
11
|
+
P extends RoArray<Rationalish>
|
|
12
|
+
? OfLength<Amount<K>, P["length"]>
|
|
13
|
+
: OfLength<Amount<K>, P & number>;
|
|
14
|
+
|
|
15
|
+
//Parts are pro-rata by weight (any sign, nonzero total), or equal when a count is given;
|
|
16
|
+
// each part is a whole number of the given unit (atomic by default) and the parts sum exactly
|
|
17
|
+
// to the amount, leftover units going to the largest fractional shares (earlier index breaks
|
|
18
|
+
// ties). The amount must itself be whole in that unit: allocation never rounds the total, so
|
|
19
|
+
// quantization — and the dust decision that comes with it — stays with the caller.
|
|
20
|
+
export function allocate<
|
|
21
|
+
K extends KindWithAtomic,
|
|
22
|
+
const P extends number | RoNeTuple<Rationalish>,
|
|
23
|
+
>(amount: Amount<K>, parts: P): Allocation<K, P>;
|
|
24
|
+
export function allocate<
|
|
25
|
+
K extends Kind,
|
|
26
|
+
const P extends number | RoNeTuple<Rationalish>,
|
|
27
|
+
>(amount: Amount<K>, parts: P, unitSymbol: SymbolsOf<K>): Allocation<K, P>;
|
|
28
|
+
export function allocate(
|
|
29
|
+
amount: Amount<Kind>,
|
|
30
|
+
parts: number | RoArray<Rationalish>,
|
|
31
|
+
unitSymbol: SymbolsOf<Kind> = "atomic",
|
|
32
|
+
): RoArray<Amount<Kind>> {
|
|
33
|
+
if (!amount.eq(amount.floorTo(unitSymbol)))
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Amount is not a whole number of ${unitSymbol} units - quantize it first ` +
|
|
36
|
+
`(floorTo/roundTo/ceilTo)`
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (isArray(parts) ? parts.length === 0 : !(Number.isSafeInteger(parts) && parts > 0))
|
|
40
|
+
throw new Error("parts must be a nonempty weights list or a positive integer count");
|
|
41
|
+
|
|
42
|
+
const weights = isArray(parts)
|
|
43
|
+
? parts.map(w => Rational.from(w))
|
|
44
|
+
: Array.from({ length: parts }, () => Rational.from(1n));
|
|
45
|
+
const totalWeight = weights.reduce((acc, w) => acc.add(w), Rational.from(0n));
|
|
46
|
+
if (totalWeight.eq(0n))
|
|
47
|
+
throw new Error("weights must not sum to zero");
|
|
48
|
+
|
|
49
|
+
const inUnit: Rational | bigint = amount.in(unitSymbol);
|
|
50
|
+
const total = typeof inUnit === "bigint" ? inUnit : inUnit.floor(); //integral, so floor is exact
|
|
51
|
+
|
|
52
|
+
const shares = weights.map(w => Rational.from(total).mul(w).div(totalWeight));
|
|
53
|
+
const floors = shares.map(s => s.floor());
|
|
54
|
+
let leftover = total - floors.reduce((acc, f) => acc + f, 0n);
|
|
55
|
+
|
|
56
|
+
const byFraction = shares
|
|
57
|
+
.map((s, i) => [s.sub(floors[i]!), i] as const)
|
|
58
|
+
.sort((a, b) => a[0].eq(b[0]) ? a[1] - b[1] : a[0].gt(b[0]) ? -1 : 1);
|
|
59
|
+
|
|
60
|
+
const bumped = new Set<number>();
|
|
61
|
+
for (const [, i] of byFraction) {
|
|
62
|
+
if (leftover === 0n)
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
bumped.add(i);
|
|
66
|
+
--leftover;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return floors.map((f, i) => amount.ofSame(bumped.has(i) ? f + 1n : f, unitSymbol));
|
|
70
|
+
}
|
package/src/amount.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type { Brand, NarrowTo, Opts, RoArray } from "@onrail-xyz/utils";
|
|
2
|
+
import { brand } from "@onrail-xyz/utils";
|
|
3
|
+
import type { Rationalish, ToFixedOptions } from "./rational.js";
|
|
4
|
+
import { Rational } from "./rational.js";
|
|
5
|
+
import type { Kind, KindWithHuman, SymbolsOf,
|
|
6
|
+
DecimalSymbolsOf, KindUnitSymbols, CandidateKinds } from "./kind.js";
|
|
7
|
+
import { getUnit, identifyKind, sameKind } from "./kind.js";
|
|
8
|
+
import { Rate, isRate } from "./rate.js";
|
|
9
|
+
import { approximate, exact, inUnit, parse as parseFormat } from "./format.js";
|
|
10
|
+
|
|
11
|
+
export type AmountFromArgs<K extends Kind> = [kind: K, unitSymbol: SymbolsOf<K>];
|
|
12
|
+
|
|
13
|
+
export const scalar = brand<"scalar">();
|
|
14
|
+
|
|
15
|
+
//Free functions, not statics: a static is entered with a kind in hand (ofKind, from, parse, the
|
|
16
|
+
// *OfKind guards) and produces or narrows towards an amount of it; these are entered with an
|
|
17
|
+
// amount - or with nothing at all - and recover what the receiver's erased K cannot give back
|
|
18
|
+
// (Amount.md, "Free functions get fresh generic parameters"). rate.ts follows the same split.
|
|
19
|
+
//exact kind reads at an open K, where the `kind` property erases
|
|
20
|
+
export const kindOf = <K extends Kind>(amount: Amount<K>): K => amount.kind as K;
|
|
21
|
+
|
|
22
|
+
//class check without touching the underscored class:
|
|
23
|
+
// a union narrows to exactly its amount constituents
|
|
24
|
+
// an open Amount<K> keeps its K
|
|
25
|
+
// an unknown narrows to Amount<Kind>
|
|
26
|
+
export const isAmount = <T>(value: T): value is NarrowTo<T, Amount<Kind>> =>
|
|
27
|
+
value instanceof _Amount;
|
|
28
|
+
|
|
29
|
+
//see end of file for the actual exports
|
|
30
|
+
//two Amount spellings:
|
|
31
|
+
// * _Amount<K> is the implementation class that's only exported for type-reachability
|
|
32
|
+
// * Amount<K> (see end of file) is the distributive alias over it. It's the only spelling that
|
|
33
|
+
// should be used by consumers
|
|
34
|
+
//
|
|
35
|
+
//Kind-preserving members return this so things match as intended, even when deferred at an open K
|
|
36
|
+
export class _Amount<K extends Kind> {
|
|
37
|
+
private readonly value: Rational;
|
|
38
|
+
readonly kind: K;
|
|
39
|
+
|
|
40
|
+
private constructor(value: Rational, kind: K) {
|
|
41
|
+
this.value = value;
|
|
42
|
+
this.kind = kind;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static ofKind<const K extends KindWithHuman>(kind: K):
|
|
46
|
+
(numericalValue: Rationalish | string, unitSymbol?: SymbolsOf<K>) => Amount<K>;
|
|
47
|
+
static ofKind<const K extends Kind>(kind: K):
|
|
48
|
+
(numericalValue: Rationalish | string, unitSymbol: SymbolsOf<K>) => Amount<K>;
|
|
49
|
+
static ofKind(kind: Kind) {
|
|
50
|
+
return (numericalValue: Rationalish | string, unitSymbol?: string) =>
|
|
51
|
+
_Amount.fromInternal(numericalValue, kind, unitSymbol ?? kind.human) as Amount<Kind>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static from<const K extends KindWithHuman>(
|
|
55
|
+
numericalValue: Rationalish | string,
|
|
56
|
+
...args: [kind: K, unitSymbol?: SymbolsOf<K>]
|
|
57
|
+
): Amount<K>;
|
|
58
|
+
static from<const K extends Kind>(
|
|
59
|
+
numericalValue: Rationalish | string,
|
|
60
|
+
...args: AmountFromArgs<K>
|
|
61
|
+
): Amount<K>;
|
|
62
|
+
static from<const K extends Kind>(
|
|
63
|
+
numericalValue: Rationalish | string,
|
|
64
|
+
...args: [kind: K, unitSymbol?: SymbolsOf<K>]
|
|
65
|
+
): Amount<K> {
|
|
66
|
+
return _Amount.fromInternal(numericalValue, args[0], args[1]) as Amount<K>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//the candidates are inferred as a tuple, not as one K: a single type param would bind to the
|
|
70
|
+
// first kind and reject every other, which is the whole point of passing several
|
|
71
|
+
static parse<const KS extends RoArray<Kind>>(str: string, ...kinds: KS): Amount<KS[number]> {
|
|
72
|
+
const kind = identifyKind(kinds, str);
|
|
73
|
+
if (!kind)
|
|
74
|
+
throw new Error("Could not identify kind from string");
|
|
75
|
+
|
|
76
|
+
const value = parseFormat(kind, str);
|
|
77
|
+
return _Amount.fromInternal(value, kind, kind.standard.unit) as Amount<KS[number]>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static isOfKind<
|
|
81
|
+
A extends Amount<Kind>,
|
|
82
|
+
K extends CandidateKinds<A["kind"]>,
|
|
83
|
+
>(amt: A, kind: K): amt is NarrowTo<A, Amount<K>> {
|
|
84
|
+
return sameKind(amt.kind, kind);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static allOfKind<
|
|
88
|
+
A extends Amount<Kind>,
|
|
89
|
+
K extends CandidateKinds<A["kind"]>,
|
|
90
|
+
>(amts: RoArray<A>, kind: K): amts is RoArray<NarrowTo<A, Amount<K>>> {
|
|
91
|
+
return amts.every(amt => sameKind(amt.kind, kind));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//instance factories — "an amount of my own kind". The kind-generic replacement for reading
|
|
95
|
+
// `kind` back and feeding it to ofKind: `kind` erases at an open K, `this` does not.
|
|
96
|
+
zero(): this {
|
|
97
|
+
return new _Amount(Rational.from(0n), this.kind) as this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ofSame<S extends SymbolsOf<K>>(numericalValue: Rationalish | string, unitSymbol: S): this {
|
|
101
|
+
return _Amount.fromInternal(numericalValue, this.kind, unitSymbol) as this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
toString(): string;
|
|
105
|
+
toString(
|
|
106
|
+
system: Extract<keyof K["systems"], string>,
|
|
107
|
+
opts?: Opts<ToFixedOptions>,
|
|
108
|
+
): string;
|
|
109
|
+
toString(
|
|
110
|
+
mode: "approximate" | "exact",
|
|
111
|
+
opts?: Opts<ToFixedOptions & { system: Extract<keyof K["systems"], string> }>,
|
|
112
|
+
): string;
|
|
113
|
+
toString<S extends SymbolsOf<K>>(
|
|
114
|
+
mode: "inUnit",
|
|
115
|
+
symbol: S,
|
|
116
|
+
opts?: Opts<ToFixedOptions & {
|
|
117
|
+
precision: number | (S extends DecimalSymbolsOf<K> ? DecimalSymbolsOf<K> : never);
|
|
118
|
+
}>,
|
|
119
|
+
): string;
|
|
120
|
+
toString(
|
|
121
|
+
modeOrSys?: string,
|
|
122
|
+
symbolOrOpts?: SymbolsOf<K> | Opts<ToFixedOptions & { system?: string }>,
|
|
123
|
+
opts?: Opts<ToFixedOptions & { precision?: number | DecimalSymbolsOf<K> }>,
|
|
124
|
+
): string {
|
|
125
|
+
if (modeOrSys === "inUnit") {
|
|
126
|
+
const symbol = getUnit(this.kind, symbolOrOpts as SymbolsOf<K>).symbol as KindUnitSymbols<K>;
|
|
127
|
+
const prec = typeof opts?.precision === "string"
|
|
128
|
+
? getUnit(this.kind, opts.precision as SymbolsOf<K>).symbol as KindUnitSymbols<K>
|
|
129
|
+
: opts?.precision;
|
|
130
|
+
|
|
131
|
+
return inUnit(this.kind, this.value, symbol, prec, opts);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const isMode = !modeOrSys || modeOrSys === "approximate" || modeOrSys === "exact";
|
|
135
|
+
const o = (
|
|
136
|
+
isMode ? symbolOrOpts : { system: modeOrSys, ...(symbolOrOpts as object) }
|
|
137
|
+
) as Opts<ToFixedOptions & { system?: string }> | undefined;
|
|
138
|
+
|
|
139
|
+
return modeOrSys === "exact"
|
|
140
|
+
? exact (this.kind, this.value, o)
|
|
141
|
+
: approximate(this.kind, this.value, o);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
toJSON(): string {
|
|
145
|
+
return this.toString("exact");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
in<S extends SymbolsOf<K>>(unitSymbol: S): S extends "atomic" ? bigint : Rational;
|
|
149
|
+
in(unitSymbol: K extends { human: string } ? "human" : never): Rational;
|
|
150
|
+
in(unitSymbol: K extends { atomic: string } ? "atomic" : never): bigint;
|
|
151
|
+
in<S extends SymbolsOf<K>>(unitSymbol: S): S extends "atomic" ? bigint : Rational {
|
|
152
|
+
const rat = this.getIn(unitSymbol);
|
|
153
|
+
return (unitSymbol === "atomic" ? rat.floor() : rat) as S extends "atomic" ? bigint : Rational;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//the S on the unit-symbol methods might seem pointless, but it's in fact crucial:
|
|
157
|
+
// a bare SymbolsOf<K> breaks overload/implementation compatibility of the this-returning methods
|
|
158
|
+
// (the compatibility check relates _Amount<K> to _Amount<Kind>, where only same-shape generic
|
|
159
|
+
// signatures unify across the deferred SymbolsOf<K> / string constraint divide)
|
|
160
|
+
ceilTo<S extends SymbolsOf<K>>(unitSymbol: S): this {
|
|
161
|
+
return _Amount.fromInternal(this.getIn(unitSymbol).ceil(), this.kind, unitSymbol) as this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
roundTo<S extends SymbolsOf<K>>(unitSymbol: S): this {
|
|
165
|
+
return _Amount.fromInternal(this.getIn(unitSymbol).round(), this.kind, unitSymbol) as this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
floorTo<S extends SymbolsOf<K>>(unitSymbol: S): this {
|
|
169
|
+
return _Amount.fromInternal(this.getIn(unitSymbol).floor(), this.kind, unitSymbol) as this;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
isZero(): boolean {
|
|
173
|
+
return this.value.eq(0n);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
sign(): -1 | 0 | 1 {
|
|
177
|
+
return this.value.sign();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
eq(other: this): boolean {
|
|
181
|
+
this.checkKind(other.kind);
|
|
182
|
+
return this.value.eq(other.value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ne(other: this): boolean {
|
|
186
|
+
this.checkKind(other.kind);
|
|
187
|
+
return this.value.ne(other.value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lt(other: this): boolean {
|
|
191
|
+
this.checkKind(other.kind);
|
|
192
|
+
return this.value.lt(other.value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
le(other: this): boolean {
|
|
196
|
+
this.checkKind(other.kind);
|
|
197
|
+
return this.value.le(other.value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
gt(other: this): boolean {
|
|
201
|
+
this.checkKind(other.kind);
|
|
202
|
+
return this.value.gt(other.value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
ge(other: this): boolean {
|
|
206
|
+
this.checkKind(other.kind);
|
|
207
|
+
return this.value.ge(other.value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
abs(): this {
|
|
211
|
+
return new _Amount(this.value.abs(), this.kind) as this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
neg(): this {
|
|
215
|
+
return new _Amount(this.value.neg(), this.kind) as this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
add(other: this): this {
|
|
219
|
+
this.checkKind(other.kind);
|
|
220
|
+
return new _Amount(this.value.add(other.value), this.kind) as this;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
sub(other: this): this {
|
|
224
|
+
this.checkKind(other.kind);
|
|
225
|
+
return new _Amount(this.value.sub(other.value), this.kind) as this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
mul(other: Rationalish | Amount<Brand<Kind, "scalar">>): this;
|
|
229
|
+
mul<NK extends Kind>(other: Rate<NK, K>): Amount<NK>;
|
|
230
|
+
mul(other: Rationalish | Amount<Brand<Kind, "scalar">> | Rate<Kind, K>): Amount<Kind> {
|
|
231
|
+
if (isRate(other)) {
|
|
232
|
+
this.checkKind(other.den);
|
|
233
|
+
return new _Amount(this.value.mul(other.ratio), other.num) as Amount<Kind>;
|
|
234
|
+
}
|
|
235
|
+
const rhs = isAmount(other) ? other.value : other;
|
|
236
|
+
return new _Amount(this.value.mul(rhs), this.kind) as Amount<Kind>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
div(other: Rationalish | Amount<Brand<Kind, "scalar">>): this;
|
|
240
|
+
div<DK extends Kind>(other: Rate<K, DK>): Amount<DK>;
|
|
241
|
+
div(other: Rationalish | Amount<Brand<Kind, "scalar">> | Rate<K, Kind>): Amount<Kind> {
|
|
242
|
+
if (isRate(other)) {
|
|
243
|
+
this.checkKind(other.num);
|
|
244
|
+
return new _Amount(this.value.div(other.ratio), other.den) as Amount<Kind>;
|
|
245
|
+
}
|
|
246
|
+
const rhs = isAmount(other) ? other.value : other;
|
|
247
|
+
return new _Amount(this.value.div(rhs), this.kind) as Amount<Kind>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
//the quotient of two amounts of one kind, which is a plain number. It is not a `div` overload
|
|
251
|
+
// because an open K may be instantiated as a union (a tranche kind known only at runtime):
|
|
252
|
+
// at K1 | K2 an overload for `this` accepts an operand of the other kind - a scalar one
|
|
253
|
+
// included, which `div` scales by - and no runtime check can tell which of the two readings
|
|
254
|
+
// the types chose. `div` takes scalars only, and this checks the kind like add and sub do
|
|
255
|
+
ratio(other: this): Rational {
|
|
256
|
+
this.checkKind(other.kind);
|
|
257
|
+
return this.value.div(other.value);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
mod(other: this): this {
|
|
261
|
+
this.checkKind(other.kind);
|
|
262
|
+
return new _Amount(this.value.mod(other.value), this.kind) as this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
per<const DK extends Kind>(den: KindWithHuman & DK | Amount<DK>): Rate<K, DK> {
|
|
266
|
+
return Rate.from(this as any, den as any);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private static fromInternal<const K extends Kind>(
|
|
270
|
+
numericalValue: Rationalish | string,
|
|
271
|
+
kind: K,
|
|
272
|
+
unitSymbol?: string,
|
|
273
|
+
): _Amount<K> {
|
|
274
|
+
const unit = getUnit(kind, (unitSymbol ?? "human") as SymbolsOf<K>);
|
|
275
|
+
return new _Amount(Rational.from(numericalValue).mul(unit.scale), kind);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private getIn(unitSymbol: SymbolsOf<K>): Rational {
|
|
279
|
+
return this.value.div(getUnit(this.kind, unitSymbol).scale);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private checkKind(otherKind: Kind): void {
|
|
283
|
+
if (!sameKind(this.kind, otherKind))
|
|
284
|
+
throw new Error(`Kind mismatch: ${this.kind.name} vs ${otherKind.name}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//always distribute because _Amount<KindUnion> is useless
|
|
289
|
+
type Amount<K extends Kind> = K extends Kind ? _Amount<K> : never;
|
|
290
|
+
const Amount = _Amount;
|
|
291
|
+
export { Amount };
|