diff --git a/resources/tester/test_sheet_recalc.py b/resources/tester/test_sheet_recalc.py index 07b21e6f..c1aa94e3 100644 --- a/resources/tester/test_sheet_recalc.py +++ b/resources/tester/test_sheet_recalc.py @@ -193,3 +193,23 @@ def test_mirror_random_docs_match_decimal(): assert out["계산"] == {r: {n: _plain(v) for n, v in w.items()} for r, w in want.items()} for name, total in sums.items(): assert out["합계"]["s"][name] == _plain(total) + + +def test_text_in_number_column_is_flagged_and_skipped(): + doc = _doc( + [{"id": "a"}, {"id": "n", "꼴": "글"}, {"id": "f", "식": "[a]*2"}], + [ + {"id": "r1", "값": {"a": 5, "n": "abc"}}, + {"id": "r2", "값": {"a": "abc", "n": "x"}}, + {"id": "r3", "값": {"a": "1,000"}}, + {"id": "r4", "값": {"a": ""}}, + ], + 합계줄=[{"id": "t", "이름": "합계", "식": "SUM"}], + ) + out = _run(doc) + assert out["합계"]["t"]["a"] == "1005" # abc 는 빼고 · 1,000 은 수 · 빈 칸은 0 + errors = {(e["줄"], e["열"]): e["까닭"] for e in out["오류"]} + assert errors[("r2", "a")] == "수가 아님" + assert errors[("t", "a")].startswith("수가 아닌 칸 1 개") + # 글 칸 · 정상 수 칸은 표시 없음(r2 의 식 칸은 글을 곱해 식 오류 — 따로) + assert not any(k[1] == "n" or k[0] in ("r1", "r3", "r4") for k in errors) diff --git a/ui_template/sheet/ui_template_sheet_recalc.ts b/ui_template/sheet/ui_template_sheet_recalc.ts index a19e1026..50eba71d 100644 --- a/ui_template/sheet/ui_template_sheet_recalc.ts +++ b/ui_template/sheet/ui_template_sheet_recalc.ts @@ -34,6 +34,13 @@ import type { const ROUND_MODE = { 반올림: "round", 올림: "away", 버림: "trunc" } as const; +/** 수 칸에 수가 아닌 글 — 계산은 그 칸을 빼고 하되 알림(엑셀처럼 무시 · 오류 목록에는 이 까닭으로). */ +export const NOT_A_NUMBER = "수가 아님"; +export const SKIPPED_PREFIX = "수가 아닌 칸"; +/** 값은 그대로 두고 알리기만 하는 까닭인가 — 화면이 글을 「#오류」 로 바꾸지 않게. */ +export const isNotice = (why: string): boolean => + why === NOT_A_NUMBER || why.startsWith(SKIPPED_PREFIX); + export const isNumberColumn = (col: SheetColumn): boolean => col.꼴 !== "글"; /** 합계 줄 한 칸의 식 — 없으면 null(빈 칸). `"SUM"` 은 그 열의 열 합. */ @@ -167,6 +174,26 @@ export function recalcSheet(doc: SheetDoc): SheetResult { }; for (const id of rows.keys()) solve(id, result.계산); for (const id of totals.keys()) solve(id, result.합계); + + // 수 칸에 든 수 아닌 글 — 그 칸 + 그 열을 더하는 합계 칸에 알림 + const badCount = new Map(); + for (const row of doc.줄) { + for (const col of doc.열) { + if (!isNumberColumn(col) || formulaOf(row.id, col)) continue; + if (typeof inputValue(row.값[col.id]) !== "string") continue; + result.오류.push({ 줄: row.id, 열: col.id, 까닭: NOT_A_NUMBER }); + badCount.set(col.id, (badCount.get(col.id) ?? 0) + 1); + } + } + for (const total of totals.values()) { + for (const col of doc.열) { + const n = badCount.get(col.id); + const formula = n && totalFormula(total, col); + if (!formula || !formula.includes(`[${col.id}`)) continue; + const 까닭 = `${SKIPPED_PREFIX} ${n} 개는 빼고 더함`; + result.오류.push({ 줄: total.id, 열: col.id, 까닭 }); + } + } return result; } diff --git a/ui_template/sheet/ui_template_sheet_render.ts b/ui_template/sheet/ui_template_sheet_render.ts index 7bef596f..2ef7da45 100644 --- a/ui_template/sheet/ui_template_sheet_render.ts +++ b/ui_template/sheet/ui_template_sheet_render.ts @@ -18,7 +18,7 @@ import { pagePlan, type SheetMode, } from "./ui_template_sheet_ops"; -import { groupDigits, totalFormula } from "./ui_template_sheet_recalc"; +import { groupDigits, isNotice, totalFormula } from "./ui_template_sheet_recalc"; import { st } from "./ui_template_sheet_text"; import type { SheetColumn, SheetDoc, SheetResult, SheetRow } from "./ui_template_sheet_types"; @@ -130,7 +130,7 @@ export function renderSheet(state: RenderState): HTMLElement { const why = errors.get(`${key}|${col.id}`); if (why) { td.classList.add("is-error"); - td.textContent = st("Error"); + if (!isNotice(why)) td.textContent = st("Error"); td.title = why; } else if (text) td.title = text; if (state.sel?.r === key && state.sel.c === col.id) td.classList.add("is-selected");