utils.js

/**
 * Join paths while accounting for `.` inputs.
 *
 * @param {string} args - One or more strings containing path components.
 * These should not have leading or trailing `/` but may be equal to `.`
 *
 * @return The joined path.
 */
export function joinPath(...args) {
    let output = args[0];
    for (var i = 1; i < args.length; i++) {
        let current = args[i];
        if (output == ".") {
            output = current;
        } else if (current != ".") {
            output += "/" + current;
        }
    }
    return output;
}

export function exceedsInt32(x) {
    const upper = 2**31, lower = -upper;
    for (const v of x) {
        if (v !== null && (v < lower || v >= upper)) {
            return true;
        }
    }
    return false;
}

function substitutePlaceholder(x, placeholder, outclass) {
    let output = new outclass(x.length);
    x.forEach((y, i) => {
        output[i] = (y === null ? placeholder : y);
    });
    return output;
}

export function formatIntegerArrayForHdf5(x) {
    let has_missing = x.some(y => y == null);

    if (exceedsInt32(x)) {
        let placeholder = null;
        if (has_missing) {
            placeholder = Number.NaN;
            x = substitutePlaceholder(x, placeholder, Float64Array);
        }
        return { integer: false, data: x, placeholder: placeholder };
    }

    if (!has_missing) {
        return { integer: true, data: x, placeholder: null };
    }

    let output = { integer: true, data: null, placeholder: null };

    // Quickly searching some of the most obvious candidates.
    const upper = 2**31, lower = -upper;
    for (const candidate of [lower, upper - 1, 0]) {
        if (!x.some(y => y == candidate)) {
            output.placeholder = candidate;
            break;
        }
    }

    if (output.placeholder === null) {
        let everything = new Set(x);
        for (var i = lower + 1; i < upper - 1; i++) {
            if (!everything.has(i)) {
                output.placeholder = i;
                break;
            }
        }
    }

    if (output.placeholder === null) {
        output.integer = false;
        output.placeholder = Number.NaN;
    }

    output.data = substitutePlaceholder(x, output.placeholder, Int32Array);
    return output;
}

export function formatNumberArrayForHdf5(x) {
    if (!x.some(y => y == null)) {
        return { data: x, placeholder: null };
    }

    // Quickly searching some of the most obvious candidates.
    if (!x.some(Number.isNaN)) {
        return { data: substitutePlaceholder(x, Number.NaN, Float64Array), placeholder: Number.NaN };
    }

    for (const candidate of [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.MAX_VALUE, -Number.MAX_VALUE, 0]) {
        if (!x.some(y => y == candidate)) {
            return { data: substitutePlaceholder(x, candidate, Float64Array), placeholder: candidate };
        }
    }

    let previous = -Number.MAX_VALUE;
    let sorted = (new Float64Array(x.filter(Number.isFinite))).sort();
    let chosen;
    for (const y of sorted) {
        let candidate = previous + (y - previous) / 2;
        if (candidate != previous && candidate != y) {
            chosen = candidate;
            break;
        }
        previous = y;
    }

    return { data: substitutePlaceholder(x, chosen, Float64Array), placeholder: chosen };
}

export function formatStringArrayForHdf5(x) {
    if (!x.some(y => y == null)) {
        return { data: x, placeholder: null };
    }

    let placeholder = "NA";
    if (x.some(y => y == "NA")) { // Quickly searching the most obvious candidates.
        let everything = new Set(x);
        placeholder += "_";
        while (everything.has(placeholder)) {
            placeholder += "_";
        }
    }

    return { data: x.map(y => y == null ? placeholder : y), placeholder: placeholder };
}

export function formatBooleanArrayForHdf5(x) {
    return { 
        data: substitutePlaceholder(x, 2, Uint8Array),
        placeholder: (x.some(y => y == null) ? 2 : null)
    };
}

export function jsonBuffer(obj) {
    const str = JSON.stringify(obj);
    const enc = new TextEncoder;
    return enc.encode(str);
}