summarum/src/engine/lexer.ts
Horizontal42 0cef414e9a feat: Summarum — natural language calculator notebook for Windows
Write plain text, get live results per line. English + Russian, units,
live currency rates with crypto, percents, dates and timezones, bitwise
ops, variables and sheet totals. Tauri 2 + TypeScript engine +
CodeMirror 6; daily backups and a deleted-sheets bin.

Inspired by Numi for macOS (numi.app); built from scratch for Windows.
2026-06-10 22:19:24 +03:00

79 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Splits a line into raw lexemes (numbers, words, symbols). Shared by the
// tokenizer and the phrase registry so both segment text the same way.
import { Decimal, NumeralRepr } from "./types";
export interface Lex {
type: "num" | "word" | "sym";
raw: string;
start: number;
end: number;
value?: Decimal;
repr?: NumeralRepr;
}
const WORD_RE = /^[\p{L}_]+/u;
// Order matters: grouped thousands first, then plain decimal/exponent, then leading-dot.
const NUM_RE = /^(?:\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\.\d+)/;
export function lexLine(text: string): Lex[] {
const out: Lex[] = [];
let i = 0;
while (i < text.length) {
const ch = text[i];
if (ch === " " || ch === "\t" || ch === " ") {
i++;
continue;
}
const rest = text.slice(i);
if (ch >= "0" && ch <= "9") {
const hex = /^0[xX][0-9a-fA-F]+/.exec(rest);
if (hex) {
out.push({ type: "num", raw: hex[0], start: i, end: i + hex[0].length, value: new Decimal(parseInt(hex[0], 16)), repr: "hex" });
i += hex[0].length;
continue;
}
const bin = /^0[bB][01]+/.exec(rest);
if (bin) {
out.push({ type: "num", raw: bin[0], start: i, end: i + bin[0].length, value: new Decimal(parseInt(bin[0].slice(2), 2)), repr: "binary" });
i += bin[0].length;
continue;
}
const oct = /^0[oO][0-7]+/.exec(rest);
if (oct) {
out.push({ type: "num", raw: oct[0], start: i, end: i + oct[0].length, value: new Decimal(parseInt(oct[0].slice(2), 8)), repr: "octal" });
i += oct[0].length;
continue;
}
}
const num = NUM_RE.exec(rest);
if (num && (ch >= "0" && ch <= "9")) {
const raw = num[0];
out.push({ type: "num", raw, start: i, end: i + raw.length, value: new Decimal(raw.replace(/,/g, "")), repr: "decimal" });
i += raw.length;
continue;
}
if (ch === "." && /^\.\d/.test(rest)) {
const m = /^\.\d+/.exec(rest)!;
out.push({ type: "num", raw: m[0], start: i, end: i + m[0].length, value: new Decimal(m[0]), repr: "decimal" });
i += m[0].length;
continue;
}
const word = WORD_RE.exec(rest);
if (word) {
out.push({ type: "word", raw: word[0], start: i, end: i + word[0].length });
i += word[0].length;
continue;
}
out.push({ type: "sym", raw: ch, start: i, end: i + 1 });
i++;
}
return out;
}
/** Lexemes of a vocabulary phrase (positions are irrelevant). */
export function lexPhrase(phrase: string): string[] {
return lexLine(phrase).map((l) => l.raw);
}