@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.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +617 -0
  3. package/dist/aggregating.d.ts +31 -0
  4. package/dist/aggregating.d.ts.map +1 -0
  5. package/dist/aggregating.js +31 -0
  6. package/dist/allocating.d.ts +9 -0
  7. package/dist/allocating.d.ts.map +1 -0
  8. package/dist/allocating.js +31 -0
  9. package/dist/amount.d.ts +68 -0
  10. package/dist/amount.d.ts.map +1 -0
  11. package/dist/amount.js +184 -0
  12. package/dist/format.d.ts +15 -0
  13. package/dist/format.d.ts.map +1 -0
  14. package/dist/format.js +226 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +8 -0
  18. package/dist/jsonCodecs.d.ts +9 -0
  19. package/dist/jsonCodecs.d.ts.map +1 -0
  20. package/dist/jsonCodecs.js +46 -0
  21. package/dist/kind.d.ts +126 -0
  22. package/dist/kind.d.ts.map +1 -0
  23. package/dist/kind.js +118 -0
  24. package/dist/rate.d.ts +58 -0
  25. package/dist/rate.d.ts.map +1 -0
  26. package/dist/rate.js +178 -0
  27. package/dist/rational.d.ts +49 -0
  28. package/dist/rational.d.ts.map +1 -0
  29. package/dist/rational.js +378 -0
  30. package/dist/segmenting.d.ts +15 -0
  31. package/dist/segmenting.d.ts.map +1 -0
  32. package/dist/segmenting.js +97 -0
  33. package/dist/unitSpecs.d.ts +47 -0
  34. package/dist/unitSpecs.d.ts.map +1 -0
  35. package/dist/unitSpecs.js +8 -0
  36. package/package.json +56 -0
  37. package/src/aggregating.ts +160 -0
  38. package/src/allocating.ts +70 -0
  39. package/src/amount.ts +291 -0
  40. package/src/format.ts +330 -0
  41. package/src/index.ts +8 -0
  42. package/src/jsonCodecs.ts +63 -0
  43. package/src/kind.ts +442 -0
  44. package/src/rate.ts +316 -0
  45. package/src/rational.ts +471 -0
  46. package/src/segmenting.ts +154 -0
  47. package/src/unitSpecs.ts +37 -0
