core/parsers/field.parser.ts

core/parsers/field.parser.ts is a file in GovLab Patterns. 116 lines of code and 31 definitions.

const FLOAT_MARKERS: ReadonlySet<string> = new Set([".", "e", "E"]);
const DIGITS: readonly string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
const NUMBER_START: ReadonlySet<string> = new Set(["-", ...DIGITS]);
const NUMBER_BODY: ReadonlySet<string> = new Set(["-", "+", ".", "e", "E", ...DIGITS]);
const WHITESPACE: ReadonlySet<string> = new Set([" ", "\t", "\r", "\n"]);
const STRUCTURE_OPEN: ReadonlySet<string> = new Set(["{", "["]);
const STRUCTURE_CLOSE: ReadonlySet<string> = new Set(["}", "]"]);
const KEY_SEPARATOR = ":";
const ITEM_SEPARATOR = ",";
const QUOTE = '"';
const ESCAPE = "\\";
const ESCAPE_SKIP = 2;

interface StringSpan {
    value: string;
    next: number;
}

interface LineState {
    depth: number;
    pendingKey: string | null;
    expectValue: boolean;
}

interface ScanContext {
    state: LineState;
    floats: Set<string>;
    recordDepth: number;
}

export const numberTokenIsFloat = function numberTokenIsFloat(token: string): boolean {
    for (const symbol of token) {
        if (FLOAT_MARKERS.has(symbol)) {
            return true;
        }
    }
    return false;
};

const appendChar = function appendChar(line: string, index: number, value: string): StringSpan {
    if (line[index] === ESCAPE) {
        return { next: index + ESCAPE_SKIP, value: value + (line[index + 1] ?? "") };
    }
    return { next: index + 1, value: value + (line[index] ?? "") };
};

const readString = function readString(line: string, start: number): StringSpan {
    let index = start + 1;
    let value = "";
    while (index < line.length && line[index] !== QUOTE) {
        ({ next: index, value } = appendChar(line, index, value));
    }
    return { next: index < line.length ? index + 1 : index, value };
};

const readNumber = function readNumber(line: string, start: number): StringSpan {
    let index = start;
    while (index < line.length && NUMBER_BODY.has(line[index] ?? "")) {
        index += 1;
    }
    return { next: index, value: line.slice(start, index) };
};

const handleNumber = function handleNumber(line: string, index: number, context: ScanContext): number {
    const { state, floats, recordDepth } = context;
    const { value, next } = readNumber(line, index);
    if (state.depth === recordDepth && state.pendingKey !== null && numberTokenIsFloat(value)) {
        floats.add(state.pendingKey);
    }
    state.expectValue = false;
    state.pendingKey = null;
    return next;
};

const handleString = function handleString(line: string, index: number, state: LineState): number {
    const { value, next } = readString(line, index);
    if (state.expectValue) {
        state.expectValue = false;
        state.pendingKey = null;
    } else {
        state.pendingKey = value;
    }
    return next;
};

const stepStructural = function stepStructural(symbol: string, state: LineState, index: number): number {
    if (symbol === KEY_SEPARATOR) {
        state.expectValue = true;
        return index + 1;
    }
    if (STRUCTURE_OPEN.has(symbol)) {
        state.depth += 1;
        state.expectValue = false;
        return index + 1;
    }
    if (STRUCTURE_CLOSE.has(symbol)) {
        state.depth -= 1;
        return index + 1;
    }
    if (symbol === ITEM_SEPARATOR) {
        state.pendingKey = null;
    }
    if (!WHITESPACE.has(symbol)) {
        state.expectValue = false;
    }
    return index + 1;
};

const stepScan = function stepScan(text: string, index: number, context: ScanContext): number {
    const symbol = text[index] ?? "";
    if (symbol === QUOTE) {
        return handleString(text, index, context.state);
    }
    if (context.state.expectValue && NUMBER_START.has(symbol)) {
        return handleNumber(text, index, context);
    }
    return stepStructural(symbol, context.state, index);
};

export const scanFloatFields = function scanFloatFields(text: string, recordDepth: number): Set<string> {
    const floats = new Set<string>();
    const context: ScanContext = { floats, recordDepth, state: { depth: 0, expectValue: false, pendingKey: null } };
    let index = 0;
    while (index < text.length) {
        index = stepScan(text, index, context);
    }
    return floats;
};