- M01_SL_Import.ts — 로직 줄 → 시트 로직(상자 = 시트 · 알약 · 선) · 만약/최소/최대/올림/버림/반올림 → IF/MIN/MAX/ROUNDUP/ROUNDDOWN/ROUND · 찾기 → 표 상자(맞음 칸 + INDEX/MATCH) · 로직 → 다른 로직 상자 · 바깥 값 자리 outsideOf - M01_SL_Import_Parse.ts — 엔진 문법 식 읽기 · 품셈 표 칸 놓기 - 시험 test_m01_sl_import.py — 견본 6개 시트 셈 = /calc · 마스터 1,469 중 1,386 다 바뀜 · 바뀐 통합문서 #NAME? 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
184 lines
6.5 KiB
TypeScript
184 lines
6.5 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 = ["초과·이하", "이상·이하", "이상·미만", "이하"];
|
|
|
|
/** 표 칸 놓기 — 조건 칸 차례(범위는 아래 · 위 두 칸) → 값 칸 차례 */
|
|
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)))];
|
|
}
|