"use strict"; const assert = require("assert"); const bigintLog2 = require("@extra-bigint/log2"); function bitsNeeded(value) { if (value === 0) { return 1n; } else { return bigintLog2(value) + 1n; } } function remainderDivide(number, divideBy) { let remainder = number % divideBy; let wholes = (number - remainder) / divideBy; return [ wholes, remainder ]; } module.exports = function createArithmeticCoder(fields) { // NOTE: The fields are order-sensitive! You can *only* add a field to the definition afterwards without breaking decoding of existing values, if you put that new field at the *end*. Ranges of existing fields should never be changed, as this will break decoding. // NOTE: Minimum is inclusive, maximum is exclusive // NOTE: For binary sortability, the fields should be ordered from least to most significant // second, ..., day, ... year, mask, timezone let nextMultiplier = 1n; let processedFields = fields.map((field) => { let minimum = BigInt(field.minimum); let maximum = BigInt(field.maximum); let range = maximum - minimum; let processed = { offset: minimum, range: range, minimum: minimum, maximum: maximum, multiplier: nextMultiplier, name: field.name }; nextMultiplier = nextMultiplier * range; return processed; }); let maximumValue = nextMultiplier; let reverseFields = processedFields.slice().reverse(); return { bits: bitsNeeded(maximumValue - 1n), encode: function (data) { let number = processedFields.reduce((total, field) => { let value = data[field.name]; if (value != null) { let valueN = BigInt(value); assert(valueN >= field.minimum && valueN < field.maximum); let normalized = valueN - field.offset; return total + (normalized * field.multiplier); } else { // Effectively store a 0, and assume that the calling code deals with any requiredness constraints and understands how to handle this case return total; } }, 0n); return number; }, decode: function (number) { let result = {}; for (let field of reverseFields) { let [ wholes, remainder ] = remainderDivide(number, field.multiplier); number = remainder; result[field.name] = wholes + field.offset; } return result; } }; };