Merge 5a5ef737cec2134df127aa177f44d5fa803c98fb into 78d40eab60a46066e1d6bf5e50e7afa9432c162d

This commit is contained in:
atsiv1 2026-03-21 18:40:34 +00:00 committed by GitHub
commit 2bd5eb4de2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 703 additions and 0 deletions

View File

@ -1263,6 +1263,18 @@ class Utils {
return Utils.gcd(y, x % y);
}
/**
* Finds the greatest common divisor of two BigInt numbers.
*
* @author atsiv1 [atsiv1@proton.me]
* @param {BigInt} x
* @param {BigInt} y
* @returns {BigInt}
*/
static gcdBigInt(a, b) {
return b === 0n ? a : Utils.gcdBigInt(b, a % b);
}
/**
* Finds the modular inverse of two values.

View File

@ -20,6 +20,8 @@
"From Binary",
"To Octal",
"From Octal",
"To IEEEBinary",
"From IEEEBinary",
"To Base32",
"From Base32",
"To Base45",

389
src/core/lib/IEEEBinary.mjs Normal file
View File

@ -0,0 +1,389 @@
/**
* IEEEBinary functions.
*
* Convert between:
* - exact decimal strings IEEE 754 double-precision (binary64) bits
* using BigInt rational arithmetic and correct Round-to-Nearest-Even (RNE).
*
* @author atsiv1 [atsiv1@proton.me]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import Utils from "../Utils.mjs";
// IEEE 754 double-precision constants
const EXP_BITS = 11n;
const MANT_BITS = 52n;
const PRECISION = MANT_BITS + 1n;
const BIAS = (1n << (EXP_BITS - 1n)) - 1n;
const MAX_EXP = (1n << EXP_BITS) - 1n;
const EXP_BITS_NUM = Number(EXP_BITS);
const MANT_BITS_NUM = Number(MANT_BITS);
function normaliseInput(input) {
if (input === null || input === undefined)
throw new Error("Invalid decimal number");
return String(input).trim();
}
function safeBigInt(str, context="number") {
try {
return BigInt(str);
} catch {
throw new Error(`Invalid ${context}`);
}
}
function validateDecimal(input) {
// validates optional-sign base-10 decimal input: integers, decimals with optional digits before or after the decimal point, and scientific notation
if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(input))
throw new Error("Invalid decimal number");
}
function validateBinary64(input) {
input = normaliseInput(input);
if (!/^[01\s]+$/.test(input))
throw new Error("Binary64 must contain only 0 and 1");
if (input.replace(/\s+/g,"").length !== 64)
throw new Error("Binary64 must be exactly 64 bits");
return input.replace(/\s+/g,"");
}
/**
* Compute 10^exp as BigInt without using ** on BigInt, to avoid
* environments that try to coerce BigInt to Number.
*
* @param {bigint} exp Non-negative BigInt exponent.
* @returns {bigint}
*/
function pow10BigInt(exp) {
if (exp < 0n) {
throw new RangeError("pow10BigInt expects non-negative exponent");
}
let e = exp;
let result = 1n;
let base = 10n;
// fast exponentiation by squaring
while (e > 0n) {
if ((e & 1n) === 1n) {
result *= base;
}
base *= base;
e >>= 1n;
}
return result;
}
/**
* Converts an exact rational number (num / den) into its decimal string representation.
*
* The result is returned as a string. If the decimal is repeating, the repeating
* part will be enclosed in parentheses (e.g., "0.(3)").
*
* @param {bigint} num - The numerator of the fraction.
* @param {bigint} den - The denominator of the fraction.
* @returns {string} The decimal representation of the fraction, including repeating decimals in parentheses.
*
* @example
* // returns "0.5"
* fractionToDecimal(1n, 2n);
*
* // returns "0.(3)"
* fractionToDecimal(1n, 3n);
*
* // returns "1.25"
* fractionToDecimal(5n, 4n);
*
* @note For many binary64 values, the repeating cycle can be long, which is
* mathematically correct but may produce large strings.
*/
function fractionToDecimal(num, den) {
const intPart = num / den;
let rem = num % den;
if (rem === 0n) {
return intPart.toString();
}
const seen = new Map();
const digits = [];
let pos = 0;
while (rem !== 0n) {
if (seen.has(rem)) {
const p = seen.get(rem);
digits.splice(p, 0, "(");
digits.push(")");
break;
}
seen.set(rem, pos++);
rem *= 10n;
digits.push((rem / den).toString());
rem %= den;
}
return intPart.toString() + "." + digits.join("");
}
/**
* Converts a 64-bit IEEE-754 binary64 representation into its exact
* decimal string.
*
* Note: While binary64 can only represent a subset of all real numbers,
* every representable value has a finite decimal expansion. This function
* uses BigInt arithmetic to retrieve the exact value without the
* precision loss typically encountered in standard floating-point arithmetic.
*
* @param {string} binary64String
* @returns {string}
*
* @example
* // returns "10"
* FromIEEE754Float64("0 10000000010 0100000000000000000000000000000000000000000000000000");
*
* // returns "7.29999999999999982236431605997495353221893310546875"
* FromIEEE754Float64("0 10000000001 1101001100110011001100110011001100110011001100110011);
*/
export function FromIEEE754Float64(binary64String) {
const bin = validateBinary64(binary64String);
if (bin.length !== 64) {
throw new Error("Input must be 64 bits.");
}
const signBit = bin[0];
const expBits = bin.slice(1, 12);
const mantBits = bin.slice(12);
const sign = signBit === "1" ? "-" : "";
const exp = parseInt(expBits, 2);
const mant = BigInt("0b" + mantBits);
// Special values
if (exp === Number(MAX_EXP)) {
if (mant === 0n) {
return sign + "Infinity";
}
return "NaN";
}
let e;
let m;
if (exp === 0) {
// Subnormal: exponent is 1 - BIAS, no implicit leading 1
e = 1n - BIAS - MANT_BITS;
m = mant;
} else {
// Normal: exponent is exp - BIAS, with implicit leading 1
e = BigInt(exp) - BIAS - MANT_BITS;
m = mant | (1n << MANT_BITS);
}
// If exponent is non-negative, result is an integer
if (e >= 0n) {
return sign + (m << e).toString();
}
// Otherwise, it's a fraction m / 2^(-e)
const den = 1n << (-e);
return sign + fractionToDecimal(m, den);
}
/**
*
* Converts a decimal number into its IEEE-754 double-precision
* (binary64) bit representation.
*
* @param {string|number|BigInt} input
* @returns {string}
*
* @example
* // returns "0 10000000000 0000000000000000000000000000000000000000000000000000"
* ToIEEE754Float64("2");
*
* // returns "0 10000000011 0111010011001100110011001100110011001100110011001101"
* ToIEEE754Float64("23.300000000000000710542735760100185871124267578125");
*/
export function ToIEEE754Float64(input) {
input = normaliseInput(input);
const expOnes = "1".repeat(EXP_BITS_NUM);
const mantZeros = "0".repeat(MANT_BITS_NUM);
// Special values: NaN / ±Infinity
if (/^(NaN)$/i.test(input)) {
return `0 ${expOnes} 1${"0".repeat(MANT_BITS_NUM - 1)}`;
}
if (/^[+-]?inf(inity)?$/i.test(input)) {
const s = input.startsWith("-") ? "1" : "0";
return `${s} ${expOnes} ${mantZeros}`;
}
validateDecimal(input);
let sign = 0n;
if (input.startsWith("-")) {
sign = 1n;
input = input.slice(1);
} else if (input.startsWith("+")) {
input = input.slice(1);
}
let sci = 0n;
const sciMatch = input.match(/^(.*)e([+-]?\d+)$/i);
if (sciMatch) {
input = sciMatch[1];
sci = safeBigInt(sciMatch[2], "exponent");
}
let [intStr, fracStr] = input.split(".");
intStr = (intStr || "0").replace(/_/g, "");
fracStr = (fracStr || "").replace(/_/g, "");
let N = safeBigInt(intStr, "integer part");
let D = 1n;
if (fracStr.length > 0) {
const pow10 = pow10BigInt(BigInt(fracStr.length));
N = N * pow10 + safeBigInt(fracStr, "fractional part");
D = pow10;
}
// Apply scientific exponent
if (sci > 0n) {
N *= pow10BigInt(sci);
} else if (sci < 0n) {
D *= pow10BigInt(-sci);
}
// Zero (preserve sign, including -0)
if (N === 0n) {
const expZero = "0".repeat(EXP_BITS_NUM);
return `${sign} ${expZero} ${mantZeros}`;
}
// Reduce the fraction
const g = Utils.gcdBigInt(N, D);
N /= g;
D /= g;
// Compute binary exponent approximation (with fix)
const eN = BigInt(N.toString(2).length - 1);
const eD = BigInt(D.toString(2).length - 1);
let e2 = eN - eD;
// Guard, Round, Sticky bits
const GRS = 3n;
const totalBits = PRECISION + GRS;
// Approximate bit-length of normalised N/D: e2 + 1
const currentBitLength = eN - eD + 1n;
// Shift so we get 'totalBits' bits of precision in the quotient
let shift = totalBits - currentBitLength;
let num, den;
if (shift >= 0n) {
num = N << shift;
den = D;
} else {
num = N;
den = D << (-shift);
}
let full = num / den;
let rem = num % den;
// Normalisation Fix
if (full < (1n << (totalBits - 1n))) {
e2 -= 1n;
shift += 1n;
if (shift >= 0n) {
num <<= 1n;
} else {
den >>= 1n;
}
full = num / den;
rem = num % den;
}
const roundMask = (1n << GRS) - 1n;
const roundBits = full & roundMask;
let mant = full >> GRS;
// Extract G, R, S bits
const G = (roundBits >> 2n) & 1n;
const R = (roundBits >> 1n) & 1n;
const S = ((roundBits & 1n) === 1n || rem !== 0n) ? 1n : 0n;
let roundUp = false;
if (G === 1n) {
if (R === 1n || S === 1n) {
// strictly > 0.5
roundUp = true;
} else if ((mant & 1n) === 1n) {
// exactly 0.5 → round to even
roundUp = true;
}
}
if (roundUp) mant++;
// Renormalise if mantissa overflowed
if (mant >= (1n << PRECISION)) {
mant >>= 1n;
e2 += 1n;
}
// Overflow → Infinity
if (e2 + BIAS >= MAX_EXP) {
return `${sign} ${expOnes} ${mantZeros}`;
}
// Normal number (exponent >= 1)
if (e2 + BIAS >= 1n) {
const expValue = e2 + BIAS;
const expBits = expValue.toString(2).padStart(EXP_BITS_NUM, "0");
// Remove the implicit leading 1: mantissa stores only the 52 explicit bits
const mantBits = (mant & ((1n << MANT_BITS) - 1n))
.toString(2)
.padStart(MANT_BITS_NUM, "0");
return `${sign} ${expBits} ${mantBits}`;
}
// Subnormal numbers (exponent field = 0)
const minNormalExponentValue = 1n - BIAS;
const subShift = minNormalExponentValue - e2;
let subMant = mant >> subShift;
// Subnormal mantissa uses all 52 explicit bits
subMant &= (1n << MANT_BITS) - 1n;
const expZero = "0".repeat(EXP_BITS_NUM);
if (subMant === 0n) {
return `${sign} ${expZero} ${mantZeros}`;
}
const subMantBits = subMant.toString(2).padStart(MANT_BITS_NUM, "0");
return `${sign} ${expZero} ${subMantBits}`;
}

