amounts: more tests and closer behavior to reference impl
This commit is contained in:
parent
075fe28f74
commit
92843e3e92
@ -38,6 +38,11 @@ export const fractionalBase = 1e8;
|
|||||||
*/
|
*/
|
||||||
export const fractionalLength = 8;
|
export const fractionalLength = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum allowed value field of an amount.
|
||||||
|
*/
|
||||||
|
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
|
||||||
@ -86,18 +91,6 @@ export interface Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the largest amount that is safely representable.
|
|
||||||
*/
|
|
||||||
export function getMaxAmount(currency: string): AmountJson {
|
|
||||||
return {
|
|
||||||
currency,
|
|
||||||
fraction: 2 ** 32,
|
|
||||||
value: Number.MAX_SAFE_INTEGER,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an amount that represents zero units of a currency.
|
* Get an amount that represents zero units of a currency.
|
||||||
*/
|
*/
|
||||||
@ -120,8 +113,11 @@ export function getZero(currency: string): AmountJson {
|
|||||||
export function add(first: AmountJson, ...rest: AmountJson[]): Result {
|
export function add(first: AmountJson, ...rest: AmountJson[]): Result {
|
||||||
const currency = first.currency;
|
const currency = first.currency;
|
||||||
let value = first.value + Math.floor(first.fraction / fractionalBase);
|
let value = first.value + Math.floor(first.fraction / fractionalBase);
|
||||||
if (value > Number.MAX_SAFE_INTEGER) {
|
if (value > maxAmountValue) {
|
||||||
return { amount: getMaxAmount(currency), saturated: true };
|
return {
|
||||||
|
amount: { currency, value: maxAmountValue, fraction: fractionalBase - 1 },
|
||||||
|
saturated: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let fraction = first.fraction % fractionalBase;
|
let fraction = first.fraction % fractionalBase;
|
||||||
for (const x of rest) {
|
for (const x of rest) {
|
||||||
@ -131,8 +127,11 @@ export function add(first: AmountJson, ...rest: AmountJson[]): Result {
|
|||||||
|
|
||||||
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 > Number.MAX_SAFE_INTEGER) {
|
if (value > maxAmountValue) {
|
||||||
return { amount: getMaxAmount(currency), saturated: true };
|
return {
|
||||||
|
amount: { currency, value: maxAmountValue, fraction: fractionalBase - 1 },
|
||||||
|
saturated: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { amount: { currency, value, fraction }, saturated: false };
|
return { amount: { currency, value, fraction }, saturated: false };
|
||||||
@ -246,14 +245,22 @@ export function isNonZero(a: AmountJson): boolean {
|
|||||||
* 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;
|
||||||
}
|
}
|
||||||
|
const tail = res[3] || ".0";
|
||||||
|
if (tail.length > fractionalLength + 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let value = Number.parseInt(res[2]);
|
||||||
|
if (value > maxAmountValue) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
currency: res[1],
|
currency: res[1],
|
||||||
fraction: Math.round(fractionalBase * Number.parseFloat(res[3] || "0")),
|
fraction: Math.round(fractionalBase * Number.parseFloat(tail)),
|
||||||
value: Number.parseInt(res[2]),
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,12 +295,14 @@ 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.
|
||||||
*/
|
*/
|
||||||
export function toString(a: AmountJson) {
|
export function toString(a: AmountJson): string {
|
||||||
let s = a.value.toString()
|
const av = a.value + Math.floor(a.fraction / fractionalBase);
|
||||||
|
const af = a.fraction % fractionalBase;
|
||||||
|
let s = av.toString()
|
||||||
|
|
||||||
if (a.fraction) {
|
if (af) {
|
||||||
s = s + ".";
|
s = s + ".";
|
||||||
let n = a.fraction;
|
let n = af;
|
||||||
for (let i = 0; i < fractionalLength; i++) {
|
for (let i = 0; i < fractionalLength; i++) {
|
||||||
if (!n) {
|
if (!n) {
|
||||||
break;
|
break;
|
||||||
@ -310,7 +319,7 @@ export function toString(a: AmountJson) {
|
|||||||
/**
|
/**
|
||||||
* Check if the argument is a valid amount in string form.
|
* Check if the argument is a valid amount in string form.
|
||||||
*/
|
*/
|
||||||
export function check(a: any) {
|
export function check(a: any): boolean {
|
||||||
if (typeof a !== "string") {
|
if (typeof a !== "string") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ test("amount addition (simple)", (t) => {
|
|||||||
|
|
||||||
test("amount addition (saturation)", (t) => {
|
test("amount addition (saturation)", (t) => {
|
||||||
const a1 = amt(1, 0, "EUR");
|
const a1 = amt(1, 0, "EUR");
|
||||||
const res = Amounts.add(Amounts.getMaxAmount("EUR"), a1);
|
const res = Amounts.add(amt(Amounts.maxAmountValue, 0, "EUR"), a1);
|
||||||
t.true(res.saturated);
|
t.true(res.saturated);
|
||||||
t.pass();
|
t.pass();
|
||||||
});
|
});
|
||||||
@ -54,20 +54,52 @@ test("amount subtraction (saturation)", (t) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("amount comparison", (t) => {
|
||||||
|
t.is(Amounts.cmp(amt(1, 0, "EUR"), amt(1, 0, "EUR")), 0);
|
||||||
|
t.is(Amounts.cmp(amt(1, 1, "EUR"), amt(1, 0, "EUR")), 1);
|
||||||
|
t.is(Amounts.cmp(amt(1, 1, "EUR"), amt(1, 2, "EUR")), -1);
|
||||||
|
t.is(Amounts.cmp(amt(1, 0, "EUR"), amt(0, 0, "EUR")), 1);
|
||||||
|
t.is(Amounts.cmp(amt(0, 0, "EUR"), amt(1, 0, "EUR")), -1);
|
||||||
|
t.is(Amounts.cmp(amt(1, 0, "EUR"), amt(0, 100000000, "EUR")), 0);
|
||||||
|
t.throws(() => Amounts.cmp(amt(1, 0, "FOO"), amt(1, 0, "BAR")));
|
||||||
|
t.pass();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test("amount parsing", (t) => {
|
test("amount parsing", (t) => {
|
||||||
const a1 = Amounts.parseOrThrow("TESTKUDOS:10");
|
t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0"),
|
||||||
t.is(a1.currency, "TESTKUDOS");
|
amt(0, 0, "TESTKUDOS")), 0);
|
||||||
t.is(a1.value, 10);
|
t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:10"),
|
||||||
t.is(a1.fraction, 0);
|
amt(10, 0, "TESTKUDOS")), 0);
|
||||||
|
t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0.1"),
|
||||||
|
amt(0, 10000000, "TESTKUDOS")), 0);
|
||||||
|
t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0.00000001"),
|
||||||
|
amt(0, 1, "TESTKUDOS")), 0);
|
||||||
|
t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:4503599627370496.99999999"),
|
||||||
|
amt(4503599627370496, 99999999, "TESTKUDOS")), 0);
|
||||||
|
t.throws(() => Amounts.parseOrThrow("foo:"));
|
||||||
|
t.throws(() => Amounts.parseOrThrow("1.0"));
|
||||||
|
t.throws(() => Amounts.parseOrThrow("42"));
|
||||||
|
t.throws(() => Amounts.parseOrThrow(":1.0"));
|
||||||
|
t.throws(() => Amounts.parseOrThrow(":42"));
|
||||||
|
t.throws(() => Amounts.parseOrThrow("EUR:.42"));
|
||||||
|
t.throws(() => Amounts.parseOrThrow("EUR:42."));
|
||||||
|
t.throws(() => Amounts.parseOrThrow("TESTKUDOS:4503599627370497.99999999"));
|
||||||
|
t.is(Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0.99999999"),
|
||||||
|
amt(0, 99999999, "TESTKUDOS")), 0);
|
||||||
|
t.throws(() => Amounts.parseOrThrow("TESTKUDOS:0.999999991"));
|
||||||
t.pass();
|
t.pass();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test("amount stringification", (t) => {
|
test("amount stringification", (t) => {
|
||||||
|
t.is(Amounts.toString(amt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
|
||||||
t.is(Amounts.toString(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
|
t.is(Amounts.toString(amt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
|
||||||
t.is(Amounts.toString(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
|
t.is(Amounts.toString(amt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
|
||||||
t.is(Amounts.toString(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
|
t.is(Amounts.toString(amt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
|
||||||
t.is(Amounts.toString(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
|
t.is(Amounts.toString(amt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
|
||||||
|
// denormalized
|
||||||
|
t.is(Amounts.toString(amt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
|
||||||
t.pass();
|
t.pass();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user