UNPKG

hyparquet

Version:

Parquet file parser for JavaScript

268 lines (244 loc) • 9.39 kB
/** * ALP (Adaptive Lossless floating-Point) encoding decoder. * Supports FLOAT and DOUBLE types. * * ALP encodes floating-point values by converting them to integers using * decimal scaling, then applying frame of reference (FOR) encoding and * bit-packing. Values that cannot be losslessly converted are stored as exceptions. * * See https://github.com/apache/parquet-format/blob/master/AlpEncoding.md */ // Correctly-rounded powers of ten (all exactly representable as doubles) const POW10 = [ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, ] const POW10_NEG = [ 1e0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15, 1e-16, 1e-17, 1e-18, ] // float32 versions for FLOAT decoding (exponent range [0, 10]) const POW10F = POW10.slice(0, 11).map(Math.fround) const POW10F_NEG = POW10_NEG.slice(0, 11).map(Math.fround) /** * Decode ALP encoded data. * * @import {DataReader, DecodedArray, ParquetType} from '../src/types.d.ts' * @param {DataReader} reader - data reader positioned at start of ALP data * @param {number} count - number of values to decode * @param {ParquetType} type - FLOAT or DOUBLE * @returns {DecodedArray} decoded float or double array */ export function alpDecode(reader, count, type) { if (type === 'FLOAT') { return alpDecodeFloat(reader, count) } else if (type === 'DOUBLE') { return alpDecodeDouble(reader, count) } else { throw new Error(`ALP encoding unsupported type: ${type}`) } } /** * Read the ALP page header and offset array, returning vector layout info. * * @param {DataReader} reader * @returns {{ vectorSize: number, numElements: number, numVectors: number, offsetArrayStart: number }} */ function readAlpHeader(reader) { const { view } = reader const compressionMode = view.getUint8(reader.offset++) if (compressionMode !== 0) { throw new Error(`ALP unsupported compression mode: ${compressionMode}`) } const integerEncoding = view.getUint8(reader.offset++) if (integerEncoding !== 0) { throw new Error(`ALP unsupported integer encoding: ${integerEncoding}`) } const logVectorSize = view.getUint8(reader.offset++) if (logVectorSize < 3 || logVectorSize > 15) { throw new Error(`ALP invalid log_vector_size: ${logVectorSize}`) } const vectorSize = 1 << logVectorSize const numElements = view.getInt32(reader.offset, true) reader.offset += 4 const numVectors = Math.ceil(numElements / vectorSize) const offsetArrayStart = reader.offset reader.offset += numVectors * 4 return { vectorSize, numElements, numVectors, offsetArrayStart } } /** * Decode ALP encoded float data. * * @param {DataReader} reader * @param {number} count * @returns {Float32Array} */ export function alpDecodeFloat(reader, count) { const { view } = reader const { vectorSize, numElements, numVectors, offsetArrayStart } = readAlpHeader(reader) if (numElements !== count) { throw new Error(`ALP num_elements ${numElements} does not match expected ${count}`) } const output = new Float32Array(count) const outputBytes = new Uint8Array(output.buffer) for (let v = 0; v < numVectors; v++) { reader.offset = offsetArrayStart + view.getUint32(offsetArrayStart + v * 4, true) const outputOffset = v * vectorSize const n = Math.min(vectorSize, numElements - outputOffset) // AlpInfo (4 bytes) const exponent = view.getUint8(reader.offset++) const factor = view.getUint8(reader.offset++) const numExceptions = view.getUint16(reader.offset, true) reader.offset += 2 // ForInfo (5 bytes) const frameOfReference = view.getInt32(reader.offset, true) reader.offset += 4 const bitWidth = view.getUint8(reader.offset++) // PackedValues const deltas = unpackBits(reader, n, bitWidth) // Reverse FOR (wrapping int32) and decimal decode in float32 arithmetic const mulFactor = POW10F[factor] const mulExponent = POW10F_NEG[exponent] for (let i = 0; i < n; i++) { const encoded = deltas[i] + frameOfReference | 0 output[outputOffset + i] = Math.fround(Math.fround(encoded) * mulFactor) * mulExponent } patchExceptions(reader, numExceptions, 4, outputBytes, outputOffset) } return output } /** * Decode ALP encoded double data. * * @param {DataReader} reader * @param {number} count * @returns {Float64Array} */ export function alpDecodeDouble(reader, count) { const { view } = reader const { vectorSize, numElements, numVectors, offsetArrayStart } = readAlpHeader(reader) if (numElements !== count) { throw new Error(`ALP num_elements ${numElements} does not match expected ${count}`) } const output = new Float64Array(count) const outputBytes = new Uint8Array(output.buffer) for (let v = 0; v < numVectors; v++) { reader.offset = offsetArrayStart + view.getUint32(offsetArrayStart + v * 4, true) const outputOffset = v * vectorSize const n = Math.min(vectorSize, numElements - outputOffset) // AlpInfo (4 bytes) const exponent = view.getUint8(reader.offset++) const factor = view.getUint8(reader.offset++) const numExceptions = view.getUint16(reader.offset, true) reader.offset += 2 // ForInfo (9 bytes) const frameOfReference = view.getBigInt64(reader.offset, true) reader.offset += 8 const bitWidth = view.getUint8(reader.offset++) const mulFactor = POW10[factor] const mulExponent = POW10_NEG[exponent] const forNumber = Number(frameOfReference) if (bitWidth <= 32 && Number.isSafeInteger(forNumber)) { // Fast path: deltas and frame of reference are exact as doubles, // so delta + FOR rounds identically to an int64 -> double cast. const deltas = unpackBits(reader, n, bitWidth) for (let i = 0; i < n; i++) { output[outputOffset + i] = (deltas[i] + forNumber) * mulFactor * mulExponent } } else { // Wide values: use BigInt for exact wrapping int64 arithmetic const deltas = unpackBitsBig(reader, n, bitWidth) for (let i = 0; i < n; i++) { const encoded = BigInt.asIntN(64, deltas[i] + frameOfReference) output[outputOffset + i] = Number(encoded) * mulFactor * mulExponent } } patchExceptions(reader, numExceptions, 8, outputBytes, outputOffset) } return output } /** * Read exception positions and values, and copy the raw IEEE-754 bytes * into the output (preserving NaN payloads bit-for-bit). * * @param {DataReader} reader * @param {number} numExceptions * @param {number} byteSize - 4 for float, 8 for double * @param {Uint8Array} outputBytes - byte view of the output array * @param {number} outputOffset - element index of the current vector */ function patchExceptions(reader, numExceptions, byteSize, outputBytes, outputOffset) { if (!numExceptions) return const { view } = reader const positionsStart = reader.offset const valuesStart = positionsStart + numExceptions * 2 const source = new Uint8Array(view.buffer, view.byteOffset + valuesStart, numExceptions * byteSize) for (let e = 0; e < numExceptions; e++) { const pos = view.getUint16(positionsStart + e * 2, true) const dest = (outputOffset + pos) * byteSize outputBytes.set(source.subarray(e * byteSize, (e + 1) * byteSize), dest) } reader.offset = valuesStart + numExceptions * byteSize } /** * Unpack LSB-first bit-packed unsigned values with bitWidth <= 32. * Consumes ceil(count * bitWidth / 8) bytes from the reader. * * @param {DataReader} reader * @param {number} count - number of values to unpack * @param {number} bitWidth - bits per value * @returns {Uint32Array} */ function unpackBits(reader, count, bitWidth) { const output = new Uint32Array(count) if (!bitWidth) return output const { view } = reader const start = reader.offset const modulus = 2 ** bitWidth let bitPos = 0 for (let i = 0; i < count; i++) { let byteIndex = start + (bitPos >>> 3) const shift = bitPos & 7 // accumulate as a double to avoid 32-bit overflow (at most 40 bits needed) let value = view.getUint8(byteIndex++) >>> shift let bits = 8 - shift while (bits < bitWidth) { value += view.getUint8(byteIndex++) * 2 ** bits bits += 8 } output[i] = value % modulus bitPos += bitWidth } reader.offset = start + Math.ceil(count * bitWidth / 8) return output } /** * Unpack LSB-first bit-packed unsigned values with bitWidth <= 64 as BigInts. * Consumes ceil(count * bitWidth / 8) bytes from the reader. * * @param {DataReader} reader * @param {number} count - number of values to unpack * @param {number} bitWidth - bits per value * @returns {BigUint64Array} */ function unpackBitsBig(reader, count, bitWidth) { const output = new BigUint64Array(count) if (!bitWidth) return output const { view } = reader const start = reader.offset const mask = (1n << BigInt(bitWidth)) - 1n let buffer = 0n let bitsInBuffer = 0 let byteIndex = start for (let i = 0; i < count; i++) { while (bitsInBuffer < bitWidth) { buffer |= BigInt(view.getUint8(byteIndex++)) << BigInt(bitsInBuffer) bitsInBuffer += 8 } output[i] = buffer & mask buffer >>= BigInt(bitWidth) bitsInBuffer -= bitWidth } reader.offset = start + Math.ceil(count * bitWidth / 8) return output }