type safety
This commit is contained in:
parent
690bbfcfd8
commit
a4a9b16153
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user