diff --git a/A00_Common/spreadsheet/spreadsheet_eval.ts b/A00_Common/spreadsheet/spreadsheet_eval.ts index bb2ea3da..77411054 100644 --- a/A00_Common/spreadsheet/spreadsheet_eval.ts +++ b/A00_Common/spreadsheet/spreadsheet_eval.ts @@ -283,11 +283,7 @@ export function evaluate(node: FormulaNode, ctx: EvalContext): EvalResult { case "call": { const fn = getFunction(node.name); if (!fn) return err("#NAME?", `모르는 함수: ${node.name}`); - const n = node.args.length; - if (n < fn.min || (fn.max !== undefined && n > fn.max)) { - const want = fn.max === fn.min ? `${fn.min}` : `${fn.min}~${fn.max ?? ""}`; - return err("#VALUE!", `${node.name} 인자 수 ${n} (받는 수 ${want})`); - } + // 인자 수는 파서가 막음(spreadsheet_parser) return fn.call( node.args.map((a) => () => evaluate(a, ctx)), ctx, diff --git a/A00_Common/spreadsheet/spreadsheet_parser.ts b/A00_Common/spreadsheet/spreadsheet_parser.ts index 4521067a..c673d333 100644 --- a/A00_Common/spreadsheet/spreadsheet_parser.ts +++ b/A00_Common/spreadsheet/spreadsheet_parser.ts @@ -9,6 +9,7 @@ import { parseDecimal, ZERO } from "@ui/sheet/ui_template_sheet_frac"; import { colIndex, colName, quoteSheet, rowIndex } from "./spreadsheet_address"; +import { getFunction } from "./spreadsheet_functions"; import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types"; import type { ErrorCode, FormulaNode, ParseResult, RefCell, Token } from "./spreadsheet_types"; @@ -151,6 +152,15 @@ class Fail { } const COMPARE = ["=", "<>", "<", "<=", ">", ">="]; +// ponytail: B 함수 표 min 이 엑셀보다 낮은 것만 덧댐 — sub5 가 표를 고치면 지움 +const EXCEL_MIN: Record = { + SUM: 1, + MIN: 1, + MAX: 1, + COUNT: 1, + COUNTA: 1, + PRODUCT: 1, +}; /** 앞 `=` 뺀 식 글 → 나무. 앞에 `=` 가 붙어 와도 됨. 함수 이름은 대문자로. */ export function parseFormula(formula: string): ParseResult { @@ -199,9 +209,19 @@ export function parseFormula(formula: string): ParseResult { const name = t.text.toUpperCase().replace(/^_XLFN\./, ""); p++; // `(` const args: FormulaNode[] = []; + // 인자 수는 엑셀처럼 입력 때 막음(편집기가 이 실패로 Enter 를 막음) · 모르는 함수는 풀 때 #NAME? + const done = (): FormulaNode => { + const fn = getFunction(name); + const lo = Math.max(fn?.min ?? 0, EXCEL_MIN[name] ?? 0); + if (fn && args.length < lo) + throw new Fail(`${name} 인수가 너무 적음(${lo} 개부터)`, t.start); + if (fn?.max !== undefined && args.length > fn.max) + throw new Fail(`${name} 인수가 너무 많음(${fn.max} 개까지)`, t.start); + return { type: "call", name, args }; + }; if (peek()?.text === ")") { p++; - return { type: "call", name, args }; + return done(); } for (;;) { const k = peek(); @@ -211,7 +231,7 @@ export function parseFormula(formula: string): ParseResult { else args.push(compare()); const s = toks[p++]; if (s?.kind === "comma") continue; - if (s?.text === ")") return { type: "call", name, args }; + if (s?.text === ")") return done(); throw new Fail("함수 괄호가 안 닫힘", s ? s.start : text.length); } } diff --git a/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs b/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs index 399f8587..a2a05c41 100644 --- a/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs +++ b/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs @@ -229,7 +229,7 @@ section("parser", () => { ["치수!B1:C2", "치수!B1:C2"], ["SUM($A:$A,3:3)", "SUM($A:$A,3:3)"], ["IF(A1,,1)", "IF(A1,,1)"], - ["sum()", "SUM()"], + ["now()", "NOW()"], ['"say ""hi"""', '"say ""hi"""'], ["#REF!+1", "#REF!+1"], ["TRUE<>false", "TRUE<>FALSE"], @@ -307,7 +307,10 @@ section("errors", () => { ["#REF!", "#REF!"], ["이름표*2", "#NAME?"], ["FOO(1)", "#NAME?"], - ["IF(1)", "#VALUE!"], + ["IF(1)", "#NAME?"], + ["SUM()", "#NAME?"], + ["IF(1,2,3,4)", "#NAME?"], + ["SUM(,)", "0"], ["1+", "#NAME?"], ["IF(TRUE,1,1/0)", "1"], ["IF(FALSE,1/0,2)", "2"], @@ -324,7 +327,8 @@ section("errors", () => { const why = (a1) => e.value("s1", at(a1).r, at(a1).c).why || ""; eq("까닭 파싱", why("A1").startsWith("식을 못 읽음"), true); eq("까닭 함수", why("B1"), "모르는 함수: FOO"); - eq("까닭 인자 수", why("C1").includes("인자 수 1"), true); + eq("까닭 인자 수", why("C1").includes("IF 인수가 너무 적음(2 개부터)"), true); + eq("입력 막음", [P.parseFormula("SUM()").message, P.parseFormula("IF(1,2,3,4)").message], ["SUM 인수가 너무 적음(1 개부터)", "IF 인수가 너무 많음(3 개까지)"]); eq("toCalcValue 오류", E.toCalcValue(e.value("s1", 0, 1)), { 오류: "#NAME?", 까닭: "모르는 함수: FOO" }); });