/* ============================================================================= * spreadsheet_condition_text.ts * 「조건 만들기」 의 글 쪽(DOM 없음) — 줄들 ↔ IF 식 글 · 바깥 쉼표 가름 · 시험이 이 파일만 떼어 Node 로 돌림 * ========================================================================== */ export type CondOp = "=" | "≠" | "≥" | "≤" | ">" | "<"; export type CondOperand = { kind: "cell"; cell: string } | { kind: "value"; value: number | string }; export interface CondBranch { /** 비교하는 칸 */ left: string; op: CondOp; right: CondOperand; then: CondOperand; } export interface CondDraft { branches: CondBranch[]; otherwise: CondOperand; } export const XL_OP: Record = { "=": "=", "≠": "<>", "≥": ">=", "≤": "<=", ">": ">", "<": "<", }; export const FROM_XL: Record = { "=": "=", "<>": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<", }; export const COND_OPS: CondOp[] = ["=", "≠", "≥", "≤", ">", "<"]; export const CELL = /^\$?([A-Z]{1,3})\$?(\d+)$/; /* ── 글 ↔ 줄 (DOM 없음) ───────────────────────────────────────────────── */ export const operandText = (o: CondOperand): string => o.kind === "cell" ? o.cell : typeof o.value === "number" ? String(o.value) : `"${o.value.replace(/"/g, '""')}"`; /** 줄들 → 식 글(앞 = 뺌) — 아니면 만약 은 안쪽 IF */ export function draftToFormula(d: CondDraft): string { return d.branches.reduceRight( (rest, b) => `IF(${b.left}${XL_OP[b.op]}${operandText(b.right)},${operandText(b.then)},${rest})`, operandText(d.otherwise), ); } /** 바깥 쉼표로 가름 — 따옴표 · 괄호 안은 건너뜀 */ export function splitArgs(s: string): string[] { const out: string[] = []; let depth = 0; let quote = false; let from = 0; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (ch === '"') quote = !quote; else if (quote) continue; else if (ch === "(") depth++; else if (ch === ")") depth--; else if (ch === "," && depth === 0) { out.push(s.slice(from, i)); from = i + 1; } } out.push(s.slice(from)); return out; } export function parseOperand(raw: string): CondOperand | null { const t = raw.trim(); const cell = CELL.exec(t); if (cell) return { kind: "cell", cell: `${cell[1]}${cell[2]}` }; if (/^-?\d+(\.\d+)?$/.test(t)) return { kind: "value", value: Number(t) }; const q = /^"((?:[^"]|"")*)"$/.exec(t); return q ? { kind: "value", value: q[1].replace(/""/g, '"') } : null; } /** 바깥 괄호 하나로 감싼 IF( … ) 의 안쪽 글 — 아니면 null */ export function ifBody(f: string): string | null { const t = f.trim(); if (!/^IF\(/i.test(t) || !t.endsWith(")")) return null; let depth = 0; let quote = false; for (let i = 2; i < t.length; i++) { if (t[i] === '"') quote = !quote; else if (quote) continue; else if (t[i] === "(") depth++; else if (t[i] === ")" && --depth === 0 && i !== t.length - 1) return null; } return t.slice(3, -1); } /** 식 글(앞 = 뺌) → 줄들 — 이 판이 만드는 꼴(칸 비교 · 단순 값 · 아니면 쪽 중첩)만 읽고 나머지는 null */ export function formulaToDraft(formula: string): CondDraft | null { const body = ifBody(formula); if (body === null) return null; const args = splitArgs(body); if (args.length !== 3) return null; const m = /^\s*(.+?)\s*(>=|<=|<>|=|>|<)\s*(.+?)\s*$/.exec(args[0]); if (!m) return null; const left = parseOperand(m[1]); const right = parseOperand(m[3]); const then = parseOperand(args[1]); if (!left || left.kind !== "cell" || !right || !then) return null; const branch: CondBranch = { left: left.cell, op: FROM_XL[m[2]], right, then }; const plain = parseOperand(args[2]); if (plain) return { branches: [branch], otherwise: plain }; const rest = formulaToDraft(args[2]); return rest ? { branches: [branch, ...rest.branches], otherwise: rest.otherwise } : null; }