From 6e817a06a1b3a4bbac5ec4435f06ce6b1fa6d9e1 Mon Sep 17 00:00:00 2001 From: Alhadis Date: Sat, 22 Feb 2020 15:00:42 +1100 Subject: [PATCH] Add functions for encoding IEEE-754 floating-points --- index.d.ts | 7 ++++ lib/binary.mjs | 80 ++++++++++++++++++++++++++++++++++++++++ test/binary.mjs | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) diff --git a/index.d.ts b/index.d.ts index 2b2755c..1f978c9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -28,6 +28,8 @@ export declare function base64Encode(bytes: number[]): string; export declare function bindMethods(subject: object): object; export declare function buildDict(dl: HTMLDListElement, valueKey?: boolean, filter?: Function | RegExp): object; export declare function byteCount(value: number, byteSize?: number): number; +export declare function bytesToFloat32(bytes: number[], littleEndian?: boolean): Float32Array; +export declare function bytesToFloat64(bytes: number[], littleEndian?: boolean): Float64Array; export declare function bytesToInt16(bytes: number[], littleEndian?: boolean): Int16Array; export declare function bytesToInt32(bytes: number[], littleEndian?: boolean): Int32Array; export declare function bytesToInt64(bytes: number[], littleEndian?: boolean): BigInt64Array; @@ -77,6 +79,10 @@ export declare function hslToRGB(input: HSLColour): RGBColour; export declare function hsvToHSL(input: HSVColour): HSLColour; export declare function hsvToRGB(input: HSVColour): RGBColour; export declare function injectWordBreaks(element: Element, limit?: number): HTMLElement[]; +export declare function int16ToBytes(input: number | number[], littleEndian?: boolean): Uint8Array; +export declare function int32ToBytes(input: number | number[], littleEndian?: boolean): Uint8Array; +export declare function int64ToBytes(input: bigint | bigint[], littleEndian?: boolean): Uint8Array; +export declare function int8ToBytes(input: number | number[]): Uint8Array; export declare function isByteArray(input: any): boolean; export declare function isFixedWidth(font: string): boolean; export declare function isIE(version: string, operand: string): boolean; @@ -92,6 +98,7 @@ export declare function keyGrep(subject: object, pattern: RegExp | string): obje export declare function ls(paths?: string[], options?: {filter?: RegExp | Function; ignore?: RegExp | Function; recurse?: number; followSymlinks?: boolean}): Promise>; export declare function nearest(subject: Node, selector: string, ignoreSelf?: boolean): Element; export declare function nerf(fn: Function, context?: object): Function; +export declare function normalise(value: number): number[]; export declare function ordinalSuffix(n: number): string; export declare function parseCSSDuration(value: string): number; export declare function parseHTMLFragment(input: string): Node[]; diff --git a/lib/binary.mjs b/lib/binary.mjs index 691ba87..6d7af2f 100644 --- a/lib/binary.mjs +++ b/lib/binary.mjs @@ -19,6 +19,86 @@ export function adler32(bytes){ } +/** + * Convert bytes to 32-bit IEEE 754 floating-point values. + * + * @example bytesToFloat32([0x41, 0xC8, 0x00, 0x00]) == [25]; + * @param {Number[]} bytes + * @param {Boolean} [littleEndian=false] + * @return {Float32Array} + */ +export function bytesToFloat32(bytes, littleEndian = false){ + const {length} = bytes; + const floats = new Float32Array(Math.ceil(length / 4)); + for(let i = 0; i < length; i += 4){ + let a = bytes[i] || 0; + let b = bytes[i + 1] || 0; + let c = bytes[i + 2] || 0; + let d = bytes[i + 3] || 0; + if(littleEndian) [a, b, c, d] = [d, c, b, a]; + const sign = (-1) ** +!!(a & 128); + const expo = (a & 127) << 1 | (b & 128) >>> 7; + let frac = (b & 127) << 16 | c << 8 | d; + d = ~~(i / 4); + switch(expo){ + case 0xFF: floats[d] = 0 === frac ? sign * Infinity : NaN; break; + case 0x00: if((floats[d] = (sign * 0)) === frac) break; // Fall-through + default: + if(expo) frac |= 1 << 23; + let float = 0; + for(let i = 0; i < 24; float += (frac >> 23 - i & 1) * 2 ** -i++); + floats[d] = float * 2 ** (expo ? expo - 127 : -126) * sign; + } + } + return floats; +} + + +/** + * Convert bytes to 64-bit IEEE 754 floating-point values. + * + * @example bytesToFloat64([0x40,0x37,0,0,0,0,0,0]) == [23]; + * @param {Number[]} bytes + * @param {Boolean} [littleEndian=false] + * @return {Float64Array} + */ +export function bytesToFloat64(input, littleEndian = false){ + const {length} = input; + const floats = new Float64Array(Math.ceil(length / 8)); + for(let i = 0; i < length; i += 8){ + let a = input[i] || 0; + let b = input[i + 1] || 0; + let c = input[i + 2] || 0; + let d = input[i + 3] || 0; + let e = input[i + 4] || 0; + let f = input[i + 5] || 0; + let g = input[i + 6] || 0; + let h = input[i + 7] || 0; + if(littleEndian) [a, b, c, d, e, f, g, h] = [h, g, f, e, d, c, b, a]; + const sign = (-1) ** +!!(a & 128); + const expo = (a & 127) << 4 | (b & 240) >>> 4; + let frac = BigInt(b & 15) << 48n + | BigInt(c) << 40n + | BigInt(d) << 32n + | BigInt(e) << 24n + | BigInt(f) << 16n + | BigInt(g) << 8n + | BigInt(h); + h = ~~(i / 8); + switch(expo){ + case 0x7FF: floats[h] = 0n === frac ? sign * Infinity : NaN; break; + case 0x000: if((floats[h] = (sign * 0)) === Number(frac)) break; // Fall-through + default: + if(expo) frac |= 1n << 52n; + let float = 0; + for(let i = 0; i < 53; float += Number(frac >> 52n - BigInt(i) & 1n) * 2 ** -i++); + floats[h] = float * 2 ** (expo ? expo - 1023 : -126) * sign; + } + } + return floats; +} + + /** * Convert bytes to 8-bit signed integers. * diff --git a/test/binary.mjs b/test/binary.mjs index 6e05a2e..4e55a92 100644 --- a/test/binary.mjs +++ b/test/binary.mjs @@ -51,6 +51,103 @@ describe("Byte-level functions", () => { }); }); + describe("bytesToFloat32()", () => { + const {bytesToFloat32} = utils; + const encode = (input, expected, thresh = 0) => { + if(thresh){ + expect(bytesToFloat32(input)[0]).to.be.closeTo(expected[0], thresh); + expect(bytesToFloat32(input.reverse(), true)[0]).to.be.closeTo(expected[0], thresh); + } + else{ + expected = Float32Array.from(expected); + expect(bytesToFloat32(input, false)).to.eql(expected); + expect(bytesToFloat32(input.reverse(), true)).to.eql(expected); + } + }; + it("encodes positive numbers", () => { + encode([0x3F, 0x80, 0x00, 0x00], [1]); + encode([0x41, 0xC8, 0x00, 0x00], [25]); + encode([0x3F, 0x80, 0x00, 0x01], [1.0000001192], 1e-11); + }); + it("encodes negative numbers", () => encode([0xC0, 0x00, 0x00, 0x00], [-2])); + it("encodes positive zero", () => encode([0x00, 0x00, 0x00, 0x00], [0])); + it("encodes negative zero", () => encode([0x80, 0x00, 0x00, 0x00], [-0])); + it("encodes positive infinity", () => encode([0x7F, 0x80, 0x00, 0x00], [Infinity])); + it("encodes negative infinity", () => encode([0xFF, 0x80, 0x00, 0x00], [-Infinity])); + it("roughly approximates π", () => encode([0x40, 0x49, 0x0F, 0xDB], [3.14159274101], 1e-11)); + it("encodes subnormal numbers", () => { + encode([0x00, 0x00, 0x00, 0x01], [1.4012984643 * (10 ** -45)], 1e-11); + encode([0x00, 0x7F, 0xFF, 0xFF], [1.1754942107 * (10 ** -38)], 1e-11); + encode([0x3F, 0x7F, 0xFF, 0xFF], [0.9999999404], 1e-11); + encode([0x3E, 0xAA, 0xAA, 0xAB], [0.333333343267], 1e-11); + }); + it("encodes NaN", () => { + encode([0xFF, 0xC0, 0x00, 0x01], [NaN]); + encode([0xFF, 0x80, 0x00, 0x01], [NaN]); + }); + it("zero-fills missing bytes", () => { + const expected = new Float32Array([-2]); + expect(bytesToFloat32([0xC0])).to.eql(expected); + expect(bytesToFloat32([0xC0, 0x00])).to.eql(expected); + expect(bytesToFloat32([0xC0, 0x00, 0x00])).to.eql(expected); + expect(bytesToFloat32([0xC0, 0x00, 0x00, 0x00, 0xC0])).to.eql(new Float32Array([-2, -2])); + }); + }); + + describe("bytesToFloat64()", () => { + const {bytesToFloat64} = utils; + const encode = (input, expected, thresh = 0) => { + if(thresh){ + expect(bytesToFloat64(input)[0]).to.be.closeTo(expected[0], thresh); + expect(bytesToFloat64(input.reverse(), true)[0]).to.be.closeTo(expected[0], thresh); + } + else{ + expected = Float64Array.from(expected); + expect(bytesToFloat64(input)).to.eql(expected); + expect(bytesToFloat64(input.reverse(), true)).to.eql(expected); + } + }; + it("encodes positive numbers", () => { + encode([0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [1]); + encode([0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [2]); + encode([0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [3]); + encode([0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [4]); + encode([0x40, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [5]); + encode([0x40, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [6]); + encode([0x40, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [23]); + encode([0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], [1.0000000000000002]); + encode([0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02], [1.0000000000000004]); + encode([0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [1.7976931348623157 * (10 ** 308)]); + }); + it("encodes negative numbers", () => encode([0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [-2])); + it("encodes positive zero", () => encode([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [+0])); + it("encodes negative zero", () => encode([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [-0])); + it("encodes positive infinity", () => encode([0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [Infinity])); + it("encodes negative infinity", () => encode([0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [-Infinity])); + it("roughly approximates π", () => encode([0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18], [Math.PI])); + it("encodes subnormal numbers", () => { + encode([0x3F, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [0.01171875]); + encode([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], [2 ** -1074], Number.EPSILON); + encode([0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [2.2250738585072009 * (10 ** -308)], Number.EPSILON); + encode([0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [2.2250738585072014 * (10 ** -308)], Number.EPSILON); + encode([0x3F, 0xD5, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55], [1 / 3]); + }); + it("encodes NaN", () => { + encode([0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], [NaN]); + encode([0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], [NaN]); + encode([0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [NaN]); + }); + it("zero-fills missing bytes", () => { + const input = [0xC0]; + for(let i = 0; i < 7; ++i){ + expect(bytesToFloat64(input)).to.eql(Float64Array.from([-2])); + input.push(0x00); + } + input.push(0xC0); + expect(bytesToFloat64(input)).to.eql(Float64Array.from([-2, -2])); + }); + }); + describe("bytesToInt8()", () => { const {bytesToInt8} = utils; it("encodes positive integers up to +127", () => {