@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
package/src/rate.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import type { Opts, MaybeArray, Brand, NarrowTo, RoArray } from "@onrail-xyz/utils";
|
|
2
|
+
import { isArray } from "@onrail-xyz/utils";
|
|
3
|
+
import type { Rationalish, ToFixedOptions } from "./rational.js";
|
|
4
|
+
import { Rational } from "./rational.js";
|
|
5
|
+
import type { Kind, KindWithHuman, CandidateKinds,
|
|
6
|
+
SymbolsOf, DecimalSymbolsOf, KindUnitSymbols } from "./kind.js";
|
|
7
|
+
import { getUnit, sameKind } from "./kind.js";
|
|
8
|
+
import { Amount, isAmount } from "./amount.js";
|
|
9
|
+
import { bestDenUnit, exactRate, inUnit, inUnitApprox } from "./format.js";
|
|
10
|
+
import { findUnit } from "./segmenting.js";
|
|
11
|
+
|
|
12
|
+
//most things in here rhyme with Amount and so the rationales transfer
|
|
13
|
+
|
|
14
|
+
export const numKindOf = <NK extends Kind, DK extends Kind>(rate: Rate<NK, DK>): NK =>
|
|
15
|
+
rate.num as NK;
|
|
16
|
+
export const denKindOf = <NK extends Kind, DK extends Kind>(rate: Rate<NK, DK>): DK =>
|
|
17
|
+
rate.den as DK;
|
|
18
|
+
|
|
19
|
+
export const invert = <NK extends Kind, DK extends Kind>(rate: Rate<NK, DK>): Rate<DK, NK> =>
|
|
20
|
+
rate.inv() as Rate<DK, NK>;
|
|
21
|
+
|
|
22
|
+
export const isRate = <T>(value: T): value is NarrowTo<T, Rate<Kind, Kind>> =>
|
|
23
|
+
value instanceof _Rate;
|
|
24
|
+
|
|
25
|
+
export class _Rate<NK extends Kind, DK extends Kind> {
|
|
26
|
+
readonly ratio: Rational;
|
|
27
|
+
readonly num: NK;
|
|
28
|
+
readonly den: DK;
|
|
29
|
+
|
|
30
|
+
private constructor(ratio: Rational, num: NK, den: DK) {
|
|
31
|
+
this.ratio = ratio;
|
|
32
|
+
this.num = num;
|
|
33
|
+
this.den = den;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static from<const NK extends Kind, const DK extends Kind>(
|
|
37
|
+
ratio: Rationalish,
|
|
38
|
+
num: KindWithHuman & NK,
|
|
39
|
+
den: KindWithHuman & DK,
|
|
40
|
+
): Rate<NK, DK>;
|
|
41
|
+
static from<const NK extends Kind, const DK extends Kind>(
|
|
42
|
+
num: Amount<NK>,
|
|
43
|
+
den: KindWithHuman & DK | Amount<DK>,
|
|
44
|
+
): Rate<NK, DK>;
|
|
45
|
+
static from(
|
|
46
|
+
ratioOrNum: Rationalish | Amount<Kind>,
|
|
47
|
+
numOrDen: KindWithHuman | Amount<Kind>,
|
|
48
|
+
maybeDen?: KindWithHuman,
|
|
49
|
+
): any {
|
|
50
|
+
const [amtNum, den] = isAmount(ratioOrNum)
|
|
51
|
+
? [ratioOrNum, numOrDen]
|
|
52
|
+
: [Amount.from(ratioOrNum, numOrDen as KindWithHuman, "human") as Amount<Kind>, maybeDen];
|
|
53
|
+
const amtDen = isAmount(den)
|
|
54
|
+
? den
|
|
55
|
+
: Amount.from(1, den as KindWithHuman, "human") as Amount<Kind>;
|
|
56
|
+
|
|
57
|
+
return _Rate.checkedNew(
|
|
58
|
+
amtNum.in("standard").div(amtDen.in("standard")),
|
|
59
|
+
amtNum.kind,
|
|
60
|
+
amtDen.kind,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static parse<const NK extends Kind, const DK extends Kind>(
|
|
65
|
+
str: string,
|
|
66
|
+
numKinds: MaybeArray<NK>,
|
|
67
|
+
denKinds: MaybeArray<DK>
|
|
68
|
+
): Rate<NK, DK> {
|
|
69
|
+
const lastSlash = str.lastIndexOf("/");
|
|
70
|
+
if (lastSlash === -1)
|
|
71
|
+
throw new Error("Expected string in format 'numerator/denominator'");
|
|
72
|
+
|
|
73
|
+
const numKindsArray = isArray(numKinds) ? numKinds : [numKinds];
|
|
74
|
+
const denKindsArray = isArray(denKinds) ? denKinds : [denKinds];
|
|
75
|
+
const numAmount = Amount.parse(str.substring(0, lastSlash), ...numKindsArray);
|
|
76
|
+
const denStr = str.substring(lastSlash + 1);
|
|
77
|
+
const denKindRes = denKindsArray.filter(k => findUnit(k.units, denStr) !== undefined);
|
|
78
|
+
if (denKindRes.length !== 1)
|
|
79
|
+
throw new Error("Could not identify denominator kind from string");
|
|
80
|
+
|
|
81
|
+
const denKind = denKindRes[0]!;
|
|
82
|
+
const denUnit = findUnit(denKind.units, denStr)!;
|
|
83
|
+
const ratio = numAmount.in("standard").div(denUnit.scale);
|
|
84
|
+
|
|
85
|
+
return _Rate.checkedNew(ratio, numAmount.kind as NK, denKind);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static hasNum<R extends Rate<Kind, Kind>, K extends CandidateKinds<R["num"]>>(
|
|
89
|
+
rate: R,
|
|
90
|
+
numKind: K,
|
|
91
|
+
): rate is NarrowTo<R, Rate<K, Kind>> {
|
|
92
|
+
return sameKind(rate.num, numKind);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static hasDen<R extends Rate<Kind, Kind>, K extends CandidateKinds<R["den"]>>(
|
|
96
|
+
rate: R,
|
|
97
|
+
denKind: K,
|
|
98
|
+
): rate is NarrowTo<R, Rate<Kind, K>> {
|
|
99
|
+
return sameKind(rate.den, denKind);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static allHaveNum<R extends Rate<Kind, Kind>, K extends CandidateKinds<R["num"]>>(
|
|
103
|
+
rates: RoArray<R>,
|
|
104
|
+
numKind: K,
|
|
105
|
+
): rates is RoArray<NarrowTo<R, Rate<K, Kind>>> {
|
|
106
|
+
return rates.every(rate => sameKind(rate.num, numKind));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static allHaveDen<R extends Rate<Kind, Kind>, K extends CandidateKinds<R["den"]>>(
|
|
110
|
+
rates: RoArray<R>,
|
|
111
|
+
denKind: K,
|
|
112
|
+
): rates is RoArray<NarrowTo<R, Rate<Kind, K>>> {
|
|
113
|
+
return rates.every(rate => sameKind(rate.den, denKind));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
zero(): this {
|
|
117
|
+
return new _Rate(Rational.from(0n), this.num, this.den) as this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ofSame<NS extends SymbolsOf<NK>, DS extends SymbolsOf<DK>>(
|
|
121
|
+
numericalValue: Rationalish | string,
|
|
122
|
+
numUnit: NS,
|
|
123
|
+
denUnit: DS,
|
|
124
|
+
): this {
|
|
125
|
+
const num = getUnit(this.num, numUnit);
|
|
126
|
+
const den = getUnit(this.den, denUnit);
|
|
127
|
+
return new _Rate(
|
|
128
|
+
Rational.from(numericalValue).mul(num.scale).div(den.scale),
|
|
129
|
+
this.num,
|
|
130
|
+
this.den,
|
|
131
|
+
) as this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
toString<
|
|
135
|
+
NS extends SymbolsOf<NK> = SymbolsOf<NK>,
|
|
136
|
+
DS extends SymbolsOf<DK> = SymbolsOf<DK>,
|
|
137
|
+
>(opts?: Opts<ToFixedOptions & {
|
|
138
|
+
numSymbol: NS;
|
|
139
|
+
denSymbol: DS;
|
|
140
|
+
precision: number | (NS extends DecimalSymbolsOf<NK> ? DecimalSymbolsOf<NK> : never);
|
|
141
|
+
}>): string;
|
|
142
|
+
toString(mode: "exact", opts?: Opts<ToFixedOptions>): string;
|
|
143
|
+
toString(
|
|
144
|
+
modeOrOpts?: "exact" | Opts<ToFixedOptions & {
|
|
145
|
+
numSymbol: SymbolsOf<NK>;
|
|
146
|
+
denSymbol: SymbolsOf<DK>;
|
|
147
|
+
precision: number | DecimalSymbolsOf<NK>;
|
|
148
|
+
}>,
|
|
149
|
+
exactOpts?: Opts<ToFixedOptions>,
|
|
150
|
+
): string {
|
|
151
|
+
if (modeOrOpts === "exact")
|
|
152
|
+
return exactRate(this.num, this.den, this.ratio, exactOpts);
|
|
153
|
+
|
|
154
|
+
const opts = modeOrOpts;
|
|
155
|
+
const numSym = (opts?.numSymbol ?? this.num.human ?? this.num.standard.unit) as SymbolsOf<NK>;
|
|
156
|
+
const numUnit = getUnit(this.num, numSym);
|
|
157
|
+
const denUnit = opts?.denSymbol !== undefined
|
|
158
|
+
? getUnit(this.den, opts.denSymbol)
|
|
159
|
+
: bestDenUnit(this.num, this.den, this.ratio);
|
|
160
|
+
const stdVal = this.ratio.mul(denUnit.scale);
|
|
161
|
+
const prec = typeof opts?.precision === "string"
|
|
162
|
+
? getUnit(this.num, opts.precision as SymbolsOf<NK>).symbol as KindUnitSymbols<NK>
|
|
163
|
+
: opts?.precision;
|
|
164
|
+
|
|
165
|
+
const numSymbol = numUnit.symbol as KindUnitSymbols<NK>;
|
|
166
|
+
const num = prec === undefined
|
|
167
|
+
? inUnitApprox(this.num, stdVal, numSymbol, opts)
|
|
168
|
+
: inUnit(this.num, stdVal, numSymbol, prec, opts);
|
|
169
|
+
return `${num}/${denUnit.symbol}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
toJSON(): string {
|
|
173
|
+
return this.toString("exact");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
in<NS extends SymbolsOf<NK>, DS extends SymbolsOf<DK>>(numUnit: NS, denUnit: DS): Rational {
|
|
177
|
+
const num = getUnit(this.num, numUnit);
|
|
178
|
+
const den = getUnit(this.den, denUnit);
|
|
179
|
+
return this.ratio.mul(den.scale).div(num.scale);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ceilTo<NS extends SymbolsOf<NK>, DS extends SymbolsOf<DK>>(numUnit: NS, denUnit: DS): this {
|
|
183
|
+
return this.ofSame(this.in(numUnit, denUnit).ceil(), numUnit, denUnit);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
roundTo<NS extends SymbolsOf<NK>, DS extends SymbolsOf<DK>>(numUnit: NS, denUnit: DS): this {
|
|
187
|
+
return this.ofSame(this.in(numUnit, denUnit).round(), numUnit, denUnit);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
floorTo<NS extends SymbolsOf<NK>, DS extends SymbolsOf<DK>>(numUnit: NS, denUnit: DS): this {
|
|
191
|
+
return this.ofSame(this.in(numUnit, denUnit).floor(), numUnit, denUnit);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
isZero(): boolean {
|
|
195
|
+
return this.ratio.eq(0n);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
sign(): -1 | 0 | 1 {
|
|
199
|
+
return this.ratio.sign();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
eq(other: this): boolean {
|
|
203
|
+
this.checkKinds(other);
|
|
204
|
+
return this.ratio.eq(other.ratio);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
ne(other: this): boolean {
|
|
208
|
+
this.checkKinds(other);
|
|
209
|
+
return this.ratio.ne(other.ratio);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
lt(other: this): boolean {
|
|
213
|
+
this.checkKinds(other);
|
|
214
|
+
return this.ratio.lt(other.ratio);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
le(other: this): boolean {
|
|
218
|
+
this.checkKinds(other);
|
|
219
|
+
return this.ratio.le(other.ratio);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
gt(other: this): boolean {
|
|
223
|
+
this.checkKinds(other);
|
|
224
|
+
return this.ratio.gt(other.ratio);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
ge(other: this): boolean {
|
|
228
|
+
this.checkKinds(other);
|
|
229
|
+
return this.ratio.ge(other.ratio);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
abs(): this {
|
|
233
|
+
return new _Rate(this.ratio.abs(), this.num, this.den) as this;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
neg(): this {
|
|
237
|
+
return new _Rate(this.ratio.neg(), this.num, this.den) as this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
add(other: this): this {
|
|
241
|
+
this.checkKinds(other);
|
|
242
|
+
return new _Rate(this.ratio.add(other.ratio), this.num, this.den) as this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
sub(other: this): this {
|
|
246
|
+
this.checkKinds(other);
|
|
247
|
+
return new _Rate(this.ratio.sub(other.ratio), this.num, this.den) as this;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
mul(scalar: Rationalish | Amount<Brand<Kind, "scalar">>): this {
|
|
251
|
+
scalar = isAmount(scalar) ? scalar.in("standard") : scalar as Rationalish;
|
|
252
|
+
return new _Rate(this.ratio.mul(scalar), this.num, this.den) as this;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
div(scalar: Rationalish | Amount<Brand<Kind, "scalar">>): this {
|
|
256
|
+
scalar = isAmount(scalar) ? scalar.in("standard") : scalar as Rationalish;
|
|
257
|
+
return new _Rate(this.ratio.div(scalar), this.num, this.den) as this;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
inv(): Rate<DK, NK> {
|
|
261
|
+
return new _Rate(this.ratio.inv(), this.den, this.num) as Rate<DK, NK>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
combine<
|
|
265
|
+
NKO extends DK,
|
|
266
|
+
DKO extends Kind,
|
|
267
|
+
>(other: Rate<NKO, DKO>): Rate<NK, DKO> {
|
|
268
|
+
if (!sameKind(this.den, other.num))
|
|
269
|
+
throw new Error(`Kind mismatch: ${this.den.name} vs ${other.num.name}`);
|
|
270
|
+
|
|
271
|
+
return _Rate.checkedNew(this.ratio.mul(other.ratio), this.num, other.den);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
cancel(other: Rate<DK, NK>): Rational {
|
|
275
|
+
if (!sameKind(this.den, other.num) || !sameKind(this.num, other.den))
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Kind mismatch: ${this.den.name}/${this.num.name} vs ${other.num.name}/${other.den.name}`
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
return this.ratio.mul(other.ratio);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private checkKinds(other: _Rate<NK, DK>): void {
|
|
284
|
+
if (!sameKind(this.num, other.num) || !sameKind(this.den, other.den))
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Kind mismatch: ${this.num.name}/${this.den.name} vs ${other.num.name}/${other.den.name}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private static checkedNew<
|
|
291
|
+
NK extends Kind,
|
|
292
|
+
DK extends Kind,
|
|
293
|
+
>(ratio: Rational, num: NK, den: DK): Rate<NK, DK> {
|
|
294
|
+
if (sameKind(num, den))
|
|
295
|
+
throw new Error(`Must be distinct kinds: ${num.name} vs ${den.name}`);
|
|
296
|
+
|
|
297
|
+
return new _Rate(ratio, num, den) as Rate<NK, DK>;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
//always distribute, like Amount. The `[DK] extends [never]` guard is for an intersection of
|
|
302
|
+
// distinct kinds handed in as the denominator (what an aggregate over mismatched rates yields):
|
|
303
|
+
// tsc reduces such an intersection to never only on the way into the outermost conditional -
|
|
304
|
+
// the inner one is reached by tail recursion (canTailRecurse in checker.ts), which passes the
|
|
305
|
+
// check type through unreduced, so the intersection would land in _Rate instead of collapsing
|
|
306
|
+
// the rate. The tuple check relates by assignability, which does reduce.
|
|
307
|
+
type Rate<NK extends Kind, DK extends Kind> =
|
|
308
|
+
NK extends Kind
|
|
309
|
+
? [DK] extends [never]
|
|
310
|
+
? never
|
|
311
|
+
: DK extends Kind
|
|
312
|
+
? _Rate<NK, DK>
|
|
313
|
+
: never
|
|
314
|
+
: never;
|
|
315
|
+
const Rate = _Rate;
|
|
316
|
+
export { Rate };
|