View File

@ -0,0 +1,57 @@
/**
* @author atsiv1
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError.mjs";
import Operation from "../Operation.mjs";
import { FromIEEE754Float64 } from "../lib/IEEEBinary.mjs";
/**
* From IEEEBinary operation
*/
class FromIEEEBinary extends Operation {
/**
* FromIEEEBinary constructor
*/
constructor() {
super();
this.name = "From IEEEBinary";
this.module = "Default";
this.description = `
Converts a 64-bit IEEE-754 binary64 representation (float64)
into its exact decimal value.<br><br>
Example: <code>0 10000000010 0100000000000000000000000000000000000000000000000000</code>
becomes <code>10</code>.`;
this.infoURL = "https://en.wikipedia.org/wiki/IEEE_754";
this.inputType = "string";
this.outputType = "string";
this.args = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
if (input === null || input === undefined)
return "";
input = String(input);
if (input.trim().length === 0)
return "";
try {
return FromIEEE754Float64(input);
} catch (err) {
throw new OperationError(err.message);
}
}
}
export default FromIEEEBinary;

View File

@ -0,0 +1,57 @@
/**
* @author atsiv1
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError.mjs";
import Operation from "../Operation.mjs";
import { ToIEEE754Float64 } from "../lib/IEEEBinary.mjs";
/**
* To IEEEBinary operation
*/
class ToIEEEBinary extends Operation {
/**
* ToIEEEBinary constructor
*/
constructor() {
super();
this.name = "To IEEEBinary";
this.module = "Default";
this.description = `
Converts a decimal number into IEEE-754 double-precision float64.<br><br>
Example: <code>2</code> becomes
<code>0 10000000000 0000000000000000000000000000000000000000000000000000</code>.`;
this.infoURL = "https://en.wikipedia.org/wiki/IEEE_754";
this.inputType = "string";
this.outputType = "string";
this.args = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
if (input === null || input === undefined)
return "";
input = String(input);
if (input.trim().length === 0)
return "";
try {
return ToIEEE754Float64(input);
} catch (err) {
throw new OperationError(err.message);
}
}
}
export default ToIEEEBinary;

