수학 26 · 논리/글/찾기/세기 22 함수 · 엑셀 숫자 형식 코드 해석기 · 채우기 핸들(복사 · 등차 연속 · 글 끝번호 · 식 상대이동) 구현. 자기 시험 69건(resources/tester/test_spreadsheet_b.py, vite ssr 번들 → node) 전부 통과. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrQ65VtGZbae2VKgYVMhmc
286 lines
12 KiB
TypeScript
286 lines
12 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_b_test_entry.ts (B 자기 시험)
|
|
* B 가 채운 함수 · 숫자 형식 · 채우기를 Node 로 바로 돌리는 시험 진입.
|
|
* `npm run` 스크립트 없이 vite `--ssr` 로 번들해 node 로 실행(F 의 build:spreadsheet 앞).
|
|
* 경계값은 실제 엑셀 결과와 대조(수학 함수 다수는 손으로 계산해 확인한 값).
|
|
* ========================================================================== */
|
|
|
|
import { frac, fracToString, toFrac, type Frac } from "@ui/sheet/ui_template_sheet_frac";
|
|
import { getFunction, functionNames } from "../../../A00_Common/spreadsheet/spreadsheet_functions";
|
|
import { formatValue } from "../../../A00_Common/spreadsheet/spreadsheet_numfmt";
|
|
import { fillCells } from "../../../A00_Common/spreadsheet/spreadsheet_fill";
|
|
import type {
|
|
Cell,
|
|
ErrorValue,
|
|
EvalContext,
|
|
EvalResult,
|
|
LazyArg,
|
|
RangeValue,
|
|
Scalar,
|
|
Workbook,
|
|
} from "../../../A00_Common/spreadsheet/spreadsheet_types";
|
|
|
|
let pass = 0;
|
|
let fail = 0;
|
|
const failures: string[] = [];
|
|
|
|
function ok(name: string, cond: boolean, detail?: string): void {
|
|
if (cond) {
|
|
pass++;
|
|
} else {
|
|
fail++;
|
|
failures.push(detail ? `${name} — ${detail}` : name);
|
|
}
|
|
}
|
|
|
|
// ── 도우미 ───────────────────────────────────────────────────────────────
|
|
|
|
const DUMMY_CTX: EvalContext = {
|
|
sheet: "s1",
|
|
r: 0,
|
|
c: 0,
|
|
cell: () => null,
|
|
range: () => ({ kind: "range", rows: 0, cols: 0, at: () => null }),
|
|
sheetId: () => null,
|
|
};
|
|
|
|
function n(v: number | string): LazyArg {
|
|
const f = toFrac(v);
|
|
return () => f;
|
|
}
|
|
function lit(v: EvalResult): LazyArg {
|
|
return () => v;
|
|
}
|
|
function txt(v: string): LazyArg {
|
|
return () => v;
|
|
}
|
|
function bl(v: boolean): LazyArg {
|
|
return () => v;
|
|
}
|
|
function range(rows: number, cols: number, data: Scalar[][]): LazyArg {
|
|
const rv: RangeValue = { kind: "range", rows, cols, at: (r, c) => data[r][c] };
|
|
return () => rv;
|
|
}
|
|
|
|
function call(name: string, ...args: LazyArg[]): EvalResult {
|
|
const fn = getFunction(name);
|
|
if (!fn) throw new Error(`함수 없음: ${name}`);
|
|
return fn.call(args, DUMMY_CTX);
|
|
}
|
|
|
|
function isFrac(v: unknown): v is Frac {
|
|
return typeof v === "object" && v !== null && "n" in (v as Record<string, unknown>);
|
|
}
|
|
function isErr(v: unknown): v is ErrorValue {
|
|
return typeof v === "object" && v !== null && "error" in (v as Record<string, unknown>);
|
|
}
|
|
|
|
function fracEq(name: string, got: EvalResult, expect: string): void {
|
|
ok(
|
|
name,
|
|
isFrac(got) && fracToString(got) === expect,
|
|
`got=${isFrac(got) ? fracToString(got) : JSON.stringify(got)} want=${expect}`,
|
|
);
|
|
}
|
|
function textEq(name: string, got: EvalResult, expect: string): void {
|
|
ok(name, got === expect, `got=${JSON.stringify(got)} want=${expect}`);
|
|
}
|
|
function errEq(name: string, got: EvalResult, expect: string): void {
|
|
ok(name, isErr(got) && got.error === expect, `got=${JSON.stringify(got)} want=${expect}`);
|
|
}
|
|
function boolEq(name: string, got: EvalResult, expect: boolean): void {
|
|
ok(name, got === expect, `got=${JSON.stringify(got)} want=${expect}`);
|
|
}
|
|
|
|
// ── 함수 표 자체 ─────────────────────────────────────────────────────────
|
|
|
|
ok("함수 이름 45+ 개", functionNames().length >= 45, `count=${functionNames().length}`);
|
|
ok("모르는 함수 = undefined", getFunction("없는함수123") === undefined);
|
|
|
|
// ── 수학 · 끝수 (경계값은 엑셀 실측) ─────────────────────────────────────
|
|
|
|
fracEq("SUM", call("SUM", n(1), n(2), n(3)), "6");
|
|
fracEq("SUM 범위", call("SUM", range(2, 1, [[frac(1n)], [frac(2n)]])), "3");
|
|
fracEq("SUM 범위+글자 건너뜀", call("SUM", range(1, 2, [["x", frac(5n)]])), "5");
|
|
fracEq("PRODUCT", call("PRODUCT", n(2), n(3), n(4)), "24");
|
|
fracEq(
|
|
"SUMPRODUCT",
|
|
call("SUMPRODUCT", range(1, 2, [[frac(2n), frac(3n)]]), range(1, 2, [[frac(4n), frac(5n)]])),
|
|
"23",
|
|
); // 2*4+3*5
|
|
fracEq("ROUND", call("ROUND", n("1.155"), n(2)), "1.16"); // 분수 반올림 — 부동소수 함정 없음
|
|
fracEq("ROUNDUP", call("ROUNDUP", n("1.001"), n(2)), "1.01");
|
|
fracEq("ROUNDDOWN", call("ROUNDDOWN", n("1.999"), n(2)), "1.99");
|
|
fracEq("INT 양수", call("INT", n("8.9")), "8");
|
|
fracEq("INT 음수(floor)", call("INT", n("-8.9")), "-9");
|
|
fracEq("TRUNC", call("TRUNC", n("8.9")), "8");
|
|
fracEq("CEILING", call("CEILING", n("2.5"), n(1)), "3");
|
|
fracEq("FLOOR", call("FLOOR", n("2.5"), n(1)), "2");
|
|
fracEq("ABS", call("ABS", n("-3.5")), "3.5");
|
|
fracEq("MOD 양수", call("MOD", n(7), n(3)), "1");
|
|
fracEq("MOD 음수 나눔값 부호", call("MOD", n(-7), n(3)), "2"); // 엑셀 MOD(-7,3)=2
|
|
fracEq("POWER 정수", call("POWER", n(2), n(10)), "1024");
|
|
fracEq("POWER 음수지수", call("POWER", n(2), n(-1)), "0.5");
|
|
ok(
|
|
"SQRT(2) 유효 15 자리",
|
|
(() => {
|
|
const v = call("SQRT", n(2));
|
|
return isFrac(v) && fracToString(v).startsWith("1.4142135623731");
|
|
})(),
|
|
);
|
|
ok(
|
|
"PI() 근사",
|
|
(() => {
|
|
const v = call("PI");
|
|
return isFrac(v) && fracToString(v).startsWith("3.14159265358979");
|
|
})(),
|
|
);
|
|
fracEq("MIN", call("MIN", n(5), n(2), n(9)), "2");
|
|
fracEq("MAX", call("MAX", n(5), n(2), n(9)), "9");
|
|
fracEq("AVERAGE", call("AVERAGE", n(2), n(4), n(6)), "4");
|
|
errEq("AVERAGE 빈 값 없음", call("AVERAGE", range(1, 1, [["x"]])), "#DIV/0!");
|
|
errEq("MOD 0 나눔", call("MOD", n(1), n(0)), "#DIV/0!");
|
|
errEq("CEILING 부호 다름", call("CEILING", n(-2), n(1)), "#NUM!");
|
|
|
|
// ── 논리 · 글 ────────────────────────────────────────────────────────────
|
|
|
|
fracEq("IF 참 값", call("IF", bl(true), n(1), n(2)), "1");
|
|
fracEq("IF 거짓 값", call("IF", bl(false), n(1), n(2)), "2");
|
|
boolEq("AND", call("AND", bl(true), bl(true), n(1)) as boolean, true);
|
|
boolEq("OR", call("OR", bl(false), n(0), n(1)) as boolean, true);
|
|
boolEq("NOT", call("NOT", bl(true)) as boolean, false);
|
|
fracEq("IFERROR 정상값", call("IFERROR", n(5), n(0)), "5");
|
|
fracEq("IFERROR 오류 대체", call("IFERROR", lit({ error: "#DIV/0!" }), n(9)), "9");
|
|
textEq("CONCATENATE", call("CONCATENATE", txt("가"), txt("나"), n(1)), "가나1");
|
|
textEq("FIXED 기본", call("FIXED", n("1234.5")), "1,234.50");
|
|
textEq("FIXED 콤마 없음", call("FIXED", n("1234.5"), n(1), bl(true)), "1234.5");
|
|
textEq("TEXT", call("TEXT", n("1234.5"), txt("#,##0.00")), "1,234.50");
|
|
fracEq("LEN", call("LEN", txt("가나다")), "3");
|
|
textEq("LEFT", call("LEFT", txt("가나다"), n(2)), "가나");
|
|
textEq("RIGHT", call("RIGHT", txt("가나다"), n(2)), "나다");
|
|
textEq("MID", call("MID", txt("가나다라"), n(2), n(2)), "나다");
|
|
fracEq("VALUE", call("VALUE", txt("1,234.5")), "1234.5");
|
|
errEq("VALUE 실패", call("VALUE", txt("가나다")), "#VALUE!");
|
|
|
|
// ── 찾기 ────────────────────────────────────────────────────────────────
|
|
|
|
const TABLE = range(3, 2, [
|
|
[frac(1n), "일"],
|
|
[frac(2n), "이"],
|
|
[frac(3n), "삼"],
|
|
]);
|
|
textEq("VLOOKUP 정확", call("VLOOKUP", n(2), TABLE, n(2), bl(false)), "이");
|
|
errEq("VLOOKUP 없음", call("VLOOKUP", n(9), TABLE, n(2), bl(false)), "#N/A");
|
|
textEq("VLOOKUP 근사", call("VLOOKUP", n("2.5"), TABLE, n(2)), "이");
|
|
|
|
const HTABLE = range(2, 3, [
|
|
[frac(1n), frac(2n), frac(3n)],
|
|
["일", "이", "삼"],
|
|
]);
|
|
textEq("HLOOKUP", call("HLOOKUP", n(2), HTABLE, n(2), bl(false)), "이");
|
|
|
|
fracEq("INDEX 칸", call("INDEX", TABLE, n(2), n(1)), "2");
|
|
fracEq(
|
|
"MATCH 오름차순",
|
|
call("MATCH", n("2.5"), range(3, 1, [[frac(1n)], [frac(2n)], [frac(3n)]]), n(1)),
|
|
"2",
|
|
);
|
|
textEq("CHOOSE", call("CHOOSE", n(2), txt("가"), txt("나"), txt("다")), "나");
|
|
|
|
// ── 세기 ────────────────────────────────────────────────────────────────
|
|
|
|
fracEq("COUNT", call("COUNT", range(1, 3, [[frac(1n), "x", frac(2n)]])), "2");
|
|
fracEq("COUNTA", call("COUNTA", range(1, 3, [[frac(1n), "x", null]])), "2");
|
|
fracEq(
|
|
"COUNTIF 조건",
|
|
call("COUNTIF", range(1, 4, [[frac(1n), frac(2n), frac(3n), frac(4n)]]), txt(">2")),
|
|
"2",
|
|
);
|
|
fracEq(
|
|
"SUMIF",
|
|
call(
|
|
"SUMIF",
|
|
range(1, 3, [["가", "나", "가"]]),
|
|
txt("가"),
|
|
range(1, 3, [[frac(10n), frac(20n), frac(30n)]]),
|
|
),
|
|
"40",
|
|
);
|
|
|
|
// ── 숫자 형식 ────────────────────────────────────────────────────────────
|
|
|
|
function fmt(value: Scalar, code: string): string {
|
|
return formatValue(value, code).글;
|
|
}
|
|
ok("0.00", fmt(frac(BigInt(150), 100n), "0.00") === "1.50", fmt(frac(150n, 100n), "0.00"));
|
|
ok("#,##0", fmt(frac(1234567n), "#,##0") === "1,234,567", fmt(frac(1234567n), "#,##0"));
|
|
ok("천 나눔 끝쉼표", fmt(frac(1234000n), "#,##0,") === "1,234", fmt(frac(1234000n), "#,##0,"));
|
|
ok("퍼센트", fmt(frac(50n, 100n), "0%") === "50%", fmt(frac(50n, 100n), "0%"));
|
|
ok(
|
|
"음수 회계형",
|
|
fmt(frac(-150n, 100n), "0.00_);[Red](0.00)") === "(1.50)",
|
|
fmt(frac(-150n, 100n), "0.00_);[Red](0.00)"),
|
|
);
|
|
ok("글자 형식 @", fmt("abc", "@@") === "abcabc", fmt("abc", "@@"));
|
|
ok(
|
|
"일반(General)",
|
|
fmt(frac(15n, 10n), undefined as unknown as string) === "1.5",
|
|
fmt(frac(15n, 10n), undefined as unknown as string),
|
|
);
|
|
ok(
|
|
"오류 값은 코드 글 그대로",
|
|
fmt({ error: "#DIV/0!" }, "0.00") === "#DIV/0!",
|
|
fmt({ error: "#DIV/0!" }, "0.00"),
|
|
);
|
|
ok("참거짓은 형식 무시", fmt(true, "0.00") === "TRUE", fmt(true, "0.00"));
|
|
|
|
// ── 채우기 ──────────────────────────────────────────────────────────────
|
|
|
|
function bookWith(cells: Record<string, Cell>): Workbook {
|
|
return {
|
|
종류: "통합문서",
|
|
판: 1,
|
|
열: "test",
|
|
서식: [{}],
|
|
시트: [{ id: "s1", 이름: "s1", 칸: cells }],
|
|
};
|
|
}
|
|
|
|
{
|
|
const book = bookWith({ A1: { 값: 5 } });
|
|
const out = fillCells(book, "s1", { r0: 0, c0: 0, r1: 0, c1: 0 }, { r0: 0, c0: 0, r1: 3, c1: 0 });
|
|
ok(
|
|
"채우기 수 하나 = 복사",
|
|
out.A2?.값 === 5 && out.A3?.값 === 5 && out.A4?.값 === 5,
|
|
JSON.stringify(out),
|
|
);
|
|
}
|
|
{
|
|
const book = bookWith({ A1: { 값: 1 }, A2: { 값: 2 } });
|
|
const out = fillCells(book, "s1", { r0: 0, c0: 0, r1: 1, c1: 0 }, { r0: 0, c0: 0, r1: 4, c1: 0 });
|
|
ok(
|
|
"채우기 등차 연속",
|
|
out.A3?.값 === 3 && out.A4?.값 === 4 && out.A5?.값 === 5,
|
|
JSON.stringify(out),
|
|
);
|
|
}
|
|
{
|
|
const book = bookWith({ A1: { 값: "구-01" } });
|
|
const out = fillCells(book, "s1", { r0: 0, c0: 0, r1: 0, c1: 0 }, { r0: 0, c0: 0, r1: 2, c1: 0 });
|
|
ok("채우기 글 끝 번호", out.A2?.값 === "구-02" && out.A3?.값 === "구-03", JSON.stringify(out));
|
|
}
|
|
{
|
|
const book = bookWith({ A1: { 값: "고정글" } });
|
|
const out = fillCells(book, "s1", { r0: 0, c0: 0, r1: 0, c1: 0 }, { r0: 0, c0: 0, r1: 2, c1: 0 });
|
|
ok(
|
|
"채우기 끝번호 없는 글 = 복사",
|
|
out.A2?.값 === "고정글" && out.A3?.값 === "고정글",
|
|
JSON.stringify(out),
|
|
);
|
|
}
|
|
|
|
// ── 요약 ────────────────────────────────────────────────────────────────
|
|
|
|
console.log(JSON.stringify({ pass, fail, failures }, null, 2));
|
|
if (fail > 0) process.exitCode = 1;
|