Add @cosmjs/encoding and @cosmjs/math

This commit is contained in:
Simon Warta
2020-06-09 17:46:03 +02:00
parent 4cca97651b
commit 3cfad7a541
53 changed files with 2176 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
import { Decimal } from "./decimal";
describe("Decimal", () => {
describe("fromAtomics", () => {
it("leads to correct atomics value", () => {
expect(Decimal.fromAtomics("1", 0).atomics).toEqual("1");
expect(Decimal.fromAtomics("1", 1).atomics).toEqual("1");
expect(Decimal.fromAtomics("1", 2).atomics).toEqual("1");
expect(Decimal.fromAtomics("1", 5).atomics).toEqual("1");
expect(Decimal.fromAtomics("2", 5).atomics).toEqual("2");
expect(Decimal.fromAtomics("3", 5).atomics).toEqual("3");
expect(Decimal.fromAtomics("10", 5).atomics).toEqual("10");
expect(Decimal.fromAtomics("20", 5).atomics).toEqual("20");
expect(Decimal.fromAtomics("30", 5).atomics).toEqual("30");
expect(Decimal.fromAtomics("100000000000000000000000", 5).atomics).toEqual("100000000000000000000000");
expect(Decimal.fromAtomics("200000000000000000000000", 5).atomics).toEqual("200000000000000000000000");
expect(Decimal.fromAtomics("300000000000000000000000", 5).atomics).toEqual("300000000000000000000000");
expect(Decimal.fromAtomics("44", 5).atomics).toEqual("44");
expect(Decimal.fromAtomics("044", 5).atomics).toEqual("44");
expect(Decimal.fromAtomics("0044", 5).atomics).toEqual("44");
expect(Decimal.fromAtomics("00044", 5).atomics).toEqual("44");
});
it("reads fractional digits correctly", () => {
expect(Decimal.fromAtomics("44", 0).toString()).toEqual("44");
expect(Decimal.fromAtomics("44", 1).toString()).toEqual("4.4");
expect(Decimal.fromAtomics("44", 2).toString()).toEqual("0.44");
expect(Decimal.fromAtomics("44", 3).toString()).toEqual("0.044");
expect(Decimal.fromAtomics("44", 4).toString()).toEqual("0.0044");
});
});
describe("fromUserInput", () => {
it("throws helpful error message for invalid characters", () => {
expect(() => Decimal.fromUserInput(" 13", 5)).toThrowError(/invalid character at position 1/i);
expect(() => Decimal.fromUserInput("1,3", 5)).toThrowError(/invalid character at position 2/i);
expect(() => Decimal.fromUserInput("13-", 5)).toThrowError(/invalid character at position 3/i);
expect(() => Decimal.fromUserInput("13/", 5)).toThrowError(/invalid character at position 3/i);
expect(() => Decimal.fromUserInput("13\\", 5)).toThrowError(/invalid character at position 3/i);
});
it("throws for more than one separator", () => {
expect(() => Decimal.fromUserInput("1.3.5", 5)).toThrowError(/more than one separator found/i);
expect(() => Decimal.fromUserInput("1..3", 5)).toThrowError(/more than one separator found/i);
expect(() => Decimal.fromUserInput("..", 5)).toThrowError(/more than one separator found/i);
});
it("throws for separator only", () => {
expect(() => Decimal.fromUserInput(".", 5)).toThrowError(/fractional part missing/i);
});
it("throws for more fractional digits than supported", () => {
expect(() => Decimal.fromUserInput("44.123456", 5)).toThrowError(
/got more fractional digits than supported/i,
);
expect(() => Decimal.fromUserInput("44.1", 0)).toThrowError(
/got more fractional digits than supported/i,
);
});
it("throws for fractional digits that are not non-negative integers", () => {
// no integer
expect(() => Decimal.fromUserInput("1", Number.NaN)).toThrowError(
/fractional digits is not an integer/i,
);
expect(() => Decimal.fromUserInput("1", Number.POSITIVE_INFINITY)).toThrowError(
/fractional digits is not an integer/i,
);
expect(() => Decimal.fromUserInput("1", Number.NEGATIVE_INFINITY)).toThrowError(
/fractional digits is not an integer/i,
);
expect(() => Decimal.fromUserInput("1", 1.78945544484)).toThrowError(
/fractional digits is not an integer/i,
);
// negative
expect(() => Decimal.fromUserInput("1", -1)).toThrowError(/fractional digits must not be negative/i);
expect(() => Decimal.fromUserInput("1", Number.MIN_SAFE_INTEGER)).toThrowError(
/fractional digits must not be negative/i,
);
// exceeds supported range
expect(() => Decimal.fromUserInput("1", 101)).toThrowError(/fractional digits must not exceed 100/i);
});
it("returns correct value", () => {
expect(Decimal.fromUserInput("44", 0).atomics).toEqual("44");
expect(Decimal.fromUserInput("44", 1).atomics).toEqual("440");
expect(Decimal.fromUserInput("44", 2).atomics).toEqual("4400");
expect(Decimal.fromUserInput("44", 3).atomics).toEqual("44000");
expect(Decimal.fromUserInput("44.2", 1).atomics).toEqual("442");
expect(Decimal.fromUserInput("44.2", 2).atomics).toEqual("4420");
expect(Decimal.fromUserInput("44.2", 3).atomics).toEqual("44200");
expect(Decimal.fromUserInput("44.1", 6).atomics).toEqual("44100000");
expect(Decimal.fromUserInput("44.12", 6).atomics).toEqual("44120000");
expect(Decimal.fromUserInput("44.123", 6).atomics).toEqual("44123000");
expect(Decimal.fromUserInput("44.1234", 6).atomics).toEqual("44123400");
expect(Decimal.fromUserInput("44.12345", 6).atomics).toEqual("44123450");
expect(Decimal.fromUserInput("44.123456", 6).atomics).toEqual("44123456");
});
it("cuts leading zeros", () => {
expect(Decimal.fromUserInput("4", 2).atomics).toEqual("400");
expect(Decimal.fromUserInput("04", 2).atomics).toEqual("400");
expect(Decimal.fromUserInput("004", 2).atomics).toEqual("400");
});
it("cuts tailing zeros", () => {
expect(Decimal.fromUserInput("4.12", 5).atomics).toEqual("412000");
expect(Decimal.fromUserInput("4.120", 5).atomics).toEqual("412000");
expect(Decimal.fromUserInput("4.1200", 5).atomics).toEqual("412000");
expect(Decimal.fromUserInput("4.12000", 5).atomics).toEqual("412000");
expect(Decimal.fromUserInput("4.120000", 5).atomics).toEqual("412000");
expect(Decimal.fromUserInput("4.1200000", 5).atomics).toEqual("412000");
});
it("interprets the empty string as zero", () => {
expect(Decimal.fromUserInput("", 0).atomics).toEqual("0");
expect(Decimal.fromUserInput("", 1).atomics).toEqual("0");
expect(Decimal.fromUserInput("", 2).atomics).toEqual("0");
expect(Decimal.fromUserInput("", 3).atomics).toEqual("0");
});
it("accepts american notation with skipped leading zero", () => {
expect(Decimal.fromUserInput(".1", 3).atomics).toEqual("100");
expect(Decimal.fromUserInput(".12", 3).atomics).toEqual("120");
expect(Decimal.fromUserInput(".123", 3).atomics).toEqual("123");
});
});
describe("toString", () => {
it("displays no decimal point for full numbers", () => {
expect(Decimal.fromUserInput("44", 0).toString()).toEqual("44");
expect(Decimal.fromUserInput("44", 1).toString()).toEqual("44");
expect(Decimal.fromUserInput("44", 2).toString()).toEqual("44");
expect(Decimal.fromUserInput("44", 2).toString()).toEqual("44");
expect(Decimal.fromUserInput("44.0", 2).toString()).toEqual("44");
expect(Decimal.fromUserInput("44.00", 2).toString()).toEqual("44");
expect(Decimal.fromUserInput("44.000", 2).toString()).toEqual("44");
});
it("only shows significant digits", () => {
expect(Decimal.fromUserInput("44.1", 2).toString()).toEqual("44.1");
expect(Decimal.fromUserInput("44.10", 2).toString()).toEqual("44.1");
expect(Decimal.fromUserInput("44.100", 2).toString()).toEqual("44.1");
});
it("fills up leading zeros", () => {
expect(Decimal.fromAtomics("3", 0).toString()).toEqual("3");
expect(Decimal.fromAtomics("3", 1).toString()).toEqual("0.3");
expect(Decimal.fromAtomics("3", 2).toString()).toEqual("0.03");
expect(Decimal.fromAtomics("3", 3).toString()).toEqual("0.003");
});
});
describe("toFloatApproximation", () => {
it("works", () => {
expect(Decimal.fromUserInput("0", 5).toFloatApproximation()).toEqual(0);
expect(Decimal.fromUserInput("1", 5).toFloatApproximation()).toEqual(1);
expect(Decimal.fromUserInput("1.5", 5).toFloatApproximation()).toEqual(1.5);
expect(Decimal.fromUserInput("0.1", 5).toFloatApproximation()).toEqual(0.1);
expect(Decimal.fromUserInput("1234500000000000", 5).toFloatApproximation()).toEqual(1.2345e15);
expect(Decimal.fromUserInput("1234500000000000.002", 5).toFloatApproximation()).toEqual(1.2345e15);
});
});
describe("plus", () => {
it("returns correct values", () => {
const zero = Decimal.fromUserInput("0", 5);
expect(zero.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("0");
expect(zero.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("1");
expect(zero.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("2");
expect(zero.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("2.8");
expect(zero.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.12345");
const one = Decimal.fromUserInput("1", 5);
expect(one.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("1");
expect(one.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("2");
expect(one.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("3");
expect(one.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("3.8");
expect(one.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("1.12345");
const oneDotFive = Decimal.fromUserInput("1.5", 5);
expect(oneDotFive.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("1.5");
expect(oneDotFive.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("2.5");
expect(oneDotFive.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("3.5");
expect(oneDotFive.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("4.3");
expect(oneDotFive.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("1.62345");
// original value remain unchanged
expect(zero.toString()).toEqual("0");
expect(one.toString()).toEqual("1");
expect(oneDotFive.toString()).toEqual("1.5");
});
it("throws for different fractional digits", () => {
const zero = Decimal.fromUserInput("0", 5);
expect(() => zero.plus(Decimal.fromUserInput("1", 1))).toThrowError(/do not match/i);
expect(() => zero.plus(Decimal.fromUserInput("1", 2))).toThrowError(/do not match/i);
expect(() => zero.plus(Decimal.fromUserInput("1", 3))).toThrowError(/do not match/i);
expect(() => zero.plus(Decimal.fromUserInput("1", 4))).toThrowError(/do not match/i);
expect(() => zero.plus(Decimal.fromUserInput("1", 6))).toThrowError(/do not match/i);
expect(() => zero.plus(Decimal.fromUserInput("1", 7))).toThrowError(/do not match/i);
});
});
});
+121
View File
@@ -0,0 +1,121 @@
import BN from "bn.js";
// Too large values lead to massive memory usage. Limit to something sensible.
// The largest value we need is 18 (Ether).
const maxFractionalDigits = 100;
/**
* A type for arbitrary precision, non-negative decimals.
*
* Instances of this class are immutable.
*/
export class Decimal {
public static fromUserInput(input: string, fractionalDigits: number): Decimal {
Decimal.verifyFractionalDigits(fractionalDigits);
const badCharacter = input.match(/[^0-9.]/);
if (badCharacter) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
throw new Error(`Invalid character at position ${badCharacter.index! + 1}`);
}
let whole: string;
let fractional: string;
if (input.search(/\./) === -1) {
// integer format, no separator
whole = input;
fractional = "";
} else {
const parts = input.split(".");
switch (parts.length) {
case 0:
case 1:
throw new Error("Fewer than two elements in split result. This must not happen here.");
case 2:
if (!parts[1]) throw new Error("Fractional part missing");
whole = parts[0];
fractional = parts[1].replace(/0+$/, "");
break;
default:
throw new Error("More than one separator found");
}
}
if (fractional.length > fractionalDigits) {
throw new Error("Got more fractional digits than supported");
}
const quantity = `${whole}${fractional.padEnd(fractionalDigits, "0")}`;
return new Decimal(quantity, fractionalDigits);
}
public static fromAtomics(atomics: string, fractionalDigits: number): Decimal {
Decimal.verifyFractionalDigits(fractionalDigits);
return new Decimal(atomics, fractionalDigits);
}
private static verifyFractionalDigits(fractionalDigits: number): void {
if (!Number.isInteger(fractionalDigits)) throw new Error("Fractional digits is not an integer");
if (fractionalDigits < 0) throw new Error("Fractional digits must not be negative");
if (fractionalDigits > maxFractionalDigits) {
throw new Error(`Fractional digits must not exceed ${maxFractionalDigits}`);
}
}
public get atomics(): string {
return this.data.atomics.toString();
}
public get fractionalDigits(): number {
return this.data.fractionalDigits;
}
private readonly data: {
readonly atomics: BN;
readonly fractionalDigits: number;
};
private constructor(atomics: string, fractionalDigits: number) {
this.data = {
atomics: new BN(atomics),
fractionalDigits: fractionalDigits,
};
}
public toString(): string {
const factor = new BN(10).pow(new BN(this.data.fractionalDigits));
const whole = this.data.atomics.div(factor);
const fractional = this.data.atomics.mod(factor);
if (fractional.isZero()) {
return whole.toString();
} else {
const fullFractionalPart = fractional.toString().padStart(this.data.fractionalDigits, "0");
const trimmedFractionalPart = fullFractionalPart.replace(/0+$/, "");
return `${whole.toString()}.${trimmedFractionalPart}`;
}
}
/**
* Returns an approximation as a float type. Only use this if no
* exact calculation is required.
*/
public toFloatApproximation(): number {
const out = Number(this.toString());
if (Number.isNaN(out)) throw new Error("Conversion to number failed");
return out;
}
/**
* a.plus(b) returns a+b.
*
* Both values need to have the same fractional digits.
*/
public plus(b: Decimal): Decimal {
if (this.fractionalDigits !== b.fractionalDigits) throw new Error("Fractional digits do not match");
const sum = this.data.atomics.add(new BN(b.atomics));
return new Decimal(sum.toString(), this.fractionalDigits);
}
}
+2
View File
@@ -0,0 +1,2 @@
export { Decimal } from "./decimal";
export { Int53, Uint32, Uint53, Uint64 } from "./integers";
+475
View File
@@ -0,0 +1,475 @@
import { Int53, Uint32, Uint53, Uint64 } from "./integers";
describe("Integers", () => {
describe("Uint32", () => {
it("can be constructed", () => {
expect(new Uint32(0)).toBeTruthy();
expect(new Uint32(1)).toBeTruthy();
expect(new Uint32(1.0)).toBeTruthy();
expect(new Uint32(42)).toBeTruthy();
expect(new Uint32(1000000000)).toBeTruthy();
expect(new Uint32(2147483647)).toBeTruthy();
expect(new Uint32(2147483648)).toBeTruthy();
expect(new Uint32(4294967295)).toBeTruthy();
});
it("throws for invald numbers", () => {
expect(() => new Uint32(NaN)).toThrowError(/not a number/);
expect(() => new Uint32(1.1)).toThrowError(/not an integer/i);
expect(() => new Uint32(Number.NEGATIVE_INFINITY)).toThrowError(/not an integer/i);
expect(() => new Uint32(Number.POSITIVE_INFINITY)).toThrowError(/not an integer/i);
});
it("throws for values out of range", () => {
expect(() => new Uint32(-1)).toThrowError(/not in uint32 range/);
expect(() => new Uint32(4294967296)).toThrowError(/not in uint32 range/);
expect(() => new Uint32(Number.MIN_SAFE_INTEGER)).toThrowError(/not in uint32 range/);
expect(() => new Uint32(Number.MAX_SAFE_INTEGER)).toThrowError(/not in uint32 range/);
});
it("can convert to number", () => {
expect(new Uint32(0).toNumber()).toEqual(0);
expect(new Uint32(1).toNumber()).toEqual(1);
expect(new Uint32(42).toNumber()).toEqual(42);
expect(new Uint32(1000000000).toNumber()).toEqual(1000000000);
expect(new Uint32(2147483647).toNumber()).toEqual(2147483647);
expect(new Uint32(2147483648).toNumber()).toEqual(2147483648);
expect(new Uint32(4294967295).toNumber()).toEqual(4294967295);
});
it("can convert to string", () => {
expect(new Uint32(0).toString()).toEqual("0");
expect(new Uint32(1).toString()).toEqual("1");
expect(new Uint32(42).toString()).toEqual("42");
expect(new Uint32(1000000000).toString()).toEqual("1000000000");
expect(new Uint32(2147483647).toString()).toEqual("2147483647");
expect(new Uint32(2147483648).toString()).toEqual("2147483648");
expect(new Uint32(4294967295).toString()).toEqual("4294967295");
});
describe("toBytesBigEndian", () => {
it("works", () => {
expect(new Uint32(0).toBytesBigEndian()).toEqual(new Uint8Array([0, 0, 0, 0]));
expect(new Uint32(1).toBytesBigEndian()).toEqual(new Uint8Array([0, 0, 0, 1]));
expect(new Uint32(42).toBytesBigEndian()).toEqual(new Uint8Array([0, 0, 0, 42]));
expect(new Uint32(1000000000).toBytesBigEndian()).toEqual(new Uint8Array([0x3b, 0x9a, 0xca, 0x00]));
expect(new Uint32(2147483647).toBytesBigEndian()).toEqual(new Uint8Array([0x7f, 0xff, 0xff, 0xff]));
expect(new Uint32(2147483648).toBytesBigEndian()).toEqual(new Uint8Array([0x80, 0x00, 0x00, 0x00]));
expect(new Uint32(4294967295).toBytesBigEndian()).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff]));
});
});
describe("toBytesLittleEndian", () => {
it("works", () => {
expect(new Uint32(0).toBytesLittleEndian()).toEqual(new Uint8Array([0, 0, 0, 0]));
expect(new Uint32(1).toBytesLittleEndian()).toEqual(new Uint8Array([1, 0, 0, 0]));
expect(new Uint32(42).toBytesLittleEndian()).toEqual(new Uint8Array([42, 0, 0, 0]));
expect(new Uint32(1000000000).toBytesLittleEndian()).toEqual(
new Uint8Array([0x00, 0xca, 0x9a, 0x3b]),
);
expect(new Uint32(2147483647).toBytesLittleEndian()).toEqual(
new Uint8Array([0xff, 0xff, 0xff, 0x7f]),
);
expect(new Uint32(2147483648).toBytesLittleEndian()).toEqual(
new Uint8Array([0x00, 0x00, 0x00, 0x80]),
);
expect(new Uint32(4294967295).toBytesLittleEndian()).toEqual(
new Uint8Array([0xff, 0xff, 0xff, 0xff]),
);
});
});
describe("fromBigEndianBytes", () => {
it("can be constructed from to byte array", () => {
expect(Uint32.fromBigEndianBytes([0, 0, 0, 0]).toNumber()).toEqual(0);
expect(Uint32.fromBigEndianBytes([0, 0, 0, 1]).toNumber()).toEqual(1);
expect(Uint32.fromBigEndianBytes([0, 0, 0, 42]).toNumber()).toEqual(42);
expect(Uint32.fromBigEndianBytes([0x3b, 0x9a, 0xca, 0x00]).toNumber()).toEqual(1000000000);
expect(Uint32.fromBigEndianBytes([0x7f, 0xff, 0xff, 0xff]).toNumber()).toEqual(2147483647);
expect(Uint32.fromBigEndianBytes([0x80, 0x00, 0x00, 0x00]).toNumber()).toEqual(2147483648);
expect(Uint32.fromBigEndianBytes([0xff, 0xff, 0xff, 0xff]).toNumber()).toEqual(4294967295);
});
it("can be constructed from Buffer", () => {
expect(Uint32.fromBigEndianBytes(Buffer.from([0, 0, 0, 0])).toNumber()).toEqual(0);
expect(Uint32.fromBigEndianBytes(Buffer.from([0, 0, 0, 1])).toNumber()).toEqual(1);
expect(Uint32.fromBigEndianBytes(Buffer.from([0, 0, 0, 42])).toNumber()).toEqual(42);
expect(Uint32.fromBigEndianBytes(Buffer.from([0x3b, 0x9a, 0xca, 0x00])).toNumber()).toEqual(
1000000000,
);
expect(Uint32.fromBigEndianBytes(Buffer.from([0x7f, 0xff, 0xff, 0xff])).toNumber()).toEqual(
2147483647,
);
expect(Uint32.fromBigEndianBytes(Buffer.from([0x80, 0x00, 0x00, 0x00])).toNumber()).toEqual(
2147483648,
);
expect(Uint32.fromBigEndianBytes(Buffer.from([0xff, 0xff, 0xff, 0xff])).toNumber()).toEqual(
4294967295,
);
});
it("throws for invalid input length", () => {
expect(() => Uint32.fromBigEndianBytes([])).toThrowError(/Invalid input length/);
expect(() => Uint32.fromBigEndianBytes([0, 0, 0])).toThrowError(/Invalid input length/);
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, 0, 0])).toThrowError(/Invalid input length/);
});
it("throws for invalid values", () => {
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, -1])).toThrowError(/Invalid value in byte/);
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, 1.5])).toThrowError(/Invalid value in byte/);
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, 256])).toThrowError(/Invalid value in byte/);
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, NaN])).toThrowError(/Invalid value in byte/);
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, Number.NEGATIVE_INFINITY])).toThrowError(
/Invalid value in byte/,
);
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, Number.POSITIVE_INFINITY])).toThrowError(
/Invalid value in byte/,
);
});
});
});
describe("Int53", () => {
it("can be constructed", () => {
expect(new Int53(0)).toBeTruthy();
expect(new Int53(1)).toBeTruthy();
expect(new Int53(1.0)).toBeTruthy();
expect(new Int53(42)).toBeTruthy();
expect(new Int53(1000000000)).toBeTruthy();
expect(new Int53(2147483647)).toBeTruthy();
expect(new Int53(2147483648)).toBeTruthy();
expect(new Int53(4294967295)).toBeTruthy();
expect(new Int53(9007199254740991)).toBeTruthy();
expect(new Int53(-1)).toBeTruthy();
expect(new Int53(-42)).toBeTruthy();
expect(new Int53(-2147483648)).toBeTruthy();
expect(new Int53(-2147483649)).toBeTruthy();
expect(new Int53(-9007199254740991)).toBeTruthy();
});
it("throws for invald numbers", () => {
expect(() => new Int53(NaN)).toThrowError(/not a number/);
expect(() => new Int53(1.1)).toThrowError(/not an integer/i);
expect(() => new Int53(Number.NEGATIVE_INFINITY)).toThrowError(/not an integer/i);
expect(() => new Int53(Number.POSITIVE_INFINITY)).toThrowError(/not an integer/i);
});
it("throws for values out of range", () => {
expect(() => new Int53(Number.MIN_SAFE_INTEGER - 1)).toThrowError(/not in int53 range/);
expect(() => new Int53(Number.MAX_SAFE_INTEGER + 1)).toThrowError(/not in int53 range/);
});
it("can convert to number", () => {
expect(new Int53(0).toNumber()).toEqual(0);
expect(new Int53(1).toNumber()).toEqual(1);
expect(new Int53(42).toNumber()).toEqual(42);
expect(new Int53(1000000000).toNumber()).toEqual(1000000000);
expect(new Int53(2147483647).toNumber()).toEqual(2147483647);
expect(new Int53(2147483648).toNumber()).toEqual(2147483648);
expect(new Int53(4294967295).toNumber()).toEqual(4294967295);
expect(new Int53(9007199254740991).toNumber()).toEqual(9007199254740991);
expect(new Int53(-1).toNumber()).toEqual(-1);
expect(new Int53(-9007199254740991).toNumber()).toEqual(-9007199254740991);
});
it("can convert to string", () => {
expect(new Int53(0).toString()).toEqual("0");
expect(new Int53(1).toString()).toEqual("1");
expect(new Int53(42).toString()).toEqual("42");
expect(new Int53(1000000000).toString()).toEqual("1000000000");
expect(new Int53(2147483647).toString()).toEqual("2147483647");
expect(new Int53(2147483648).toString()).toEqual("2147483648");
expect(new Int53(4294967295).toString()).toEqual("4294967295");
expect(new Int53(9007199254740991).toString()).toEqual("9007199254740991");
expect(new Int53(-1).toString()).toEqual("-1");
expect(new Int53(-9007199254740991).toString()).toEqual("-9007199254740991");
});
it("can be constructed from string", () => {
expect(Int53.fromString("0").toString()).toEqual("0");
expect(Int53.fromString("1").toString()).toEqual("1");
expect(Int53.fromString("9007199254740991").toString()).toEqual("9007199254740991");
expect(Int53.fromString("-1").toString()).toEqual("-1");
expect(Int53.fromString("-9007199254740991").toString()).toEqual("-9007199254740991");
});
it("throws for invalid string format", () => {
expect(() => Int53.fromString(" 0")).toThrowError(/invalid string format/i);
expect(() => Int53.fromString("+0")).toThrowError(/invalid string format/i);
expect(() => Int53.fromString("1e6")).toThrowError(/invalid string format/i);
expect(() => Int53.fromString("9007199254740992")).toThrowError(/input not in int53 range/i);
expect(() => Int53.fromString("-9007199254740992")).toThrowError(/input not in int53 range/i);
});
});
describe("Uint53", () => {
it("can be constructed", () => {
expect(new Uint53(0)).toBeTruthy();
expect(new Uint53(1)).toBeTruthy();
expect(new Uint53(1.0)).toBeTruthy();
expect(new Uint53(42)).toBeTruthy();
expect(new Uint53(1000000000)).toBeTruthy();
expect(new Uint53(2147483647)).toBeTruthy();
expect(new Uint53(2147483648)).toBeTruthy();
expect(new Uint53(4294967295)).toBeTruthy();
expect(new Uint53(9007199254740991)).toBeTruthy();
});
it("throws for invald numbers", () => {
expect(() => new Uint53(NaN)).toThrowError(/not a number/);
expect(() => new Uint53(1.1)).toThrowError(/not an integer/i);
expect(() => new Uint53(Number.NEGATIVE_INFINITY)).toThrowError(/not an integer/i);
expect(() => new Uint53(Number.POSITIVE_INFINITY)).toThrowError(/not an integer/i);
});
it("throws for values out of range", () => {
expect(() => new Uint53(Number.MIN_SAFE_INTEGER - 1)).toThrowError(/not in int53 range/);
expect(() => new Uint53(Number.MAX_SAFE_INTEGER + 1)).toThrowError(/not in int53 range/);
});
it("throws for negative inputs", () => {
expect(() => new Uint53(-1)).toThrowError(/is negative/);
expect(() => new Uint53(-42)).toThrowError(/is negative/);
expect(() => new Uint53(-2147483648)).toThrowError(/is negative/);
expect(() => new Uint53(-2147483649)).toThrowError(/is negative/);
expect(() => new Uint53(-9007199254740991)).toThrowError(/is negative/);
});
it("can convert to number", () => {
expect(new Uint53(0).toNumber()).toEqual(0);
expect(new Uint53(1).toNumber()).toEqual(1);
expect(new Uint53(42).toNumber()).toEqual(42);
expect(new Uint53(1000000000).toNumber()).toEqual(1000000000);
expect(new Uint53(2147483647).toNumber()).toEqual(2147483647);
expect(new Uint53(2147483648).toNumber()).toEqual(2147483648);
expect(new Uint53(4294967295).toNumber()).toEqual(4294967295);
expect(new Uint53(9007199254740991).toNumber()).toEqual(9007199254740991);
});
it("can convert to string", () => {
expect(new Uint53(0).toString()).toEqual("0");
expect(new Uint53(1).toString()).toEqual("1");
expect(new Uint53(42).toString()).toEqual("42");
expect(new Uint53(1000000000).toString()).toEqual("1000000000");
expect(new Uint53(2147483647).toString()).toEqual("2147483647");
expect(new Uint53(2147483648).toString()).toEqual("2147483648");
expect(new Uint53(4294967295).toString()).toEqual("4294967295");
expect(new Uint53(9007199254740991).toString()).toEqual("9007199254740991");
});
it("can be constructed from string", () => {
expect(Uint53.fromString("0").toString()).toEqual("0");
expect(Uint53.fromString("1").toString()).toEqual("1");
expect(Uint53.fromString("9007199254740991").toString()).toEqual("9007199254740991");
});
it("throws for invalid string format", () => {
expect(() => Uint53.fromString(" 0")).toThrowError(/invalid string format/i);
expect(() => Uint53.fromString("+0")).toThrowError(/invalid string format/i);
expect(() => Uint53.fromString("1e6")).toThrowError(/invalid string format/i);
expect(() => Uint53.fromString("-9007199254740992")).toThrowError(/input not in int53 range/i);
expect(() => Uint53.fromString("9007199254740992")).toThrowError(/input not in int53 range/i);
expect(() => Uint53.fromString("-1")).toThrowError(/input is negative/i);
});
});
describe("Uint64", () => {
describe("fromBigEndianBytes", () => {
it("can be constructed from bytes", () => {
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
});
it("can be constructed from Uint8Array", () => {
Uint64.fromBytesBigEndian(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
});
it("throws for wrong number of bytes", () => {
expect(() => Uint64.fromBytesBigEndian([])).toThrowError(/invalid input length/i);
expect(() => Uint64.fromBytesBigEndian([0x00])).toThrowError(/invalid input length/i);
expect(() => Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).toThrowError(
/invalid input length/i,
);
expect(() =>
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
).toThrowError(/invalid input length/i);
});
it("throws for wrong byte value", () => {
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, 256])).toThrowError(
/invalid value in byte/i,
);
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, -1])).toThrowError(
/invalid value in byte/i,
);
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, 1.5])).toThrowError(
/invalid value in byte/i,
);
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, Number.NEGATIVE_INFINITY])).toThrowError(
/invalid value in byte/i,
);
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, Number.POSITIVE_INFINITY])).toThrowError(
/invalid value in byte/i,
);
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, Number.NaN])).toThrowError(
/invalid value in byte/i,
);
});
});
describe("fromString", () => {
it("can be constructed from string", () => {
{
const a = Uint64.fromString("0");
expect(a).toBeTruthy();
}
{
const a = Uint64.fromString("1");
expect(a).toBeTruthy();
}
{
const a = Uint64.fromString("01");
expect(a).toBeTruthy();
}
{
const a = Uint64.fromString("9999999999999999999");
expect(a).toBeTruthy();
}
{
const a = Uint64.fromString("18446744073709551615");
expect(a).toBeTruthy();
}
});
it("throws for invalid string values", () => {
expect(() => Uint64.fromString(" 1")).toThrowError(/invalid string format/i);
expect(() => Uint64.fromString("-1")).toThrowError(/invalid string format/i);
expect(() => Uint64.fromString("+1")).toThrowError(/invalid string format/i);
expect(() => Uint64.fromString("1e6")).toThrowError(/invalid string format/i);
});
it("throws for string values exceeding uint64", () => {
expect(() => Uint64.fromString("18446744073709551616")).toThrowError(/input exceeds uint64 range/i);
expect(() => Uint64.fromString("99999999999999999999")).toThrowError(/input exceeds uint64 range/i);
});
});
describe("fromNumber", () => {
it("can be constructed from number", () => {
const a = Uint64.fromNumber(0);
expect(a.toNumber()).toEqual(0);
const b = Uint64.fromNumber(1);
expect(b.toNumber()).toEqual(1);
const c = Uint64.fromNumber(Number.MAX_SAFE_INTEGER);
expect(c.toNumber()).toEqual(Number.MAX_SAFE_INTEGER);
});
it("throws when constructed from wrong numbers", () => {
// not a number
expect(() => Uint64.fromNumber(Number.NaN)).toThrowError(/input is not a number/i);
// not an integer
expect(() => Uint64.fromNumber(Number.NEGATIVE_INFINITY)).toThrowError(
/input is not a safe integer/i,
);
expect(() => Uint64.fromNumber(Number.POSITIVE_INFINITY)).toThrowError(
/input is not a safe integer/i,
);
expect(() => Uint64.fromNumber(Number.MAX_SAFE_INTEGER + 1)).toThrowError(
/input is not a safe integer/i,
);
// negative integer
expect(() => Uint64.fromNumber(-1)).toThrowError(/input is negative/i);
expect(() => Uint64.fromNumber(Number.MIN_SAFE_INTEGER)).toThrowError(/input is negative/i);
});
});
it("can export bytes (big endian)", () => {
expect(
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesBigEndian(),
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
expect(
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).toBytesBigEndian(),
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]));
expect(
Uint64.fromBytesBigEndian([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesBigEndian(),
).toEqual(new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
expect(
Uint64.fromBytesBigEndian([0xab, 0x22, 0xbc, 0x5f, 0xa9, 0x20, 0x4e, 0x0d]).toBytesBigEndian(),
).toEqual(new Uint8Array([0xab, 0x22, 0xbc, 0x5f, 0xa9, 0x20, 0x4e, 0x0d]));
expect(
Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]).toBytesBigEndian(),
).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]));
});
it("can export bytes (little endian)", () => {
expect(
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesLittleEndian(),
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
expect(
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).toBytesLittleEndian(),
).toEqual(new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
expect(
Uint64.fromBytesBigEndian([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesLittleEndian(),
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]));
expect(
Uint64.fromBytesBigEndian([0xab, 0x22, 0xbc, 0x5f, 0xa9, 0x20, 0x4e, 0x0d]).toBytesLittleEndian(),
).toEqual(new Uint8Array([0x0d, 0x4e, 0x20, 0xa9, 0x5f, 0xbc, 0x22, 0xab]));
expect(
Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]).toBytesLittleEndian(),
).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]));
});
it("can export strings", () => {
{
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
expect(a.toString()).toEqual("0");
}
{
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
expect(a.toString()).toEqual("1");
}
{
const a = Uint64.fromBytesBigEndian([0x8a, 0xc7, 0x23, 0x04, 0x89, 0xe7, 0xff, 0xff]);
expect(a.toString()).toEqual("9999999999999999999");
}
{
const a = Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
expect(a.toString()).toEqual("18446744073709551615");
}
});
it("can export numbers", () => {
{
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
expect(a.toNumber()).toEqual(0);
}
{
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
expect(a.toNumber()).toEqual(1);
}
{
// value too large for 53 bit integer
const a = Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
expect(() => a.toNumber()).toThrowError(/number can only safely store up to 53 bits/i);
}
{
// Number.MAX_SAFE_INTEGER + 1
const a = Uint64.fromBytesBigEndian([0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
expect(() => a.toNumber()).toThrowError(/number can only safely store up to 53 bits/i);
}
});
});
});
+212
View File
@@ -0,0 +1,212 @@
/* eslint-disable no-bitwise */
import BN from "bn.js";
const uint64MaxValue = new BN("18446744073709551615", 10, "be");
/** Internal interface to ensure all integer types can be used equally */
interface Integer {
readonly toNumber: () => number;
readonly toString: () => string;
}
interface WithByteConverters {
readonly toBytesBigEndian: () => Uint8Array;
readonly toBytesLittleEndian: () => Uint8Array;
}
export class Uint32 implements Integer, WithByteConverters {
public static fromBigEndianBytes(bytes: ArrayLike<number>): Uint32 {
if (bytes.length !== 4) {
throw new Error("Invalid input length. Expected 4 bytes.");
}
for (let i = 0; i < bytes.length; ++i) {
if (!Number.isInteger(bytes[i]) || bytes[i] > 255 || bytes[i] < 0) {
throw new Error("Invalid value in byte. Found: " + bytes[i]);
}
}
// Use mulitiplication instead of shifting since bitwise operators are defined
// on SIGNED int32 in JavaScript and we don't want to risk surprises
return new Uint32(bytes[0] * 2 ** 24 + bytes[1] * 2 ** 16 + bytes[2] * 2 ** 8 + bytes[3]);
}
protected readonly data: number;
public constructor(input: number) {
if (Number.isNaN(input)) {
throw new Error("Input is not a number");
}
if (!Number.isInteger(input)) {
throw new Error("Input is not an integer");
}
if (input < 0 || input > 4294967295) {
throw new Error("Input not in uint32 range: " + input.toString());
}
this.data = input;
}
public toBytesBigEndian(): Uint8Array {
// Use division instead of shifting since bitwise operators are defined
// on SIGNED int32 in JavaScript and we don't want to risk surprises
return new Uint8Array([
Math.floor(this.data / 2 ** 24) & 0xff,
Math.floor(this.data / 2 ** 16) & 0xff,
Math.floor(this.data / 2 ** 8) & 0xff,
Math.floor(this.data / 2 ** 0) & 0xff,
]);
}
public toBytesLittleEndian(): Uint8Array {
// Use division instead of shifting since bitwise operators are defined
// on SIGNED int32 in JavaScript and we don't want to risk surprises
return new Uint8Array([
Math.floor(this.data / 2 ** 0) & 0xff,
Math.floor(this.data / 2 ** 8) & 0xff,
Math.floor(this.data / 2 ** 16) & 0xff,
Math.floor(this.data / 2 ** 24) & 0xff,
]);
}
public toNumber(): number {
return this.data;
}
public toString(): string {
return this.data.toString();
}
}
export class Int53 implements Integer {
public static fromString(str: string): Int53 {
if (!str.match(/^-?[0-9]+$/)) {
throw new Error("Invalid string format");
}
return new Int53(Number.parseInt(str, 10));
}
protected readonly data: number;
public constructor(input: number) {
if (Number.isNaN(input)) {
throw new Error("Input is not a number");
}
if (!Number.isInteger(input)) {
throw new Error("Input is not an integer");
}
if (input < Number.MIN_SAFE_INTEGER || input > Number.MAX_SAFE_INTEGER) {
throw new Error("Input not in int53 range: " + input.toString());
}
this.data = input;
}
public toNumber(): number {
return this.data;
}
public toString(): string {
return this.data.toString();
}
}
export class Uint53 implements Integer {
public static fromString(str: string): Uint53 {
const signed = Int53.fromString(str);
return new Uint53(signed.toNumber());
}
protected readonly data: Int53;
public constructor(input: number) {
const signed = new Int53(input);
if (signed.toNumber() < 0) {
throw new Error("Input is negative");
}
this.data = signed;
}
public toNumber(): number {
return this.data.toNumber();
}
public toString(): string {
return this.data.toString();
}
}
export class Uint64 implements Integer, WithByteConverters {
public static fromBytesBigEndian(bytes: ArrayLike<number>): Uint64 {
if (bytes.length !== 8) {
throw new Error("Invalid input length. Expected 8 bytes.");
}
for (let i = 0; i < bytes.length; ++i) {
if (!Number.isInteger(bytes[i]) || bytes[i] > 255 || bytes[i] < 0) {
throw new Error("Invalid value in byte. Found: " + bytes[i]);
}
}
const asArray: number[] = [];
for (let i = 0; i < bytes.length; ++i) {
asArray.push(bytes[i]);
}
return new Uint64(new BN([...asArray]));
}
public static fromString(str: string): Uint64 {
if (!str.match(/^[0-9]+$/)) {
throw new Error("Invalid string format");
}
return new Uint64(new BN(str, 10, "be"));
}
public static fromNumber(input: number): Uint64 {
if (Number.isNaN(input)) {
throw new Error("Input is not a number");
}
let bigint: BN;
try {
bigint = new BN(input);
} catch {
throw new Error("Input is not a safe integer");
}
return new Uint64(bigint);
}
private readonly data: BN;
private constructor(data: BN) {
if (data.isNeg()) {
throw new Error("Input is negative");
}
if (data.gt(uint64MaxValue)) {
throw new Error("Input exceeds uint64 range");
}
this.data = data;
}
public toBytesBigEndian(): Uint8Array {
return Uint8Array.from(this.data.toArray("be", 8));
}
public toBytesLittleEndian(): Uint8Array {
return Uint8Array.from(this.data.toArray("le", 8));
}
public toString(): string {
return this.data.toString(10);
}
public toNumber(): number {
return this.data.toNumber();
}
}