View File

@ -90,6 +90,7 @@ import "./tests/HaversineDistance.mjs";
import "./tests/Hex.mjs";
import "./tests/Hexdump.mjs";
import "./tests/HKDF.mjs";
import "./tests/IEEEBinary.mjs";
import "./tests/Image.mjs";
import "./tests/IndexOfCoincidence.mjs";
import "./tests/JA3Fingerprint.mjs";

View File

@ -0,0 +1,185 @@
/**
* IEEE754 Float64 tests.
*
* @author atsiv1 [atsiv1@proton.me]
*
* @copyright Crown Copyright
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "From IEEEBinary: nothing",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "From IEEEBinary: Zero (positive)",
input: "0000000000000000000000000000000000000000000000000000000000000000",
expectedOutput: "0",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "From IEEEBinary: Zero (negative)",
input: "1000000000000000000000000000000000000000000000000000000000000000",
expectedOutput: "-0",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "From IEEEBinary: 4.5",
input: "0100000000010010000000000000000000000000000000000000000000000000",
expectedOutput: "4.5",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "From IEEEBinary: 0.1",
input: "0011111110111001100110011001100110011001100110011001100110011010",
expectedOutput:
"0.1000000000000000055511151231257827021181583404541015625",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "From IEEEBinary: Infinity",
input: "0111111111110000000000000000000000000000000000000000000000000000",
expectedOutput: "Infinity",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "From IEEEBinary: -Infinity",
input: "1111111111110000000000000000000000000000000000000000000000000000",
expectedOutput: "-Infinity",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "From IEEEBinary: NaN",
input: "0111111111111000000000000000000000000000000000000000000000000000",
expectedOutput: "NaN",
recipeConfig: [
{
op: "From IEEEBinary",
args: [],
},
],
},
{
name: "To IEEEBinary: nothing",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "To IEEEBinary",
args: [],
},
],
},
{
name: "To IEEEBinary: Zero",
input: "0",
expectedOutput:
"0 00000000000 0000000000000000000000000000000000000000000000000000",
recipeConfig: [
{
op: "To IEEEBinary",
args: [],
},
],
},
{
name: "To IEEEBinary: -0",
input: "-0",
expectedOutput:
"1 00000000000 0000000000000000000000000000000000000000000000000000",
recipeConfig: [
{
op: "To IEEEBinary",
args: [],
},
],
},
{
name: "To IEEEBinary: 4.5",
input: "4.5",
expectedOutput:
"0 10000000001 0010000000000000000000000000000000000000000000000000",
recipeConfig: [
{
op: "To IEEEBinary",
args: [],
},
],
},
{
name: "To IEEEBinary: 0.1",
input: "0.1",
expectedOutput:
"0 01111111011 1001100110011001100110011001100110011001100110011010",
recipeConfig: [
{
op: "To IEEEBinary",
args: [],
},
],
},
{
name: "To IEEEBinary: Infinity",
input: "Infinity",
expectedOutput:
"0 11111111111 0000000000000000000000000000000000000000000000000000",
recipeConfig: [
{
op: "To IEEEBinary",
args: [],
},
],
},
{
name: "To IEEEBinary: NaN",
input: "NaN",
expectedOutput:
"0 11111111111 1000000000000000000000000000000000000000000000000000",
recipeConfig: [
{
op: "To IEEEBinary",
args: [],
},
],
},
]);