@node-dlc/messaging
Version:
DLC Messaging Protocol
338 lines (292 loc) • 8.05 kB
text/typescript
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);
}
}