diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs
index eae86374..51f35725 100755
--- a/src/core/Utils.mjs
+++ b/src/core/Utils.mjs
@@ -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.
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index a2bd2d08..f7ade329 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -20,6 +20,8 @@
"From Binary",
"To Octal",
"From Octal",
+ "To IEEEBinary",
+ "From IEEEBinary",
"To Base32",
"From Base32",
"To Base45",
diff --git a/src/core/lib/IEEEBinary.mjs b/src/core/lib/IEEEBinary.mjs
new file mode 100644
index 00000000..4f5096ce
--- /dev/null
+++ b/src/core/lib/IEEEBinary.mjs
@@ -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}`;
+}
diff --git a/src/core/operations/FromIEEEBinary.mjs b/src/core/operations/FromIEEEBinary.mjs
new file mode 100644
index 00000000..7417108f
--- /dev/null
+++ b/src/core/operations/FromIEEEBinary.mjs
@@ -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.
+ Example: 0 10000000010 0100000000000000000000000000000000000000000000000000
+ becomes 10.`;
+ 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;
diff --git a/src/core/operations/ToIEEEBinary.mjs b/src/core/operations/ToIEEEBinary.mjs
new file mode 100644
index 00000000..3faa7fb8
--- /dev/null
+++ b/src/core/operations/ToIEEEBinary.mjs
@@ -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.
+ Example: 2 becomes
+ 0 10000000000 0000000000000000000000000000000000000000000000000000.`;
+ 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;
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index f030349d..4b18124e 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -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";
diff --git a/tests/operations/tests/IEEEBinary.mjs b/tests/operations/tests/IEEEBinary.mjs
new file mode 100644
index 00000000..c7f4569c
--- /dev/null
+++ b/tests/operations/tests/IEEEBinary.mjs
@@ -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: [],
+ },
+ ],
+ },
+]);