Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
/* =============================================================================
|
|
* 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<CondOp, string> = {
|
|
"=": "=",
|
|
"≠": "<>",
|
|
"≥": ">=",
|
|
"≤": "<=",
|
|
">": ">",
|
|
"<": "<",
|
|
};
|
|
export const FROM_XL: Record<string, CondOp> = {
|
|
"=": "=",
|
|
"<>": "≠",
|
|
">=": "≥",
|
|
"<=": "≤",
|
|
">": ">",
|
|
"<": "<",
|
|
};
|
|
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;
|
|
}
|