feat(spreadsheet): 분수 형식 · 함수 2차 묶음(30개) · misc 파일 쪼갬

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
This commit is contained in:
2026-09-27 20:59:21 +09:00
co-authored by Claude Sonnet 5
parent 0415177f44
commit d60b1ccc7e
6 changed files with 1148 additions and 274 deletions
@@ -415,4 +415,76 @@ export const MATH_FUNCTIONS: Record<string, SheetFunction> = {
return div(values.reduce(add, ZERO), frac(BigInt(values.length)));
},
},
MROUND: {
min: 2,
max: 2,
call: (args) => {
const n = num(args[0]());
if (isErr(n)) return n;
const m = num(args[1]());
if (isErr(m)) return m;
if (cmp(m, ZERO) === 0) return ZERO;
if (cmp(n, ZERO) !== 0 && cmp(n, ZERO) > 0 !== cmp(m, ZERO) > 0) {
return mkErr("#NUM!", "수 · 배수의 부호가 다름");
}
return mul(frac(toInteger(div(n, m), "round")), m);
},
},
ODD: {
min: 1,
max: 1,
call: (args) => {
const n = num(args[0]());
if (isErr(n)) return n;
let v = toInteger(n, "away");
if (v === 0n) v = 1n;
else if (v % 2n === 0n) v += v < 0n ? -1n : 1n;
return frac(v);
},
},
EVEN: {
min: 1,
max: 1,
call: (args) => {
const n = num(args[0]());
if (isErr(n)) return n;
let v = toInteger(n, "away");
if (v % 2n !== 0n) v += v < 0n ? -1n : 1n;
return frac(v);
},
},
LOG: {
min: 1,
max: 2,
call: (args) => {
const n = num(args[0]());
if (isErr(n)) return n;
if (cmp(n, ZERO) <= 0) return mkErr("#NUM!");
const base = args.length > 1 ? num(args[1]()) : frac(10n);
if (isErr(base)) return base;
if (cmp(base, ZERO) <= 0 || cmp(base, ONE) === 0) return mkErr("#NUM!");
return fromDouble(Math.log(fracToDouble(n)) / Math.log(fracToDouble(base)));
},
},
LOG10: {
min: 1,
max: 1,
call: (args) => {
const n = num(args[0]());
if (isErr(n)) return n;
if (cmp(n, ZERO) <= 0) return mkErr("#NUM!");
return fromDouble(Math.log10(fracToDouble(n)));
},
},
LN: {
min: 1,
max: 1,
call: (args) => {
const n = num(args[0]());
if (isErr(n)) return n;
if (cmp(n, ZERO) <= 0) return mkErr("#NUM!");
return fromDouble(Math.log(fracToDouble(n)));
},
},
EXP: unaryDouble(Math.exp),
};