numfmt — 분수 형식(0/0 · # ?/? · # ??/?? · 분모 고정 # ?/8) 엑셀과 같게 구현. 함수 — MROUND·ODD·EVEN·LOG·LOG10·LN·EXP(math) · SUMIFS·COUNTIFS·AVERAGEIF(S)·MAXIFS·MINIFS· IFS·SWITCH·XLOOKUP(찾기·조건부 집계) · TEXTJOIN·TRIM·UPPER·LOWER·SUBSTITUTE·REPLACE·FIND· SEARCH·REPT·ISNUMBER·ISTEXT·ISBLANK·ISERROR·NA·TODAY·DATE·YEAR·MONTH·DAY(misc) 더함. misc.ts 가 700 줄을 넘어 찾기·조건부 집계를 spreadsheet_functions_lookup.ts(새 파일)로 쪼갬. sub1 의 func_help 이름 목록에 더할 것을 spreadsheet_functions.ts 의 FUNC_HELP_BATCH2 에 둠 (그 파일은 안 고침 · 머리 주석에 읽는 길 적음). 경계값 시험 135건 통과. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrQ65VtGZbae2VKgYVMhmc
420 lines
18 KiB
TypeScript
420 lines
18 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("ROUND 음수 자릿수(십의 자리)", call("ROUND", n(1234), n(-2)), "1200");
|
|
fracEq("ROUNDUP 음수 자릿수", call("ROUNDUP", n(1234), n(-2)), "1300");
|
|
fracEq("ROUNDDOWN 음수 자릿수", call("ROUNDDOWN", n(1234), n(-2)), "1200");
|
|
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!");
|
|
|
|
// 2단계 첫 묶음(2026-09-27)
|
|
fracEq("MROUND", call("MROUND", n(10), n(3)), "9");
|
|
fracEq("MROUND 소수", call("MROUND", n("1.3"), n("0.2")), "1.4");
|
|
errEq("MROUND 부호 다름", call("MROUND", n(10), n(-3)), "#NUM!");
|
|
fracEq("ODD 양수", call("ODD", n("1.5")), "3");
|
|
fracEq("ODD 짝수 정수", call("ODD", n(2)), "3");
|
|
fracEq("ODD 0", call("ODD", n(0)), "1");
|
|
fracEq("ODD 음수", call("ODD", n(-2)), "-3");
|
|
fracEq("EVEN 양수", call("EVEN", n("1.5")), "2");
|
|
fracEq("EVEN 홀수 정수", call("EVEN", n(3)), "4");
|
|
fracEq("EVEN 음수", call("EVEN", n(-1)), "-2");
|
|
fracEq("LOG 밑 지정", call("LOG", n(8), n(2)), "3");
|
|
fracEq("LOG10", call("LOG10", n(100)), "2");
|
|
fracEq("LN(1) = 0", call("LN", n(1)), "0");
|
|
ok(
|
|
"EXP(1) ≈ e",
|
|
(() => {
|
|
const v = call("EXP", n(1));
|
|
return isFrac(v) && fracToString(v).startsWith("2.71828182845905");
|
|
})(),
|
|
);
|
|
errEq("LOG10 0 이하", call("LOG10", n(0)), "#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!");
|
|
textEq(
|
|
"TEXTJOIN 빈칸 건너뜀",
|
|
call("TEXTJOIN", txt("-"), bl(true), txt("a"), txt(""), txt("b")),
|
|
"a-b",
|
|
);
|
|
textEq("TRIM", call("TRIM", txt(" 가 나 ")), "가 나");
|
|
textEq("UPPER", call("UPPER", txt("abc")), "ABC");
|
|
textEq("LOWER", call("LOWER", txt("ABC")), "abc");
|
|
textEq("SUBSTITUTE 전부", call("SUBSTITUTE", txt("abcabc"), txt("a"), txt("X")), "XbcXbc");
|
|
textEq("SUBSTITUTE 몇번째", call("SUBSTITUTE", txt("abcabc"), txt("a"), txt("X"), n(2)), "abcXbc");
|
|
textEq("REPLACE", call("REPLACE", txt("abcdef"), n(2), n(3), txt("XYZ")), "aXYZef");
|
|
fracEq("FIND", call("FIND", txt("c"), txt("abcabc")), "3");
|
|
errEq("FIND 대소문자 가림", call("FIND", txt("C"), txt("abcabc")), "#VALUE!");
|
|
fracEq("SEARCH 대소문자 안가림", call("SEARCH", txt("C"), txt("abcabc")), "3");
|
|
fracEq("SEARCH 자리표시(*)", call("SEARCH", txt("a*c"), txt("xxabcyy")), "3");
|
|
textEq("REPT", call("REPT", txt("ab"), n(3)), "ababab");
|
|
|
|
// ── 찾기 ────────────────────────────────────────────────────────────────
|
|
|
|
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",
|
|
);
|
|
|
|
// 2단계 첫 묶음 — 여러 조건 · IFS 계열 · XLOOKUP
|
|
const NAMES4 = range(1, 4, [["가", "나", "가", "다"]]);
|
|
const AMOUNTS4 = range(1, 4, [[frac(10n), frac(20n), frac(30n), frac(40n)]]);
|
|
const AMOUNTS4B = range(1, 4, [[frac(1n), frac(2n), frac(3n), frac(4n)]]);
|
|
fracEq("SUMIFS 두 조건", call("SUMIFS", AMOUNTS4, NAMES4, txt("가"), AMOUNTS4B, txt(">1")), "30");
|
|
fracEq("COUNTIFS 두 조건", call("COUNTIFS", NAMES4, txt("가"), AMOUNTS4B, txt(">1")), "1");
|
|
fracEq("AVERAGEIF", call("AVERAGEIF", AMOUNTS4, txt(">15")), "30");
|
|
fracEq("AVERAGEIFS", call("AVERAGEIFS", AMOUNTS4, NAMES4, txt("가")), "20");
|
|
fracEq("MAXIFS", call("MAXIFS", AMOUNTS4, NAMES4, txt("가")), "30");
|
|
fracEq("MINIFS", call("MINIFS", AMOUNTS4, NAMES4, txt("가")), "10");
|
|
fracEq("IFS 처음 참", call("IFS", bl(false), n(1), bl(true), n(2), bl(true), n(3)), "2");
|
|
textEq(
|
|
"SWITCH 맞음",
|
|
call("SWITCH", n(2), n(1), txt("one"), n(2), txt("two"), n(3), txt("three")),
|
|
"two",
|
|
);
|
|
textEq("SWITCH 기본값", call("SWITCH", n(9), n(1), txt("one"), txt("default")), "default");
|
|
errEq("SWITCH 안 맞음(기본값 없음)", call("SWITCH", n(9), n(1), txt("one")), "#N/A");
|
|
const XNAMES = range(1, 3, [["가", "나", "다"]]);
|
|
const XAMOUNTS = range(1, 3, [[frac(10n), frac(20n), frac(30n)]]);
|
|
fracEq("XLOOKUP", call("XLOOKUP", txt("나"), XNAMES, XAMOUNTS), "20");
|
|
textEq("XLOOKUP 못찾음 대체값", call("XLOOKUP", txt("없음"), XNAMES, XAMOUNTS, txt("no")), "no");
|
|
errEq("XLOOKUP 못찾음 기본", call("XLOOKUP", txt("없음"), XNAMES, XAMOUNTS), "#N/A");
|
|
|
|
// 정보 · 날짜
|
|
boolEq("ISNUMBER 참", call("ISNUMBER", n(5)) as boolean, true);
|
|
boolEq("ISNUMBER 거짓(글)", call("ISNUMBER", txt("x")) as boolean, false);
|
|
boolEq("ISTEXT 참", call("ISTEXT", txt("x")) as boolean, true);
|
|
boolEq("ISBLANK 참", call("ISBLANK", lit(null)) as boolean, true);
|
|
boolEq("ISBLANK 거짓(0)", call("ISBLANK", n(0)) as boolean, false);
|
|
boolEq("ISERROR 참", call("ISERROR", lit({ error: "#DIV/0!" } as ErrorValue)) as boolean, true);
|
|
boolEq("ISERROR 거짓", call("ISERROR", n(1)) as boolean, false);
|
|
errEq("NA", call("NA"), "#N/A");
|
|
{
|
|
const serial = call("DATE", n(2026), n(1), n(15));
|
|
fracEq("YEAR(DATE(2026,1,15))", call("YEAR", lit(serial)), "2026");
|
|
fracEq("MONTH(DATE(2026,1,15))", call("MONTH", lit(serial)), "1");
|
|
fracEq("DAY(DATE(2026,1,15))", call("DAY", lit(serial)), "15");
|
|
}
|
|
{
|
|
const todaySerial = call("TODAY");
|
|
const wantYear = new Date().getUTCFullYear();
|
|
fracEq("YEAR(TODAY()) = 지금 연도", call("YEAR", lit(todaySerial)), String(wantYear));
|
|
}
|
|
|
|
// ── 숫자 형식 ────────────────────────────────────────────────────────────
|
|
|
|
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%"));
|
|
|
|
// 실무 구조도 xlsx 세 벌 실측 코드(2026-09-27 대조) — 회귀 방지.
|
|
ok(
|
|
"_x 폭띄움 끝쉼표",
|
|
fmt(frac(3n), "0.00_ ") === "3.00 ",
|
|
JSON.stringify(fmt(frac(3n), "0.00_ ")),
|
|
);
|
|
ok(
|
|
"_x 글에는 안 먹음(순수 수 형식)",
|
|
fmt("외부벽체", "0.00_ ") === "외부벽체",
|
|
fmt("외부벽체", "0.00_ "),
|
|
);
|
|
ok(
|
|
"General 단위접미(실무 버릇)",
|
|
fmt(frac(4n), 'General"열"') === "4열",
|
|
fmt(frac(4n), 'General"열"'),
|
|
);
|
|
ok(
|
|
"회계형(끝 구역 글)",
|
|
fmt(frac(1044n, 1000n), '_-* #,##0.00_-;\\-* #,##0.00_-;_-* "-"_-;_-@_-') === " 1.04 ",
|
|
fmt(frac(1044n, 1000n), '_-* #,##0.00_-;\\-* #,##0.00_-;_-* "-"_-;_-@_-'),
|
|
);
|
|
ok(
|
|
"통화 태그([$기호-로캘])",
|
|
fmt(frac(2n), '_-[$€-2]* #,##0.00_-;-[$€-2]* #,##0.00_-;_-[$€-2]* "-"??_-') === " €2.00 ",
|
|
fmt(frac(2n), '_-[$€-2]* #,##0.00_-;-[$€-2]* #,##0.00_-;_-[$€-2]* "-"??_-'),
|
|
);
|
|
ok("따옴표 접두·접미", fmt(frac(800n), '"φ"###') === "φ800", fmt(frac(800n), '"φ"###'));
|
|
ok(
|
|
"음수 회계형",
|
|
fmt(frac(-150n, 100n), "0.00_);[Red](0.00)") === "(1.50)",
|
|
fmt(frac(-150n, 100n), "0.00_);[Red](0.00)"),
|
|
);
|
|
|
|
// 분수 형식
|
|
ok("분수 가분수(자동 분모)", fmt(frac(1n, 2n), "0/0") === "1/2", fmt(frac(1n, 2n), "0/0"));
|
|
ok(
|
|
"분수 정수부+분자(자동 분모)",
|
|
fmt(frac(5n, 2n), "# ?/?") === "2 1/2",
|
|
fmt(frac(5n, 2n), "# ?/?"),
|
|
);
|
|
ok("분수 분모 고정", fmt(frac(4n, 10n), "# ?/8") === " 3/8", fmt(frac(4n, 10n), "# ?/8"));
|
|
ok("분수 정확히 나눠떨어짐(정수만)", fmt(frac(3n), "# ?/?") === "3", fmt(frac(3n), "# ?/?"));
|
|
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),
|
|
);
|
|
}
|
|
{
|
|
const book = bookWith({ B1: { 식: "A1*2" } });
|
|
const out = fillCells(book, "s1", { r0: 0, c0: 1, r1: 0, c1: 1 }, { r0: 0, c0: 1, r1: 2, c1: 1 });
|
|
ok(
|
|
"채우기 식 상대 이동(A moveFormula)",
|
|
out.B2?.식 === "A2*2" && out.B3?.식 === "A3*2",
|
|
JSON.stringify(out),
|
|
);
|
|
}
|
|
|
|
// ── 요약 ────────────────────────────────────────────────────────────────
|
|
|
|
console.log(JSON.stringify({ pass, fail, failures }, null, 2));
|
|
if (fail > 0) process.exitCode = 1;
|