/* ============================================================================= * common_util_line_stack_frac.ts * 산출근거 분수 층(PLAN 845-2 · 요구 880 「분모 · 분자가 쌓여 층층이 → 행으로 나눔」) * 층 = 조판(`line_stack_tex`)과 같은 셈 — ÷ 하나 = 분수 한 층 · 감싸개(반올림 · 올림 · 버림)는 안 셈 * 기계경비식(`splitFrac`) = 한 줄에 분수 한 층까지: * ① 곱 · 나눗셈 사슬에 ÷ 가 둘 넘으면 「a ÷ b」 를 짝으로 나란히(겹 대신 · 값 같음) * ② 「…분자 ÷ …분모」 짝(그림 이름 머리가 줄 이름과 다름) = 중간 줄 「상여 = 16 ÷ 12」 * ③ 분수 안 덧셈 묶음이 또 분수를 품으면 = 중간 줄(그림 사전 `partName` — 「손료계수」) * ④ 그래도 남는 안쪽 분수 = 조판이 ÷ 한 줄로(`TexCtx.frac`) * 그리기만 — 문서 식 · 값 · 금액 불변 · 중간 줄 값 = 같은 식 조각을 같은 셈(`evalNode`) · 화면 없음(시험이 Node 로 돌림) * ========================================================================== */ import type { Node } from "./common_util_formula_doc"; import { evalNode, type Values } from "./common_util_formula_doc_view"; const HIDE = new Set(["반올림", "올림", "버림"]); const hidden = (n: Node): n is ["call", string, Node[]] => n[0] === "call" && HIDE.has(n[1]) && n[2].length >= 1 && n[2].length <= 2; /** 조판 분수 층 수 */ export function fracDepth(n: Node): number { switch (n[0]) { case "neg": case "pct": return fracDepth(n[1]); case "call": { const items = n[2]; return hidden(n) ? fracDepth(items[0]) : Math.max(0, ...items.map(fracDepth)); } case "bin": { const d = Math.max(fracDepth(n[2]), fracDepth(n[3])); return n[1] === "/" ? d + 1 : d; } default: return 0; } } /** 왼쪽으로 쌓인 곱 · 나눗셈 사슬 → [연산, 항] */ function chain(n: Node): ["*" | "/", Node][] { if (n[0] === "bin" && (n[1] === "*" || n[1] === "/")) return [...chain(n[2]), [n[1] as "*" | "/", n[3]]]; return [["*", n]]; } const product = (xs: Node[]): Node => xs.reduce((a, b) => ["bin", "*", a, b]); const isFrac = (n: Node): n is ["bin", string, Node, Node] => n[0] === "bin" && n[1] === "/"; /** ① ÷ 둘 넘는 사슬 = 「a ÷ b」 짝을 나란히 — 감싸개 · 괄호 안까지 */ export function regroup(n: Node): Node { switch (n[0]) { case "neg": return ["neg", regroup(n[1])]; case "pct": return ["pct", regroup(n[1])]; case "call": return ["call", n[1], n[2].map(regroup)]; case "bin": { const items = chain(n); if (items.length < 2 || items.filter(([o]) => o === "/").length < 2) return ["bin", n[1], regroup(n[2]), regroup(n[3])]; const out: Node[] = []; for (const [o, raw] of items) { const x = regroup(raw); const last = out[out.length - 1]; if (o === "*" || !last) out.push(x); else if (isFrac(last)) out[out.length - 1] = ["bin", "/", last[2], ["bin", "*", last[3], x]]; else out[out.length - 1] = ["bin", "/", last, x]; } return product(out); } default: return n; } } export interface FracRow { /** 중간 줄 이름(그림 전용 — 문서 기호 아님) */ id: string; node: Node; } const numOk = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v); /** 기계경비식 한 줄 → 중간 줄 + 줄인 식 · 중간 값(`vals` 에 얹을 것) */ export function splitFrac( id: string, n: Node, alias: Record, taken: Set, vals: Values, part?: string, ): { rows: FracRow[]; node: Node; vals: Values } { const rows: FracRow[] = []; const got: Values = {}; const name = (x: string): string => alias[x] ?? x; const own = name(id); const take = (nm: string, sub: Node): Node | null => { if (!nm || taken.has(nm) || nm in got) return null; const v = evalNode(sub, { ...vals, ...got }); if (!numOk(v)) return null; got[nm] = v; rows.push({ id: nm, node: sub }); return ["var", nm]; }; // 감싸개(버림 등)는 그대로 두고 안쪽만 const inner = (x: Node, fn: (y: Node) => Node): Node => hidden(x) ? ["call", x[1], [inner(x[2][0], fn), ...x[2].slice(1)]] : fn(x); let node = inner(regroup(n), (x) => { // ② 분자 · 분모 짝(곱 사슬의 분수 항) const factors = (y: Node): Node[] => y[0] === "bin" && y[1] === "*" ? [...factors(y[2]), y[3]] : [y]; const parts = factors(x).map((f) => { if (!isFrac(f) || f[2][0] !== "var" || f[3][0] !== "var") return f; const a = /^(.+)분자$/.exec(name(f[2][1])); const b = /^(.+)분모$/.exec(name(f[3][1])); return a && b && a[1] === b[1] && a[1] !== own ? (take(a[1], f) ?? f) : f; }); return product(parts); }); // ③ 분수 안 덧셈 묶음이 분수를 품음 = 중간 줄(사전 이름 있을 때만) if (part && fracDepth(node) > 1) { const dig = (x: Node, inFrac: boolean): Node => { if (x[0] === "bin" && (x[1] === "+" || x[1] === "-") && inFrac && fracDepth(x) >= 1) return take(part, x) ?? x; if (x[0] === "bin") return ["bin", x[1], dig(x[2], inFrac || x[1] === "/"), dig(x[3], inFrac || x[1] === "/")]; // prettier-ignore if (x[0] === "call") return ["call", x[1], x[2].map((y) => dig(y, inFrac))]; if (x[0] === "neg" || x[0] === "pct") return [x[0], dig(x[1], inFrac)] as Node; return x; }; node = dig(node, false); } return { rows, node, vals: got }; }