fix(spreadsheet): 함수 인자 수를 입력 때 막음 — =SUM() 이 0 이 되던 흠
- 파서가 함수 표(B) 최소 · 최대 인자 수로 검사 → 「SUM 인수가 너무 적음(1 개부터)」 식 오류(편집기가 이 실패로 Enter 를 막는 이음은 sub7) - B 표 min 0 인 SUM · MIN · MAX · COUNT · COUNTA · PRODUCT 는 엑셀대로 1 로 덧댐(sub5 가 표를 고치면 지움) - 풀이 때 인자 수 검사는 파서로 옮겨 뺌(엔진 31,787 → 31,603 B) - 시험: 엔진 오류 묶음에 인자 수 사례 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF
This commit is contained in:
@@ -283,11 +283,7 @@ export function evaluate(node: FormulaNode, ctx: EvalContext): EvalResult {
|
|||||||
case "call": {
|
case "call": {
|
||||||
const fn = getFunction(node.name);
|
const fn = getFunction(node.name);
|
||||||
if (!fn) return err("#NAME?", `모르는 함수: ${node.name}`);
|
if (!fn) return err("#NAME?", `모르는 함수: ${node.name}`);
|
||||||
const n = node.args.length;
|
// 인자 수는 파서가 막음(spreadsheet_parser)
|
||||||
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})`);
|
|
||||||
}
|
|
||||||
return fn.call(
|
return fn.call(
|
||||||
node.args.map((a) => () => evaluate(a, ctx)),
|
node.args.map((a) => () => evaluate(a, ctx)),
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { parseDecimal, ZERO } from "@ui/sheet/ui_template_sheet_frac";
|
import { parseDecimal, ZERO } from "@ui/sheet/ui_template_sheet_frac";
|
||||||
import { colIndex, colName, quoteSheet, rowIndex } from "./spreadsheet_address";
|
import { colIndex, colName, quoteSheet, rowIndex } from "./spreadsheet_address";
|
||||||
|
import { getFunction } from "./spreadsheet_functions";
|
||||||
import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types";
|
import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types";
|
||||||
import type { ErrorCode, FormulaNode, ParseResult, RefCell, Token } from "./spreadsheet_types";
|
import type { ErrorCode, FormulaNode, ParseResult, RefCell, Token } from "./spreadsheet_types";
|
||||||
|
|
||||||
@@ -151,6 +152,15 @@ class Fail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COMPARE = ["=", "<>", "<", "<=", ">", ">="];
|
const COMPARE = ["=", "<>", "<", "<=", ">", ">="];
|
||||||
|
// ponytail: B 함수 표 min 이 엑셀보다 낮은 것만 덧댐 — sub5 가 표를 고치면 지움
|
||||||
|
const EXCEL_MIN: Record<string, number> = {
|
||||||
|
SUM: 1,
|
||||||
|
MIN: 1,
|
||||||
|
MAX: 1,
|
||||||
|
COUNT: 1,
|
||||||
|
COUNTA: 1,
|
||||||
|
PRODUCT: 1,
|
||||||
|
};
|
||||||
|
|
||||||
/** 앞 `=` 뺀 식 글 → 나무. 앞에 `=` 가 붙어 와도 됨. 함수 이름은 대문자로. */
|
/** 앞 `=` 뺀 식 글 → 나무. 앞에 `=` 가 붙어 와도 됨. 함수 이름은 대문자로. */
|
||||||
export function parseFormula(formula: string): ParseResult {
|
export function parseFormula(formula: string): ParseResult {
|
||||||
@@ -199,9 +209,19 @@ export function parseFormula(formula: string): ParseResult {
|
|||||||
const name = t.text.toUpperCase().replace(/^_XLFN\./, "");
|
const name = t.text.toUpperCase().replace(/^_XLFN\./, "");
|
||||||
p++; // `(`
|
p++; // `(`
|
||||||
const args: FormulaNode[] = [];
|
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 === ")") {
|
if (peek()?.text === ")") {
|
||||||
p++;
|
p++;
|
||||||
return { type: "call", name, args };
|
return done();
|
||||||
}
|
}
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const k = peek();
|
const k = peek();
|
||||||
@@ -211,7 +231,7 @@ export function parseFormula(formula: string): ParseResult {
|
|||||||
else args.push(compare());
|
else args.push(compare());
|
||||||
const s = toks[p++];
|
const s = toks[p++];
|
||||||
if (s?.kind === "comma") continue;
|
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);
|
throw new Fail("함수 괄호가 안 닫힘", s ? s.start : text.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ section("parser", () => {
|
|||||||
["치수!B1:C2", "치수!B1:C2"],
|
["치수!B1:C2", "치수!B1:C2"],
|
||||||
["SUM($A:$A,3:3)", "SUM($A:$A,3:3)"],
|
["SUM($A:$A,3:3)", "SUM($A:$A,3:3)"],
|
||||||
["IF(A1,,1)", "IF(A1,,1)"],
|
["IF(A1,,1)", "IF(A1,,1)"],
|
||||||
["sum()", "SUM()"],
|
["now()", "NOW()"],
|
||||||
['"say ""hi"""', '"say ""hi"""'],
|
['"say ""hi"""', '"say ""hi"""'],
|
||||||
["#REF!+1", "#REF!+1"],
|
["#REF!+1", "#REF!+1"],
|
||||||
["TRUE<>false", "TRUE<>FALSE"],
|
["TRUE<>false", "TRUE<>FALSE"],
|
||||||
@@ -307,7 +307,10 @@ section("errors", () => {
|
|||||||
["#REF!", "#REF!"],
|
["#REF!", "#REF!"],
|
||||||
["이름표*2", "#NAME?"],
|
["이름표*2", "#NAME?"],
|
||||||
["FOO(1)", "#NAME?"],
|
["FOO(1)", "#NAME?"],
|
||||||
["IF(1)", "#VALUE!"],
|
["IF(1)", "#NAME?"],
|
||||||
|
["SUM()", "#NAME?"],
|
||||||
|
["IF(1,2,3,4)", "#NAME?"],
|
||||||
|
["SUM(,)", "0"],
|
||||||
["1+", "#NAME?"],
|
["1+", "#NAME?"],
|
||||||
["IF(TRUE,1,1/0)", "1"],
|
["IF(TRUE,1,1/0)", "1"],
|
||||||
["IF(FALSE,1/0,2)", "2"],
|
["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 || "";
|
const why = (a1) => e.value("s1", at(a1).r, at(a1).c).why || "";
|
||||||
eq("까닭 파싱", why("A1").startsWith("식을 못 읽음"), true);
|
eq("까닭 파싱", why("A1").startsWith("식을 못 읽음"), true);
|
||||||
eq("까닭 함수", why("B1"), "모르는 함수: FOO");
|
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" });
|
eq("toCalcValue 오류", E.toCalcValue(e.value("s1", 0, 1)), { 오류: "#NAME?", 까닭: "모르는 함수: FOO" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user