Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1

This commit is contained in:
2026-09-25 17:04:52 +09:00
3 changed files with 54 additions and 3 deletions
+20
View File
@@ -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)
@@ -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<string, number>();
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;
}
@@ -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");
@@ -240,7 +240,11 @@ export function renderSheet(state: RenderState): HTMLElement {
cols.forEach((col, j) => {
const td = bodyCell(`t:${total.id}`, col, j, shown(state.result.합계[total.id]?.[col.id]));
const formula = totalFormula(total, col);
if (formula) td.title = formulaLabel(formula, names);
if (formula)
td.title = td.classList.contains("is-error")
? `${td.title}
${formulaLabel(formula, names)}`
: formulaLabel(formula, names);
tr.append(td);
});
return tr;