UNPKG

@node-dlc/messaging

Version:
338 lines (292 loc) 8.05 kB
import Decimal from 'decimal.js'; /** * IEEE 754 double-precision floating-point number implementation * * Format: 1 bit sign + 11 bits exponent + 52 bits mantissa * * This class works directly with the binary representation to avoid * precision loss when serializing/deserializing f64 values to/from buffers. */ export class F64 { private _buffer: Buffer; // 8 bytes storing the IEEE 754 representation /** * Create an F64 from raw bytes (IEEE 754 format) */ constructor(buffer?: Buffer) { if (buffer) { if (buffer.length !== 8) { throw new Error('F64 buffer must be exactly 8 bytes'); } this._buffer = Buffer.from(buffer); } else { this._buffer = Buffer.alloc(8); } } /** * Create F64 from JavaScript number */ static fromNumber(num: number): F64 { const buffer = Buffer.alloc(8); buffer.writeDoubleBE(num, 0); return new F64(buffer); } /** * Create F64 from Decimal.js value * Uses the most precise conversion possible */ static fromDecimal(decimal: Decimal): F64 { // Convert to number and let IEEE 754 handle the precision const num = decimal.toNumber(); return F64.fromNumber(num); } /** * Create F64 from string representation * Uses Decimal.js to parse the string without precision loss from JavaScript Number */ static fromString(str: string): F64 { // Parse string using Decimal.js to avoid JavaScript Number precision loss const decimal = new Decimal(str); return F64.fromDecimal(decimal); } /** * Create F64 from bigint (treating as integer value) */ static fromBigInt(bigint: bigint): F64 { const num = Number(bigint); return F64.fromNumber(num); } /** * Create F64 representing zero (static method for API compatibility) */ static zero(): F64 { return F64.fromNumber(0); } /** * Create F64 with specific IEEE 754 components */ static fromComponents( sign: boolean, exponent: number, mantissa: bigint, ): F64 { if (exponent < 0 || exponent > 2047) { throw new Error('Exponent must be between 0 and 2047'); } if (mantissa < BigInt(0) || mantissa >= BigInt(1) << BigInt(52)) { throw new Error('Mantissa must be between 0 and 2^52-1'); } const buffer = Buffer.alloc(8); // Pack into 64-bit big-endian format // Bit layout: SEEEEEEE EEEEMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM const signBit = sign ? BigInt(1) : BigInt(0); const expBits = BigInt(exponent); const mantissaBits = mantissa; const ieee754Bits = (signBit << BigInt(63)) | (expBits << BigInt(52)) | mantissaBits; // Write as big-endian bytes for (let i = 0; i < 8; i++) { const byteValue = Number( (ieee754Bits >> BigInt(56 - i * 8)) & BigInt(0xff), ); buffer.writeUInt8(byteValue, i); } return new F64(buffer); } /** * Get the raw buffer containing IEEE 754 representation */ getBuffer(): Buffer { return Buffer.from(this._buffer); } /** * Serialize to buffer (big-endian) */ serialize(): Buffer { return this.getBuffer(); } /** * Deserialize from buffer (big-endian) */ static deserialize(buffer: Buffer): F64 { return new F64(buffer); } /** * Convert to JavaScript number * This may lose precision for very large or very precise values */ toNumber(): number { return this._buffer.readDoubleBE(0); } /** * Convert to Decimal.js for arbitrary precision */ toDecimal(): Decimal { return new Decimal(this.toNumber()); } /** * Extract IEEE 754 components */ getComponents(): { sign: boolean; exponent: number; mantissa: bigint } { // Read as big-endian 64-bit integer let ieee754Bits = BigInt(0); for (let i = 0; i < 8; i++) { ieee754Bits = (ieee754Bits << BigInt(8)) | BigInt(this._buffer.readUInt8(i)); } const sign = (ieee754Bits & (BigInt(1) << BigInt(63))) !== BigInt(0); const exponent = Number((ieee754Bits >> BigInt(52)) & BigInt(0x7ff)); const mantissa = ieee754Bits & ((BigInt(1) << BigInt(52)) - BigInt(1)); return { sign, exponent, mantissa }; } /** * Check if the value is finite */ isFinite(): boolean { const { exponent } = this.getComponents(); return exponent !== 2047; // NaN and Infinity have exponent = 2047 } /** * Check if the value is NaN */ isNaN(): boolean { const { exponent, mantissa } = this.getComponents(); return exponent === 2047 && mantissa !== BigInt(0); } /** * Check if the value is infinite */ isInfinite(): boolean { const { exponent, mantissa } = this.getComponents(); return exponent === 2047 && mantissa === BigInt(0); } /** * Check if the value is zero */ isZero(): boolean { const { exponent, mantissa } = this.getComponents(); return exponent === 0 && mantissa === BigInt(0); } /** * Get string representation */ toString(): string { return this.toNumber().toString(); } /** * Convert to JSON-safe value: number if within safe range, string if too large * This preserves precision for very large numbers that exceed JavaScript's MAX_SAFE_INTEGER */ toJSONValue(): number | string { const num = this.toNumber(); // Check if the number is within JavaScript's safe integer range if (num <= Number.MAX_SAFE_INTEGER && num >= Number.MIN_SAFE_INTEGER) { return num; } // For very large numbers, return as string to preserve precision return num.toString(); } /** * Create F64 from JSON value (handles both number and string inputs) */ static fromJSONValue(value: number | string): F64 { if (typeof value === 'string') { return F64.fromString(value); } else { return F64.fromNumber(value); } } /** * Get hex representation of the raw bytes */ toHex(): string { return this._buffer.toString('hex'); } /** * Create F64 from hex string */ static fromHex(hex: string): F64 { const buffer = Buffer.from(hex, 'hex'); if (buffer.length !== 8) { throw new Error('Hex string must represent exactly 8 bytes'); } return new F64(buffer); } /** * Compare with another F64 */ equals(other: F64): boolean { return this._buffer.equals(other._buffer); } /** * Equality comparison (alias for equals, for API compatibility) */ eq(other: F64 | number): boolean { if (typeof other === 'number') { return this.toNumber() === other; } return this.equals(other); } /** * Greater than comparison */ gt(other: F64 | number): boolean { if (typeof other === 'number') { return this.toNumber() > other; } return this.toNumber() > other.toNumber(); } /** * Less than comparison */ lt(other: F64 | number): boolean { if (typeof other === 'number') { return this.toNumber() < other; } return this.toNumber() < other.toNumber(); } /** * Greater than or equal comparison */ gte(other: F64 | number): boolean { if (typeof other === 'number') { return this.toNumber() >= other; } return this.toNumber() >= other.toNumber(); } /** * Less than or equal comparison */ lte(other: F64 | number): boolean { if (typeof other === 'number') { return this.toNumber() <= other; } return this.toNumber() <= other.toNumber(); } /** * Create a copy */ clone(): F64 { return new F64(this._buffer); } /** * Constants */ static get ZERO(): F64 { return F64.fromNumber(0); } static get ONE(): F64 { return F64.fromNumber(1); } static get NEGATIVE_ONE(): F64 { return F64.fromNumber(-1); } static get INFINITY(): F64 { return F64.fromNumber(Number.POSITIVE_INFINITY); } static get NEGATIVE_INFINITY(): F64 { return F64.fromNumber(Number.NEGATIVE_INFINITY); } static get NaN(): F64 { return F64.fromNumber(Number.NaN); } }