package/src/kind.ts ADDED
@@ -0,0 +1,442 @@
1
+ import type { Opts, RoArray, RoNeTuple, RoPair, Brand,
2
+ If, IsAny, IsNever, Extends, OptionalArg } from "@onrail-xyz/utils";
3
+ import { omit, fromEntries, pick, isArray } from "@onrail-xyz/utils";
4
+ import { type Rationalish, Rational } from "./rational.js";
5
+ import { findUnit, segment, symbolKey } from "./segmenting.js";
6
+
7
+ // ---- Public API types ----
8
+
9
+ export type SymbolSpec = Readonly<{
10
+ symbol: string;
11
+ plural?: string;
12
+ position?: "postfix" | "prefix";
13
+ spacing?: "spaced" | "compact";
14
+ }>;
15
+
16
+ export type DecimalSpec<N extends number = number> =
17
+ { readonly oom: N; readonly scale?: never };
18
+
19
+ export type ScaleSpec<N extends Rationalish = Rationalish> =
20
+ { readonly oom?: never; readonly scale: N };
21
+
22
+ export type Unit = SymbolSpec & { readonly scale: Rational; readonly oom?: number };
23
+
24
+ export type SystemInfo<
25
+ N extends string = string,
26
+ U extends string = string,
27
+ D extends boolean = boolean,
28
+ > = { name: N; symbols: U; decimal: D };
29
+
30
+ export type StandardInfo<
31
+ SY extends string = string,
32
+ SU extends string = string,
33
+ > = { system: SY; unit: SU };
34
+
35
+ export type ValidStandardInfo<Y extends SystemInfo> =
36
+ Y extends SystemInfo<infer N, infer U, boolean> ? StandardInfo<N, U> : never;
37
+
38
+ export type Kind<
39
+ U extends string = string,
40
+ N extends string = string,
41
+ Y extends SystemInfo = SystemInfo,
42
+ S extends ValidStandardInfo<Y> = ValidStandardInfo<Y>,
43
+ H extends U | undefined = U | undefined,
44
+ A extends U | undefined = U | undefined,
45
+ > = {
46
+ readonly name: N;
47
+ readonly units: KindUnits<U>; //guaranteed to be in descending order by scale
48
+ readonly standard: S;
49
+ readonly systems: KindSystems<Y>;
50
+ } & (
51
+ [U | undefined] extends [H]
52
+ ? { readonly human?: U }
53
+ : [H] extends [U]
54
+ ? { readonly human: H }
55
+ : [H] extends [undefined]
56
+ ? unknown
57
+ : { readonly human?: Exclude<H, undefined> }
58
+ ) & (
59
+ [U | undefined] extends [A]
60
+ ? { readonly atomic?: U }
61
+ : [A] extends [U]
62
+ ? { readonly atomic: A }
63
+ : [A] extends [undefined]
64
+ ? unknown
65
+ : { readonly atomic?: Exclude<A, undefined> }
66
+ );
67
+
68
+ export type KindWithHuman<
69
+ U extends string = string,
70
+ N extends string = string,
71
+ Y extends SystemInfo = SystemInfo,
72
+ S extends ValidStandardInfo<Y> = ValidStandardInfo<Y>,
73
+ H extends U = U,
74
+ A extends U | undefined = U | undefined,
75
+ > = Kind<U, N, Y, S, H, A>;
76
+
77
+ export type KindWithAtomic<
78
+ U extends string = string,
79
+ N extends string = string,
80
+ Y extends SystemInfo = SystemInfo,
81
+ S extends ValidStandardInfo<Y> = ValidStandardInfo<Y>,
82
+ H extends U | undefined = U | undefined,
83
+ A extends U = U,
84
+ > = Kind<U, N, Y, S, H, A>;
85
+
86
+ export type KindWithHumanAndAtomic<
87
+ U extends string = string,
88
+ N extends string = string,
89
+ Y extends SystemInfo = SystemInfo,
90
+ S extends ValidStandardInfo<Y> = ValidStandardInfo<Y>,
91
+ H extends U = U,
92
+ A extends U = U,
93
+ > = Kind<U, N, Y, S, H, A>;
94
+
95
+ export type KindWithDecimalHumanAndAtomic<
96
+ U extends string = string,
97
+ N extends string = string,
98
+ Y extends SystemInfo = SystemInfo,
99
+ S extends ValidStandardInfo<Y> = ValidStandardInfo<Y>,
100
+ H extends U = U,
101
+ A extends U = U,
102
+ > = Brand<Kind<U, N, Y, S, H, A>, "decimalHuman" | "decimalAtomic">;
103
+
104
+ export type KindUnitSymbols<K extends Kind> = Extract<keyof K["units"], string>;
105
+
106
+ type MetaSymbolsOf<K extends Kind> =
107
+ "standard" |
108
+ (K["human"] extends string ? "human" : never) |
109
+ (K["atomic"] extends string ? "atomic" : never);
110
+
111
+ export type SymbolsOf<K extends Kind> =
112
+ If<IsAny<K>,
113
+ string,
114
+ If<Extends<string, KindUnitSymbols<K>>,
115
+ If<IsNever<("human" | "atomic") & MetaSymbolsOf<K>>,
116
+ string,
117
+ never
118
+ >,
119
+ KindUnitSymbols<K>
120
+ >
121
+ > | MetaSymbolsOf<K>;
122
+
123
+ export type DecimalSymbolsOf<K extends Kind> = DecimalUnitSymbols<K> | DecimalMetaSymbolOf<K>;
124
+
125
+ export type ResolvedSymbolOf<K extends Kind, M extends SymbolsOf<K>> =
126
+ M extends "standard" ? K["standard"]["unit"] :
127
+ M extends "human" ? K["human"] :
128
+ M extends "atomic" ? K["atomic"] :
129
+ M;
130
+
131
+ export type IsBareKind<K extends Kind> = Extends<string, K["name"]>;
132
+
133
+ export type CandidateKinds<K extends Kind> = If<IsBareKind<K>, Kind, K>;
134
+
135
+ export function sameKind(a: Kind, b: Kind): boolean {
136
+ return a === b || a.name === b.name;
137
+ }
138
+
139
+ export function getUnit<
140
+ const K extends Kind,
141
+ S extends SymbolsOf<K>,
142
+ >(kind: K, unitSymbol: S): Unit {
143
+ const resolved =
144
+ unitSymbol === "standard" ? kind.standard.unit :
145
+ unitSymbol === "human" ? kind.human :
146
+ unitSymbol === "atomic" ? kind.atomic :
147
+ unitSymbol;
148
+
149
+ const unit = resolved === undefined ? undefined : kind.units[resolved];
150
+ if (!unit)
151
+ throw new Error(`Kind ${kind.name} has no unit ${unitSymbol}`);
152
+
153
+ return unit;
154
+ }
155
+
156
+ export function getDecimals<const K extends KindWithDecimalHumanAndAtomic>(
157
+ kind: K,
158
+ opts?: GetDecimalsOpts<K>,
159
+ ): number;
160
+ export function getDecimals<const K extends Kind>(
161
+ kind: K,
162
+ opts: GetDecimalsOpts<K>,
163
+ ): number;
164
+ export function getDecimals(
165
+ kind: Kind,
166
+ opts?: Opts<{ of: string; in: string }>,
167
+ ): number {
168
+ const { of: ofSymbol = "human", in: inSymbol = "atomic" } = opts ?? {};
169
+ const oomOf = (symbol: string) => {
170
+ const { oom } = getUnit(kind, symbol);
171
+ if (oom === undefined)
172
+ throw new Error(`Unit ${symbol} of kind ${kind.name} is not decimal`);
173
+
174
+ return oom;
175
+ };
176
+
177
+ return oomOf(ofSymbol) - oomOf(inSymbol);
178
+ }
179
+
180
+ type MetaBrandTags<H, A, S, I extends KindUnitsInput> =
181
+ [H, "human"] | [A, "atomic"] | [S, "standard"] extends infer M
182
+ ? M extends [DecimalSymbolsOfSystem<SystemInfoOf<I>>, MetaSymbols]
183
+ ? `decimal${Capitalize<M[1]>}`
184
+ : ""
185
+ : never;
186
+
187
+ type KindOpts<I extends KindUnitsInput> = Opts<{
188
+ human: UnitSymbolsOf<UnitsSpecOf<I>>;
189
+ atomic: UnitSymbolsOf<UnitsSpecOf<I>>;
190
+ }>;
191
+ //a meta symbol read off the whole record: one that may be undefined - or absent, where the
192
+ // record's own type declares it optional - may be unset on the kind
193
+ type MetaOf<O, K extends "human" | "atomic"> = K extends keyof O ? O[K] : undefined;
194
+
195
+ export function kind<
196
+ N extends string,
197
+ const I extends KindUnitsInput,
198
+ const O extends KindOpts<I> = {},
199
+ >(name: N,
200
+ unitsInput: I,
201
+ ...[opts]: OptionalArg<O>
202
+ ): Brand<
203
+ Kind<
204
+ UnitSymbolsOf<UnitsSpecOf<I>>,
205
+ N,
206
+ SystemInfoOf<I>,
207
+ StandardInfoOf<I>,
208
+ MetaOf<O, "human">,
209
+ MetaOf<O, "atomic">
210
+ >,
211
+ MetaBrandTags<MetaOf<O, "human">, MetaOf<O, "atomic">, StandardInfoOf<I>["unit"], I>
212
+ > {
213
+ const isSystemsSpec = isArray(unitsInput) && isArray(unitsInput[0]);
214
+ const systemEntries = (
215
+ isSystemsSpec ? unitsInput : [["default", unitsInput]]
216
+ ) as RoArray<SystemEntry>;
217
+
218
+ const firstSpec = systemEntries[0]![1];
219
+ const standard = {
220
+ system: systemEntries[0]![0],
221
+ unit: firstSpec[0]!.symbols[0]!.symbol,
222
+ } as const;
223
+ const meta = pick<KindOpts<I>, ["human", "atomic"]>(opts ?? {}, ["human", "atomic"]);
224
+
225
+ const allUnits: [string, Unit][] = [];
226
+ const systems: Record<string, { symbols: string[]; decimal: boolean }> = {};
227
+
228
+ for (let i = 0; i < systemEntries.length; ++i) {
229
+ const [sysName, unitsSpec] = systemEntries[i]!;
230
+ if (displayModes.some(mode => mode === sysName))
231
+ throw new Error(`"${sysName}" is a display mode and cannot name a system`);
232
+
233
+ //mirrors IsSystemDecimal: an explicit oom/scale on the first unit decides; a bare first
234
+ // unit defers to the second, and a lone bare unit counts as decimal
235
+ const sysDecimal = i === 0
236
+ ? "oom" in firstSpec[0]!
237
+ ? true
238
+ : "scale" in firstSpec[0]!
239
+ ? false
240
+ : firstSpec.length === 1 || "oom" in firstSpec[1]!
241
+ : unitsSpec.every(u => "oom" in u);
242
+
243
+ if (Object.hasOwn(systems, sysName))
244
+ throw new Error(`Duplicate system "${sysName}"`);
245
+
246
+ systems[sysName] = { symbols: [], decimal: sysDecimal };
247
+
248
+ const processed = unitsSpec
249
+ .map(u => ({ ...u, ...ensureScale(sysDecimal, u) }) as UnitBaseSpec & { scale: Rational })
250
+ .sort((a, b) => b.scale.gt(a.scale) ? 1 : b.scale.lt(a.scale) ? -1 : 0);
251
+
252
+ for (const unit of processed) {
253
+ if (unit.scale.sign() !== 1)
254
+ throw new Error(`Unit scales must be positive, got ${unit.scale.toString()}`);
255
+
256
+ const magScale = omit(unit, "symbols");
257
+ for (const symbolSpec of unit.symbols) {
258
+ const unitData = { ...symbolSpec, ...magScale };
259
+ const symbols = [
260
+ symbolSpec.symbol,
261
+ ...(symbolSpec.plural !== undefined ? [symbolSpec.plural] : []),
262
+ ];
263
+ for (const symbol of symbols) {
264
+ if (metaSymbols.some(meta => meta === symbol))
265
+ throw new Error(`"${symbol}" is a reserved symbol and cannot name a unit`);
266
+
267
+ //parsing tokenizes on these characters (and takes a leading "-" for the sign), so a
268
+ // symbol containing them could never round-trip through its own kind. Digits end a
269
+ // value, so no symbol may start with one, and a prefix symbol (which the value follows,
270
+ // as in "$100") may hold none; a postfix one ends at the next space regardless
271
+ const prefix = (symbolSpec.position ?? "postfix") === "prefix";
272
+ if (symbol === "" || /[\s,_./]/u.test(symbol) || symbol.startsWith("-") ||
273
+ /^[0-9]/.test(symbol) || (prefix && /[0-9]/.test(symbol)))
274
+ throw new Error(
275
+ `Invalid unit symbol "${symbol}": symbols must be non-empty and must not ` +
276
+ `contain whitespace, ",", "_", ".", "/", a leading "-" or a leading digit ` +
277
+ `(nor any digit when prefixed)`
278
+ );
279
+
280
+ if (systems[sysName]!.symbols.includes(symbol))
281
+ throw new Error(`Duplicate symbol "${symbol}" in system ${sysName}`);
282
+
283
+ const existing = allUnits.find(([s]) => symbolKey(s) === symbolKey(symbol));
284
+ if (existing && existing[0] !== symbol)
285
+ throw new Error(`Symbol "${symbol}" is equivalent to "${existing[0]}" (NFKC)`);
286
+
287
+ systems[sysName]!.symbols.push(symbol);
288
+ if (!existing)
289
+ allUnits.push([symbol, unitData as Unit]);
290
+ else if (!existing[1].scale.eq(unitData.scale))
291
+ throw new Error(`Symbol "${symbol}" has conflicting scales across systems`);
292
+ else
293
+ existing[1] = { ...unitData, ...existing[1] } as Unit;
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ //descending by scale: a promise to consumers walking `units`, which the type states too
300
+ allUnits.sort((a, b) => b[1].scale.gt(a[1].scale) ? 1 : b[1].scale.lt(a[1].scale) ? -1 : 0);
301
+ const units = fromEntries(allUnits);
302
+
303
+ return { name, units, standard, systems, ...meta } as any;
304
+ }
305
+
306
+ export function identifyKind<const K extends Kind, A extends boolean = false>(
307
+ kinds: RoArray<K>,
308
+ str: string,
309
+ allowAmbiguous?: A
310
+ ): A extends true ? K[] : K | undefined {
311
+ const { pairs } = segment(str);
312
+ const symbols = pairs.map(p => str.substring(p.symbol[0], p.symbol[1]));
313
+
314
+ const matches = kinds.filter(k => symbols.every(s => findUnit(k.units, s) !== undefined));
315
+
316
+ return (allowAmbiguous ? matches : matches.length === 1 ? matches[0] : undefined) as any;
317
+ }
318
+
319
+ // ---- Implementation details ----
320
+
321
+ type UnitBaseSpec = { readonly symbols: RoNeTuple<SymbolSpec> };
322
+
323
+ type UnitSpec<F extends boolean, D extends boolean> =
324
+ UnitBaseSpec & (
325
+ F extends true
326
+ ? D extends true
327
+ ? Partial<DecimalSpec<0>>
328
+ : Partial<ScaleSpec<1 | 1n>>
329
+ : D extends true ? DecimalSpec : ScaleSpec
330
+ );
331
+
332
+ type KindUnitsSpec<F extends boolean, D extends boolean> =
333
+ D extends any
334
+ ? F extends true
335
+ ? readonly [UnitSpec<true, D>, ...UnitSpec<false, D>[]]
336
+ : RoNeTuple<UnitSpec<false, D>>
337
+ : never;
338
+
339
+ type KindUnits<U extends string> = { readonly [K in U]?: Unit };
340
+
341
+ type KindSystems<Y extends SystemInfo> = {
342
+ readonly [Sys in Y as Sys["name"]]: Readonly<{
343
+ symbols: RoArray<Sys["symbols"]>; //guaranteed to be in descending order by scale
344
+ decimal: Sys["decimal"];
345
+ }>;
346
+ };
347
+
348
+ type UnitSymbolsOf<U extends RoArray<UnitBaseSpec>> =
349
+ U[number]["symbols"][number] extends infer S
350
+ ? S extends SymbolSpec
351
+ ? S["symbol"] | Extract<S["plural"], string>
352
+ : never
353
+ : never;
354
+
355
+ type SystemEntry<
356
+ F extends boolean = boolean,
357
+ N extends string = string,
358
+ S extends KindUnitsSpec<F, boolean> = KindUnitsSpec<F, boolean>,
359
+ > = RoPair<N, S>;
360
+
361
+ type SystemsSpec = readonly [SystemEntry<true>, ...SystemEntry<false>[]];
362
+
363
+ type KindUnitsInput = KindUnitsSpec<true, boolean> | SystemsSpec;
364
+
365
+ type IsSystemDecimal<Spec extends KindUnitsSpec<boolean, boolean>> =
366
+ Spec[0] extends DecimalSpec
367
+ ? true
368
+ : Spec[0] extends ScaleSpec
369
+ ? false
370
+ : Spec extends readonly [unknown] | readonly [unknown, DecimalSpec, ...unknown[]]
371
+ ? true
372
+ : false;
373
+
374
+ type SystemInfoOf<I extends KindUnitsInput> =
375
+ I extends SystemsSpec
376
+ ? { [K in keyof I & `${number}`]:
377
+ I[K] extends SystemEntry<boolean, infer Name, infer Spec>
378
+ ? SystemInfo<Name, UnitSymbolsOf<Spec>, IsSystemDecimal<Spec>>
379
+ : never
380
+ }[keyof I & `${number}`]
381
+ : I extends KindUnitsSpec<true, boolean>
382
+ ? SystemInfo<"default", UnitSymbolsOf<I>, IsSystemDecimal<I>>
383
+ : never;
384
+
385
+ type UnitsSpecOf<I extends KindUnitsInput> =
386
+ I extends SystemsSpec ? I[number][1] : I;
387
+
388
+ type StandardSystemOf<I extends KindUnitsInput> =
389
+ I extends SystemsSpec ? I[0][0] : "default";
390
+
391
+ type StandardUnitOf<I extends KindUnitsInput> =
392
+ I extends SystemsSpec
393
+ ? I[0][1][0]["symbols"][0]["symbol"]
394
+ : I extends KindUnitsSpec<boolean, boolean>
395
+ ? I[0]["symbols"][0]["symbol"]
396
+ : never;
397
+
398
+ type StandardInfoOf<I extends KindUnitsInput> =
399
+ StandardInfo<StandardSystemOf<I>, StandardUnitOf<I>> extends infer S
400
+ extends ValidStandardInfo<SystemInfoOf<I>> //tell tsc that this must hold
401
+ ? S
402
+ : never;
403
+
404
+ const metaSymbols = ["standard", "human", "atomic"] as const;
405
+ //Amount.toString takes a system name in the position of its mode argument, so a system named
406
+ // like a mode could never be selected
407
+ const displayModes = ["approximate", "exact", "inUnit"] as const;
408
+ type MetaSymbols = typeof metaSymbols[number];
409
+ type DecimalMetaSymbolOf<K extends Kind, M extends MetaSymbols = MetaSymbols> =
410
+ M extends MetaSymbols
411
+ ? K extends Brand<unknown, `decimal${Capitalize<M>}`>
412
+ ? M
413
+ : never
414
+ : never;
415
+
416
+ type DecimalSymbolsOfSystem<Y extends SystemInfo> =
417
+ Y extends { symbols: infer Syms extends string; decimal: true }
418
+ ? Syms
419
+ : never;
420
+
421
+ type DecimalUnitSymbols<K extends Kind> =
422
+ K["systems"][keyof K["systems"]] extends infer Sys
423
+ ? Sys extends { symbols: RoArray<infer S extends string>; decimal: true } ? S : never
424
+ : never;
425
+
426
+ type GetDecimalsOpts<K extends Kind> =
427
+ DecimalSymbolsOf<K> extends infer D
428
+ ? ("human" extends D ? Opts<{ of: D }> : { readonly of: D }) &
429
+ ("atomic" extends D ? Opts<{ in: D }> : { readonly in: D })
430
+ : never;
431
+
432
+ const addScale = (spec: DecimalSpec) =>
433
+ ({ oom: spec.oom, scale: Rational.powerOfTen(spec.oom) });
434
+
435
+ const ensureScale = (decimal: boolean, spec: UnitBaseSpec) =>
436
+ "oom" in spec
437
+ ? addScale(spec as DecimalSpec)
438
+ : "scale" in spec
439
+ ? { scale: Rational.from((spec as ScaleSpec).scale) } as const
440
+ : decimal
441
+ ? { oom: 0, scale: Rational.from(1n) } as const
442
+ : { scale: Rational.from(1n) } as const;