type safety

This commit is contained in:
Florian Dold 2019-12-14 18:46:42 +01:00
parent 690bbfcfd8
commit a4a9b16153
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
2 changed files with 23 additions and 40 deletions

View File

@ -14,17 +14,15 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
/** /**
* Types and helper functions for dealing with Taler amounts. * Types and helper functions for dealing with Taler amounts.
*/ */
/** /**
* Imports. * Imports.
*/ */
import { Checkable } from "./checkable"; import { Checkable } from "./checkable";
import { objectCodec, numberCodec, stringCodec, Codec } from "./codec";
/** /**
* Number of fractional units that one value unit represents. * Number of fractional units that one value unit represents.
@ -43,38 +41,31 @@ export const fractionalLength = 8;
*/ */
export const maxAmountValue = 2 ** 52; export const maxAmountValue = 2 ** 52;
/** /**
* Non-negative financial amount. Fractional values are expressed as multiples * Non-negative financial amount. Fractional values are expressed as multiples
* of 1e-8. * of 1e-8.
*/ */
@Checkable.Class() export interface AmountJson {
export class AmountJson {
/** /**
* Value, must be an integer. * Value, must be an integer.
*/ */
@Checkable.Number()
readonly value: number; readonly value: number;
/** /**
* Fraction, must be an integer. Represent 1/1e8 of a unit. * Fraction, must be an integer. Represent 1/1e8 of a unit.
*/ */
@Checkable.Number()
readonly fraction: number; readonly fraction: number;
/** /**
* Currency of the amount. * Currency of the amount.
*/ */
@Checkable.String()
readonly currency: string; readonly currency: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => AmountJson;
} }
const amountJsonCodec: Codec<AmountJson> = objectCodec<AmountJson>()
.property("value", numberCodec)
.property("currency", stringCodec)
.build<AmountJson>("AmountJson");
/** /**
* Result of a possibly overflowing operation. * Result of a possibly overflowing operation.
@ -90,7 +81,6 @@ export interface Result {
saturated: boolean; saturated: boolean;
} }
/** /**
* Get an amount that represents zero units of a currency. * Get an amount that represents zero units of a currency.
*/ */
@ -102,7 +92,6 @@ export function getZero(currency: string): AmountJson {
}; };
} }
export function sum(amounts: AmountJson[]) { export function sum(amounts: AmountJson[]) {
if (amounts.length <= 0) { if (amounts.length <= 0) {
throw Error("can't sum zero amounts"); throw Error("can't sum zero amounts");
@ -110,7 +99,6 @@ export function sum(amounts: AmountJson[]) {
return add(amounts[0], ...amounts.slice(1)); return add(amounts[0], ...amounts.slice(1));
} }
/** /**
* Add two amounts. Return the result and whether * Add two amounts. Return the result and whether
* the addition overflowed. The overflow is always handled * the addition overflowed. The overflow is always handled
@ -124,7 +112,7 @@ export function add(first: AmountJson, ...rest: AmountJson[]): Result {
if (value > maxAmountValue) { if (value > maxAmountValue) {
return { return {
amount: { currency, value: maxAmountValue, fraction: fractionalBase - 1 }, amount: { currency, value: maxAmountValue, fraction: fractionalBase - 1 },
saturated: true saturated: true,
}; };
} }
let fraction = first.fraction % fractionalBase; let fraction = first.fraction % fractionalBase;
@ -133,19 +121,23 @@ export function add(first: AmountJson, ...rest: AmountJson[]): Result {
throw Error(`Mismatched currency: ${x.currency} and ${currency}`); throw Error(`Mismatched currency: ${x.currency} and ${currency}`);
} }
value = value + x.value + Math.floor((fraction + x.fraction) / fractionalBase); value =
value + x.value + Math.floor((fraction + x.fraction) / fractionalBase);
fraction = Math.floor((fraction + x.fraction) % fractionalBase); fraction = Math.floor((fraction + x.fraction) % fractionalBase);
if (value > maxAmountValue) { if (value > maxAmountValue) {
return { return {
amount: { currency, value: maxAmountValue, fraction: fractionalBase - 1 }, amount: {
saturated: true currency,
value: maxAmountValue,
fraction: fractionalBase - 1,
},
saturated: true,
}; };
} }
} }
return { amount: { currency, value, fraction }, saturated: false }; return { amount: { currency, value, fraction }, saturated: false };
} }
/** /**
* Subtract two amounts. Return the result and whether * Subtract two amounts. Return the result and whether
* the subtraction overflowed. The overflow is always handled * the subtraction overflowed. The overflow is always handled
@ -180,7 +172,6 @@ export function sub(a: AmountJson, ...rest: AmountJson[]): Result {
return { amount: { currency, value, fraction }, saturated: false }; return { amount: { currency, value, fraction }, saturated: false };
} }
/** /**
* Compare two amounts. Returns 0 when equal, -1 when a < b * Compare two amounts. Returns 0 when equal, -1 when a < b
* and +1 when a > b. Throws when currencies don't match. * and +1 when a > b. Throws when currencies don't match.
@ -209,7 +200,6 @@ export function cmp(a: AmountJson, b: AmountJson): number {
} }
} }
/** /**
* Create a copy of an amount. * Create a copy of an amount.
*/ */
@ -221,7 +211,6 @@ export function copy(a: AmountJson): AmountJson {
}; };
} }
/** /**
* Divide an amount. Throws on division by zero. * Divide an amount. Throws on division by zero.
*/ */
@ -230,17 +219,16 @@ export function divide(a: AmountJson, n: number): AmountJson {
throw Error(`Division by 0`); throw Error(`Division by 0`);
} }
if (n === 1) { if (n === 1) {
return {value: a.value, fraction: a.fraction, currency: a.currency}; return { value: a.value, fraction: a.fraction, currency: a.currency };
} }
const r = a.value % n; const r = a.value % n;
return { return {
currency: a.currency, currency: a.currency,
fraction: Math.floor(((r * fractionalBase) + a.fraction) / n), fraction: Math.floor((r * fractionalBase + a.fraction) / n),
value: Math.floor(a.value / n), value: Math.floor(a.value / n),
}; };
} }
/** /**
* Check if an amount is non-zero. * Check if an amount is non-zero.
*/ */
@ -248,11 +236,10 @@ export function isNonZero(a: AmountJson): boolean {
return a.value > 0 || a.fraction > 0; return a.value > 0 || a.fraction > 0;
} }
/** /**
* Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct. * Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
*/ */
export function parse(s: string): AmountJson|undefined { export function parse(s: string): AmountJson | undefined {
const res = s.match(/^([a-zA-Z0-9_*-]+):([0-9]+)([.][0-9]+)?$/); const res = s.match(/^([a-zA-Z0-9_*-]+):([0-9]+)([.][0-9]+)?$/);
if (!res) { if (!res) {
return undefined; return undefined;
@ -272,7 +259,6 @@ export function parse(s: string): AmountJson|undefined {
}; };
} }
/** /**
* Parse amount in standard string form (like 'EUR:20.5'), * Parse amount in standard string form (like 'EUR:20.5'),
* throw if the input is not a valid amount. * throw if the input is not a valid amount.
@ -285,7 +271,6 @@ export function parseOrThrow(s: string): AmountJson {
return res; return res;
} }
/** /**
* Convert a float to a Taler amount. * Convert a float to a Taler amount.
* Loss of precision possible. * Loss of precision possible.
@ -298,7 +283,6 @@ export function fromFloat(floatVal: number, currency: string) {
}; };
} }
/** /**
* Convert to standard human-readable string representation that's * Convert to standard human-readable string representation that's
* also used in JSON formats. * also used in JSON formats.
@ -306,7 +290,7 @@ export function fromFloat(floatVal: number, currency: string) {
export function toString(a: AmountJson): string { export function toString(a: AmountJson): string {
const av = a.value + Math.floor(a.fraction / fractionalBase); const av = a.value + Math.floor(a.fraction / fractionalBase);
const af = a.fraction % fractionalBase; const af = a.fraction % fractionalBase;
let s = av.toString() let s = av.toString();
if (af) { if (af) {
s = s + "."; s = s + ".";
@ -315,7 +299,7 @@ export function toString(a: AmountJson): string {
if (!n) { if (!n) {
break; break;
} }
s = s + Math.floor(n / fractionalBase * 10).toString(); s = s + Math.floor((n / fractionalBase) * 10).toString();
n = (n * 10) % fractionalBase; n = (n * 10) % fractionalBase;
} }
} }
@ -323,7 +307,6 @@ export function toString(a: AmountJson): string {
return `${a.currency}:${s}`; return `${a.currency}:${s}`;
} }
/** /**
* Check if the argument is a valid amount in string form. * Check if the argument is a valid amount in string form.
*/ */

View File

@ -94,10 +94,10 @@ class ObjectCodecBuilder<T, TC> {
* @param objectDisplayName name of the object that this codec operates on, * @param objectDisplayName name of the object that this codec operates on,
* used in error messages. * used in error messages.
*/ */
build<R extends (TC & T)>(objectDisplayName: string): Codec<R> { build(objectDisplayName: string): Codec<TC> {
const propList = this.propList; const propList = this.propList;
return { return {
decode(x: any, c?: Context): R { decode(x: any, c?: Context): TC {
if (!c) { if (!c) {
c = { c = {
path: [`(${objectDisplayName})`], path: [`(${objectDisplayName})`],
@ -112,7 +112,7 @@ class ObjectCodecBuilder<T, TC> {
); );
obj[prop.name] = propVal; obj[prop.name] = propVal;
} }
return obj as R; return obj as TC;
}, },
}; };
} }