/* ============================================================================= * 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; 값칸: Record; 범위규칙?: string | null; 줄: Record[]; } /** 범위 칸 꼬리 — 한 · 영 */ const LOW_HIGH = currentLanguageIndex === 1 ? ["low", "high"] : ["아래", "위"]; export const RULES = ["초과·이하", "이상·이하", "이상·미만", "이하"]; /** 표 칸 놓기 — 조건 칸 차례(범위는 아래 · 위 두 칸) → 값 칸 차례 */ export function tableCols( table: SlImportTable, ): { key: string; head: string; part: "v" | "lo" | "hi" }[] { const out: { key: string; head: string; part: "v" | "lo" | "hi" }[] = []; for (const [k, kind] of Object.entries(table.조건)) { if (kind === "범위") { 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" }); } for (const k of Object.keys(table.값칸)) if (!(k in table.조건)) out.push({ key: k, head: k, part: "v" }); return out; } /** 품셈 표 → 시트 칸(E1 부터 · 머리 줄 + 줄) — 바깥 값 모양(`M01_SL_Api` 「표」)과 같음 */ export function tableRows(table: SlImportTable): (string | number | null)[][] { const cols = tableCols(table); const one = (v: unknown, part: "v" | "lo" | "hi"): string | number | null => { if (Array.isArray(v)) { if (part === "v") return v.map((x) => x ?? "").join("~"); const x = v[part === "lo" ? 0 : 1]; return typeof x === "number" ? x : null; } return typeof v === "number" || typeof v === "string" ? v : null; }; return [cols.map((c) => c.head), ...table.줄.map((r) => cols.map((c) => one(r[c.key], c.part)))]; }