Files
Aislo/M01_MasterData/SheetLogic/M01_SL_Import_Parse.ts
T

187 lines
6.7 KiB
TypeScript

/* =============================================================================
* M01_SL_Import_Parse.ts
* 시트 로직 가져오기 — 엔진 문법(`master_formula._Parser`)으로 로직 식 글 읽기 · 나무 → `M01_SL_Import.ts` 가 엑셀 수식으로
* 꼴 = 수 · '글' · 이름 · 찾기(표, 가=나).칸 · 로직(키, 가=나).칸 · 만약/최소/최대/올림/버림/반올림 · 키:… 요소 ·
* - · * / · + - · 비교 · 그리고 · 또는 · 못 읽으면 null
* 아래 끝 = 품셈 표 → 시트 칸 놓기(조건 칸 → 값 칸 · 범위는 아래 · 위 두 칸) — 바깥 값 「표」 모양과 같음
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
export type Ast =
| { t: "num"; v: string }
| { t: "str"; v: string }
| { t: "name"; v: string }
| { t: "eref"; v: string }
| { t: "neg"; a: Ast }
| { t: "field"; a: Ast; f: string }
| { t: "call"; f: string; items: Ast[] }
| { t: "find"; head: string; args: [string, Ast][]; src: string }
| { t: "logic"; head: string; args: [string, Ast][]; src: string }
| { t: "bin"; op: string; a: Ast; b: Ast };
const CMPS = ["<=", ">=", "==", "!=", "<", ">"];
const CALLS = ["만약", "최소", "최대", "올림", "버림", "반올림"];
export function parseFormula(s: string): Ast | null {
let i = 0;
const fail = (): never => {
throw new Error("parse");
};
const ws = (): void => {
while (i < s.length && /\s/.test(s[i])) i++;
};
const take = (w: string): boolean => {
ws();
if (!s.startsWith(w, i)) return false;
i += w.length;
return true;
};
const need = (w: string): void => void (take(w) || fail());
const word = (): string => {
ws();
const m = /[\p{L}\p{N}_]+/uy;
m.lastIndex = i;
const got = m.exec(s) ?? fail();
i = m.lastIndex;
return got[0];
};
const quoted = (): string => {
const e = s.indexOf("'", i);
if (e < 0) fail();
const v = s.slice(i, e);
i = e + 1;
return v;
};
/** 로직 · 표 머리 글 — 「,」 「)」 에서 멈춤(괄호 · 중괄호 안은 넘김) */
const head = (): string => {
ws();
const st = i;
for (let d = 0; i < s.length; i++) {
const c = s[i];
if ("({".includes(c)) d++;
else if (")}".includes(c)) {
if (!d) break;
d--;
} else if (!d && c === ",") break;
}
return s.slice(st, i).trim();
};
const fields = (n: Ast): Ast => {
while (take(".")) n = { t: "field", a: n, f: take("'") ? quoted() : word() };
return n;
};
const primary = (): Ast => {
ws();
const st = i;
if (take("(")) {
const a = expr();
need(")");
return fields(a);
}
if (take("'")) return { t: "str", v: quoted() };
const num = /\d+(?:\.\d+)?/y;
num.lastIndex = i;
const n = num.exec(s);
if (n) {
i = num.lastIndex;
return { t: "num", v: n[0] };
}
const w = word();
if (w === "찾기" || w === "로직") {
need("(");
const h = head();
const args: [string, Ast][] = [];
while (take(",")) {
const k = take("'") ? quoted() : word();
need("=");
args.push([k, expr()]);
}
need(")");
const call = { head: h, args, src: s.slice(st, i) };
return fields(w === "찾기" ? { t: "find", ...call } : { t: "logic", ...call });
}
if (CALLS.includes(w)) {
need("(");
const items = [expr()];
while (take(",")) items.push(expr());
need(")");
return fields({ t: "call", f: w, items });
}
if (s[i] === ":") {
for (i++; i < s.length && !/[\s,)*/+-]/.test(s[i]); i++);
return { t: "eref", v: s.slice(st, i) };
}
return fields({ t: "name", v: w });
};
const unary = (): Ast => (take("-") ? { t: "neg", a: unary() } : primary());
const chain = (next: () => Ast, ops: string[], once = false): Ast => {
let n = next();
for (let op = ops.find(take); op; op = once ? undefined : ops.find(take))
n = { t: "bin", op, a: n, b: next() };
return n;
};
const mul = (): Ast => chain(unary, ["*", "/"]);
const add = (): Ast => chain(mul, ["+", "-"]);
const cmp = (): Ast => chain(add, CMPS, true);
const both = (): Ast => chain(cmp, ["그리고"]);
const expr = (): Ast => chain(both, ["또는"]);
try {
const node = expr();
ws();
return i === s.length ? node : null;
} catch {
return null;
}
}
/* ── 품셈 표 칸 놓기 ─────────────────────────────────────────────────────── */
/** 가져올 때 같이 받는 품셈 표 — 서버 `/table` 답의 쓸 칸만 */
export interface SlImportTable {
키?: string;
이름?: string;
조건: Record<string, string>;
값칸: Record<string, string>;
범위규칙?: string | null;
줄: Record<string, unknown>[];
}
/** 범위 칸 꼬리 — 한 · 영 */
const LOW_HIGH = currentLanguageIndex === 1 ? ["low", "high"] : ["아래", "위"];
export const RULES = ["초과·이하", "이상·이하", "이상·미만", "이하"];
/** 표 칸 놓기(서버 `M01_SL_Router._table` 과 같음 · 정함) — 조건 칸 → 값 칸 차례 · 범위(조건 「범위」 · 값 칸에
* 「20~45」 꼴이 한 줄이라도 있음)는 「이름·아래」 「이름·위」 두 칸 · 범위 칸에 숫자 하나면 두 칸에 같은 수 */
export function tableCols(
table: SlImportTable,
): { key: string; head: string; part: "v" | "lo" | "hi" }[] {
const out: { key: string; head: string; part: "v" | "lo" | "hi" }[] = [];
const conds = Object.keys(table.조건);
for (const k of [...conds, ...Object.keys(table.값칸)]) {
const split = conds.includes(k)
? table.조건[k] === "범위"
: table.줄.some((r) => Array.isArray(r[k]));
if (split) {
out.push({ key: k, head: `${k}·${LOW_HIGH[0]}`, part: "lo" });
out.push({ key: k, head: `${k}·${LOW_HIGH[1]}`, part: "hi" });
} else out.push({ key: k, head: k, part: "v" });
}
return out;
}
/** 품셈 표 → 시트 칸(E1 부터 · 머리 줄 + 줄) — 바깥 값 「표」 답(`rows`)과 같음 */
export function tableRows(table: SlImportTable): (string | number | null)[][] {
const cols = tableCols(table);
const one = (v: unknown, part: "v" | "lo" | "hi"): string | number | null => {
if (part !== "v") {
const x = Array.isArray(v) ? v[part === "lo" ? 0 : 1] : v;
return typeof x === "number" ? x : null;
}
if (Array.isArray(v)) return v.map((x) => x ?? "").join(",");
return typeof v === "number" || (typeof v === "string" && v !== "") ? v : null;
};
return [cols.map((c) => c.head), ...table.줄.map((r) => cols.map((c) => one(r[c.key], c.part)))];
}