Merge remote-tracking branch 'origin/dev' into sub_laptop_6

This commit is contained in:
2026-09-27 22:33:02 +09:00
3 changed files with 30 additions and 10 deletions
+1 -5
View File
@@ -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,
+22 -2
View File
@@ -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<string, number> = {
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);
}
}
@@ -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" });
});