@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/format.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { Opts, RoArray } from "@onrail-xyz/utils";
|
|
2
|
+
import { type ToFixedOptions, Rational } from "./rational.js";
|
|
3
|
+
import type { Kind, Unit, KindUnitSymbols } from "./kind.js";
|
|
4
|
+
import { type SegmentPair, findUnit, segment } from "./segmenting.js";
|
|
5
|
+
|
|
6
|
+
const approxDigits = 3;
|
|
7
|
+
|
|
8
|
+
export function inUnit<K extends Kind>(
|
|
9
|
+
kind: K,
|
|
10
|
+
stdVal: Rational,
|
|
11
|
+
symbol: KindUnitSymbols<K>,
|
|
12
|
+
precision: number | KindUnitSymbols<K> = 0,
|
|
13
|
+
opts?: Opts<ToFixedOptions>,
|
|
14
|
+
): string {
|
|
15
|
+
const unit = kind.units[symbol]!;
|
|
16
|
+
const value = stdVal.div(unit.scale);
|
|
17
|
+
const decimals = typeof precision === "number"
|
|
18
|
+
? precision
|
|
19
|
+
: unit.oom! - kind.units[precision]!.oom!;
|
|
20
|
+
|
|
21
|
+
if (decimals < 0)
|
|
22
|
+
throw new Error("Precision must be non-negative");
|
|
23
|
+
|
|
24
|
+
return formatWithSymbol(value, decimals, unit, opts);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function inUnitApprox<K extends Kind>(
|
|
28
|
+
kind: K,
|
|
29
|
+
stdVal: Rational,
|
|
30
|
+
symbol: KindUnitSymbols<K>,
|
|
31
|
+
opts?: Opts<ToFixedOptions>,
|
|
32
|
+
): string {
|
|
33
|
+
const unit = kind.units[symbol]!;
|
|
34
|
+
const value = stdVal.div(unit.scale);
|
|
35
|
+
|
|
36
|
+
return formatWithSymbol(value, sigDigitPrec(value), unit, opts);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function approximate(
|
|
40
|
+
kind: Kind,
|
|
41
|
+
stdVal: Rational,
|
|
42
|
+
opts?: Opts<ToFixedOptions & { system: string }>,
|
|
43
|
+
): string {
|
|
44
|
+
const sortedByScale = getSystemUnits(kind, opts?.system);
|
|
45
|
+
if (!isDecimal(kind, opts?.system))
|
|
46
|
+
return compound(kind, sortedByScale, stdVal, true, opts);
|
|
47
|
+
|
|
48
|
+
if (stdVal.eq(0))
|
|
49
|
+
return formatZero(kind, sortedByScale, opts);
|
|
50
|
+
|
|
51
|
+
const reading = approxReading(stdVal, sortedByScale);
|
|
52
|
+
//a reading that rounds up into the next unit's range renders there instead: $0.9996 reads $1,
|
|
53
|
+
// not 100c
|
|
54
|
+
const rounded = roundTo(reading.value, reading.precision).mul(reading.unit.scale);
|
|
55
|
+
const next = sortedByScale[reading.index - 1];
|
|
56
|
+
const final = reading.index > 0 && next !== undefined && rounded.abs().ge(next.scale)
|
|
57
|
+
? approxReading(rounded, sortedByScale)
|
|
58
|
+
: reading;
|
|
59
|
+
return formatWithSymbol(final.value, final.precision, final.unit, opts);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const approxReading = (stdVal: Rational, sortedByScale: RoArray<Unit>) => {
|
|
63
|
+
const { index, unit, value, promoted } = displayUnitInfo(stdVal, sortedByScale);
|
|
64
|
+
const precision = promoted ? approxDigits : getApproxPrec(index, value);
|
|
65
|
+
return { index, unit, value, precision };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//the value as a given number of decimals renders it (half away from zero, as toFixed does)
|
|
69
|
+
const roundTo = (value: Rational, decimals: number): Rational => {
|
|
70
|
+
const scale = Rational.powerOfTen(decimals);
|
|
71
|
+
return Rational.from(value.mul(scale).round()).div(scale);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function exact(
|
|
75
|
+
kind: Kind,
|
|
76
|
+
stdVal: Rational,
|
|
77
|
+
opts?: Opts<ToFixedOptions & { system: string }>,
|
|
78
|
+
): string {
|
|
79
|
+
const sortedByScale = getSystemUnits(kind, opts?.system);
|
|
80
|
+
if (!isDecimal(kind, opts?.system))
|
|
81
|
+
return compound(kind, sortedByScale, stdVal, false, opts);
|
|
82
|
+
|
|
83
|
+
if (stdVal.eq(0))
|
|
84
|
+
return formatZero(kind, sortedByScale, opts);
|
|
85
|
+
|
|
86
|
+
const { unit, value } = displayUnitInfo(stdVal, sortedByScale);
|
|
87
|
+
return renderExact(value, unit, opts);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parse(kind: Kind, str: string): Rational {
|
|
91
|
+
const processPair = (pair: SegmentPair) => {
|
|
92
|
+
const value = Rational.from(str.substring(pair.value[0], pair.value[1]));
|
|
93
|
+
const symbol = str.substring(pair.symbol[0], pair.symbol[1]);
|
|
94
|
+
const unit = findUnit(kind.units, symbol);
|
|
95
|
+
if (!unit)
|
|
96
|
+
throw new Error(`Unknown unit: ${symbol}`);
|
|
97
|
+
|
|
98
|
+
return { value, unit };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const { negative, pairs } = segment(str);
|
|
102
|
+
|
|
103
|
+
if (pairs.length === 1) {
|
|
104
|
+
const { value, unit } = processPair(pairs[0]!);
|
|
105
|
+
const absVal = value.mul(unit.scale);
|
|
106
|
+
return negative ? absVal.neg() : absVal;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let total = Rational.from(0n);
|
|
110
|
+
let lastScale: Rational | undefined = undefined;
|
|
111
|
+
for (let i = 0; i < pairs.length; ++i) {
|
|
112
|
+
const { value, unit } = processPair(pairs[i]!);
|
|
113
|
+
|
|
114
|
+
if (i !== pairs.length - 1 && !value.isInteger())
|
|
115
|
+
throw new Error("Decimal only allowed in final unit");
|
|
116
|
+
|
|
117
|
+
if (lastScale && lastScale.le(unit.scale))
|
|
118
|
+
throw new Error("Units must be in descending order");
|
|
119
|
+
|
|
120
|
+
total = total.add(value.mul(unit.scale));
|
|
121
|
+
lastScale = unit.scale;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return negative ? total.neg() : total;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function exactRate(
|
|
128
|
+
num: Kind,
|
|
129
|
+
den: Kind,
|
|
130
|
+
ratio: Rational,
|
|
131
|
+
opts?: Opts<ToFixedOptions>,
|
|
132
|
+
): string {
|
|
133
|
+
const denUnit = bestDenUnit(num, den, ratio);
|
|
134
|
+
return `${exact(num, ratio.mul(denUnit.scale), opts)}/${denUnit.symbol}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const bestDenUnit = (num: Kind, den: Kind, ratio: Rational): Unit => {
|
|
138
|
+
if (den.human !== undefined || ratio.eq(0))
|
|
139
|
+
return den.units[den.human ?? den.standard.unit]!;
|
|
140
|
+
|
|
141
|
+
let best: { mixed: boolean; size: number; unit: Unit } | undefined;
|
|
142
|
+
for (const unit of getSystemUnits(den)) {
|
|
143
|
+
const rendered = exact(num, ratio.mul(unit.scale), { thousandsSep: "" });
|
|
144
|
+
//symbols cannot contain "/", so its presence marks the mixed-number form
|
|
145
|
+
const mixed = rendered.includes("/");
|
|
146
|
+
const size = rendered.length + unit.symbol.length;
|
|
147
|
+
if (!best || (mixed === best.mixed ? size < best.size : best.mixed))
|
|
148
|
+
best = { mixed, size, unit };
|
|
149
|
+
}
|
|
150
|
+
return best!.unit;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const renderExact = (value: Rational, unit: Unit, opts?: Opts<ToFixedOptions>): string => {
|
|
154
|
+
const valStr = value.toString({ thousandsSep: opts?.thousandsSep ?? "," });
|
|
155
|
+
return attachSymbol(valStr, unit, valStr === "1" || valStr === "-1");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const compound = (
|
|
159
|
+
kind: Kind,
|
|
160
|
+
sortedByScale: Unit[],
|
|
161
|
+
stdVal: Rational,
|
|
162
|
+
approx: boolean,
|
|
163
|
+
opts?: Opts<ToFixedOptions>,
|
|
164
|
+
): string => {
|
|
165
|
+
if (stdVal.eq(0))
|
|
166
|
+
return formatZero(kind, sortedByScale, opts);
|
|
167
|
+
|
|
168
|
+
const sign = stdVal.lt(0) ? "-" : "";
|
|
169
|
+
const smallest = sortedByScale.length - 1;
|
|
170
|
+
let absStdVal = stdVal.abs();
|
|
171
|
+
|
|
172
|
+
if (approx) {
|
|
173
|
+
//a value the smallest unit carries on its own renders in it, to significant digits - unless
|
|
174
|
+
// those round up to the next unit, in which case it spans units after all
|
|
175
|
+
const { lbIndex, lbUnit, lbValue } = lowerBoundInfo(absStdVal, sortedByScale);
|
|
176
|
+
if (lbIndex === -1 || lbIndex === smallest) {
|
|
177
|
+
const precision = getApproxPrec(lbIndex, lbValue);
|
|
178
|
+
const rounded = roundTo(lbValue, precision).mul(lbUnit.scale);
|
|
179
|
+
if (smallest === 0 || lbIndex === -1 || rounded.lt(sortedByScale[smallest - 1]!.scale))
|
|
180
|
+
return sign + formatWithSymbol(lbValue, precision, lbUnit, opts);
|
|
181
|
+
|
|
182
|
+
absStdVal = rounded;
|
|
183
|
+
}
|
|
184
|
+
//rounded to whole smallest units first and decomposed exactly after, so no part can round
|
|
185
|
+
// up into the next unit's range (no "1 minute 60 seconds")
|
|
186
|
+
const smallestScale = sortedByScale[smallest]!.scale;
|
|
187
|
+
absStdVal = Rational.from(absStdVal.div(smallestScale).round()).mul(smallestScale);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parts: string[] = [];
|
|
191
|
+
const tolerance = approx ? absStdVal.div(Rational.powerOfTen(approxDigits)) : null;
|
|
192
|
+
let remainder = absStdVal;
|
|
193
|
+
let startFrom = 0;
|
|
194
|
+
|
|
195
|
+
while (remainder.ne(0)) {
|
|
196
|
+
const { lbIndex, lbUnit, lbValue } = lowerBoundInfo(remainder, sortedByScale, startFrom);
|
|
197
|
+
const atSmallest = lbIndex === -1 || lbIndex === smallest;
|
|
198
|
+
|
|
199
|
+
if (atSmallest && !approx)
|
|
200
|
+
parts.push(renderExact(lbValue, lbUnit, opts));
|
|
201
|
+
else
|
|
202
|
+
parts.push(formatWithSymbol(Rational.from(lbValue.floor()), 0, lbUnit, opts));
|
|
203
|
+
|
|
204
|
+
if (atSmallest)
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
const nextRemainder = remainder.mod(lbUnit.scale);
|
|
208
|
+
if (tolerance !== null && nextRemainder.lt(tolerance))
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
remainder = nextRemainder;
|
|
212
|
+
startFrom = lbIndex + 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return sign + parts.join(" ");
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const formatZero = (
|
|
219
|
+
kind: Kind,
|
|
220
|
+
sortedByScale: RoArray<Unit>,
|
|
221
|
+
opts?: Opts<ToFixedOptions>,
|
|
222
|
+
): string => {
|
|
223
|
+
const preferred = kind.units[kind.human ?? kind.standard.unit]!;
|
|
224
|
+
const unit = sortedByScale.find(u => u.scale.eq(preferred.scale)) ?? sortedByScale.at(-1)!;
|
|
225
|
+
return formatWithSymbol(Rational.from(0n), 0, unit, opts);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
function formatWithSymbol(
|
|
229
|
+
value: Rational,
|
|
230
|
+
decimals: number,
|
|
231
|
+
unit: Unit,
|
|
232
|
+
opts?: Opts<ToFixedOptions>,
|
|
233
|
+
): string {
|
|
234
|
+
const thousandsSep = opts?.thousandsSep ?? ",";
|
|
235
|
+
const trimZeros = opts?.trimZeros ?? true;
|
|
236
|
+
|
|
237
|
+
const valStr = value.toFixed(decimals, { thousandsSep, trimZeros });
|
|
238
|
+
//the plural follows the displayed magnitude, not the exact one (0.9999 at 2 decimals shows as 1)
|
|
239
|
+
const oneStr = Rational.from(1n).toFixed(decimals, { thousandsSep, trimZeros });
|
|
240
|
+
return attachSymbol(valStr, unit, valStr === oneStr || valStr === `-${oneStr}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const attachSymbol = (valStr: string, unit: Unit, singular: boolean): string => {
|
|
244
|
+
const sym = unit.plural && !singular ? unit.plural : unit.symbol;
|
|
245
|
+
const space = (unit.spacing ?? "spaced") === "spaced" ? " " : "";
|
|
246
|
+
|
|
247
|
+
if ((unit.position ?? "postfix") === "postfix")
|
|
248
|
+
return valStr + space + sym;
|
|
249
|
+
|
|
250
|
+
//the sign stays outside the symbol: parsing only takes a "-" that leads the whole pair, so an
|
|
251
|
+
// interior one ("$-100") would be read as part of the symbol
|
|
252
|
+
const [sign, digits] = valStr.startsWith("-") ? ["-", valStr.slice(1)] : ["", valStr];
|
|
253
|
+
return sign + sym + space + digits;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const getSystem = (kind: Kind, system?: string) => {
|
|
257
|
+
const name = system ?? kind.standard.system;
|
|
258
|
+
const sys = kind.systems[name];
|
|
259
|
+
if (!sys)
|
|
260
|
+
throw new Error(`Unknown system: ${name}`);
|
|
261
|
+
|
|
262
|
+
return sys;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const getSystemUnits = (kind: Kind, system?: string): Unit[] => {
|
|
266
|
+
const units: Unit[] = [];
|
|
267
|
+
for (const symbol of getSystem(kind, system).symbols) {
|
|
268
|
+
const unit = kind.units[symbol]!;
|
|
269
|
+
if (!units.some(u => u.scale.eq(unit.scale)))
|
|
270
|
+
units.push(unit);
|
|
271
|
+
}
|
|
272
|
+
return units;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const isDecimal = (kind: Kind, system?: string): boolean =>
|
|
276
|
+
getSystem(kind, system).decimal;
|
|
277
|
+
|
|
278
|
+
//number of decimals that yields approxDigits significant digits (0 for values >= 10^approxDigits)
|
|
279
|
+
const sigDigitPrec = (value: Rational): number => {
|
|
280
|
+
const absVal = value.abs();
|
|
281
|
+
let precision = approxDigits;
|
|
282
|
+
while (precision > 0 && absVal.ge(Rational.powerOfTen(approxDigits - precision)))
|
|
283
|
+
--precision;
|
|
284
|
+
|
|
285
|
+
while (absVal.ne(0) && absVal.lt(Rational.powerOfTen(approxDigits - precision - 1)))
|
|
286
|
+
++precision;
|
|
287
|
+
|
|
288
|
+
return precision;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
//below the smallest unit of the kind the precision is capped instead of following the value
|
|
292
|
+
const getApproxPrec = (lbIndex: number, lbValue: Rational): number =>
|
|
293
|
+
lbIndex === -1 ? approxDigits : sigDigitPrec(lbValue);
|
|
294
|
+
|
|
295
|
+
//the unit both display modes render in: the largest unit the value reaches, promoted to the next
|
|
296
|
+
// one up when the ladder gap is wide enough for the reading to run long (six orders — ¢/$ never
|
|
297
|
+
// promotes) and the promoted reading has at least a thousandth of it: 15,000 Gwei stays put,
|
|
298
|
+
// 0.5 ETH is not 500,000,000 Gwei
|
|
299
|
+
const displayUnitInfo = (stdVal: Rational, sortedByScale: RoArray<Unit>) => {
|
|
300
|
+
const { lbIndex, lbUnit, lbValue } = lowerBoundInfo(stdVal, sortedByScale);
|
|
301
|
+
const lb = { index: lbIndex, unit: lbUnit, value: lbValue, promoted: false } as const;
|
|
302
|
+
if (lbIndex <= 0)
|
|
303
|
+
return lb;
|
|
304
|
+
|
|
305
|
+
const unit = sortedByScale[lbIndex - 1]!;
|
|
306
|
+
const value = stdVal.div(unit.scale);
|
|
307
|
+
return unit.oom! - lbUnit.oom! >= 2 * approxDigits
|
|
308
|
+
&& value.abs().ge(Rational.powerOfTen(-approxDigits))
|
|
309
|
+
? { index: lbIndex - 1, unit, value, promoted: true } as const
|
|
310
|
+
: lb;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const lowerBoundInfo = (
|
|
314
|
+
stdVal: Rational,
|
|
315
|
+
sortedByScale: RoArray<Unit>,
|
|
316
|
+
startIndex: number = 0,
|
|
317
|
+
) => {
|
|
318
|
+
const absStdVal = stdVal.abs();
|
|
319
|
+
let lbIndex = startIndex;
|
|
320
|
+
for (; lbIndex < sortedByScale.length; ++lbIndex)
|
|
321
|
+
if (absStdVal.ge(sortedByScale[lbIndex]!.scale))
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
if (lbIndex === sortedByScale.length)
|
|
325
|
+
lbIndex = -1;
|
|
326
|
+
|
|
327
|
+
const lbUnit = sortedByScale.at(lbIndex)!;
|
|
328
|
+
const lbValue = stdVal.div(lbUnit.scale);
|
|
329
|
+
return { lbIndex, lbUnit, lbValue } as const;
|
|
330
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { JsonCodec, MaybeArray, RoArray, StrRecord } from "@onrail-xyz/utils";
|
|
2
|
+
import { isArray, invalidPayload, expectString } from "@onrail-xyz/utils";
|
|
3
|
+
import { Rational } from "./rational.js";
|
|
4
|
+
import type { Kind } from "./kind.js";
|
|
5
|
+
import { Amount, isAmount } from "./amount.js";
|
|
6
|
+
import { Rate, isRate } from "./rate.js";
|
|
7
|
+
|
|
8
|
+
const expectFields = <const F extends RoArray<string>>(
|
|
9
|
+
encoded: unknown,
|
|
10
|
+
tag: string,
|
|
11
|
+
fields: F,
|
|
12
|
+
): { [N in F[number]]: string } => {
|
|
13
|
+
if (typeof encoded !== "object" || encoded === null || isArray(encoded))
|
|
14
|
+
throw invalidPayload(encoded, tag);
|
|
15
|
+
|
|
16
|
+
const out: StrRecord<string> = {};
|
|
17
|
+
for (const field of fields) {
|
|
18
|
+
const value = (encoded as StrRecord)[field];
|
|
19
|
+
if (typeof value !== "string")
|
|
20
|
+
throw invalidPayload(encoded, tag);
|
|
21
|
+
|
|
22
|
+
out[field] = value;
|
|
23
|
+
}
|
|
24
|
+
return out as { [N in F[number]]: string };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const namedKind = <K extends Kind>(candidates: MaybeArray<K>, name: string, tag: string): K => {
|
|
28
|
+
const kind = (isArray(candidates) ? candidates : [candidates]).find(k => k.name === name);
|
|
29
|
+
if (!kind)
|
|
30
|
+
throw new Error(`No candidate kind named "${name}" for ${tag}`);
|
|
31
|
+
|
|
32
|
+
return kind;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const rationalCodec: JsonCodec<Rational> = {
|
|
36
|
+
tag: "Rational",
|
|
37
|
+
test: (value): value is Rational => value instanceof Rational,
|
|
38
|
+
encode: value => value.toString(),
|
|
39
|
+
decode: encoded => Rational.from(expectString(encoded, "Rational")),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const amountCodec = (kinds: MaybeArray<Kind>): JsonCodec<Amount<Kind>> => ({
|
|
43
|
+
tag: "Amount",
|
|
44
|
+
test: isAmount,
|
|
45
|
+
encode: value => ({ kind: value.kind.name, value: value.toJSON() }),
|
|
46
|
+
decode: encoded => {
|
|
47
|
+
const { kind, value } = expectFields(encoded, "Amount", ["kind", "value"]);
|
|
48
|
+
return Amount.parse(value, namedKind(kinds, kind, "Amount"));
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const rateCodec = (
|
|
53
|
+
numKinds: MaybeArray<Kind>,
|
|
54
|
+
denKinds: MaybeArray<Kind>,
|
|
55
|
+
): JsonCodec<Rate<Kind, Kind>> => ({
|
|
56
|
+
tag: "Rate",
|
|
57
|
+
test: isRate,
|
|
58
|
+
encode: value => ({ num: value.num.name, den: value.den.name, value: value.toJSON() }),
|
|
59
|
+
decode: encoded => {
|
|
60
|
+
const { num, den, value } = expectFields(encoded, "Rate", ["num", "den", "value"]);
|
|
61
|
+
return Rate.parse(value, namedKind(numKinds, num, "Rate"), namedKind(denKinds, den, "Rate"));
|
|
62
|
+
},
|
|
63
|
+
});
|