Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_2' and 'origin/sub_laptop_3' into sub_laptop_8
This commit is contained in:
@@ -164,4 +164,69 @@ section("errcheck", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// ── 4. 이름 정의 풀이 ────────────────────────────────────────────────────────
|
||||
section("names", () => {
|
||||
const { C, E, F } = require("./helper_spreadsheet_engine.cjs");
|
||||
const N = load("spreadsheet_names");
|
||||
const show = (v) => (v === null ? null : E.isError(v) ? v.error : E.isFrac(v) ? F.fracToString(v) : v);
|
||||
const b = book(
|
||||
{ A1: 1, A2: 2, A3: 3, B1: 10, C1: "=SUM(값들)", C2: "=단가*2", C3: "=없는이름+1", C4: "=SUM(값들)*단가" },
|
||||
{ D1: "=SUM(값들)", D2: "=SUM(둘값)", E1: 100 },
|
||||
);
|
||||
b.시트[0].이름정의 = [
|
||||
{ 이름: "값들", 범위: A.parseRange("A1:A3") },
|
||||
{ 이름: "단가", 범위: A.parseRange("B1") },
|
||||
];
|
||||
b.시트[1].이름정의 = [{ 이름: "둘값", 범위: A.parseRange("E1") }];
|
||||
const e = G.createCalcEngine(b);
|
||||
const v = (a1, s = "s1") => {
|
||||
const p = A.parseA1(a1);
|
||||
return show(e.value(s, p.r, p.c));
|
||||
};
|
||||
eq("풀이", ["C1", "C2", "C3", "C4"].map((a) => v(a)), ["6", "20", "#NAME?", "60"]);
|
||||
eq("다른 시트에서 · 자기 시트 이름", [v("D1", "s2"), v("D2", "s2")], ["6", "100"]);
|
||||
// 이름 범위 칸을 고치면 따라 풂
|
||||
C.applyCommand(b, { 종류: "칸", 시트: "s1", 칸: { A1: { 값: 5 } } });
|
||||
e.update([{ 시트: "s1", r: 0, c: 0 }]);
|
||||
eq("딸린 칸 다시 풂", [v("C1"), v("C4"), v("D1", "s2")], ["10", "100", "10"]);
|
||||
// 식 계산 걸음도 이름을 품
|
||||
const p = A.parseA1("C4");
|
||||
eq(
|
||||
"계산 걸음",
|
||||
ST.evalSteps(b, e, "s1", p.r, p.c).map((s) => s.글),
|
||||
["SUM(값들)*단가", "10*단가", "10*10", "100"],
|
||||
);
|
||||
|
||||
// 이름 바꾸기 · 지우기(sub4 명령 → 시트통째 → rebuild)
|
||||
const ctx = {
|
||||
get book() {
|
||||
return b;
|
||||
},
|
||||
sheet: () => b.시트[0],
|
||||
dispatch: (cmd) => {
|
||||
C.applyCommand(b, cmd);
|
||||
e.rebuild(b);
|
||||
},
|
||||
};
|
||||
eq("이름 바꿈", N.renameName(ctx, "값들", "수량"), null);
|
||||
eq("바꾼 뒤", [b.시트[0].칸.C1.식, v("C1"), v("D1", "s2")], ["SUM(수량)", "10", "10"]);
|
||||
N.deleteName(ctx, "단가");
|
||||
eq("지운 뒤 #NAME?", [v("C2"), v("C4")], ["#NAME?", "#NAME?"]);
|
||||
|
||||
// 행 넣기 · 지우기 때 이름 범위가 따라감 · 되돌리기
|
||||
const fx = C.applyCommand(b, { 종류: "행넣기", 시트: "s1", at: 0, 수: 1 });
|
||||
e.rebuild(b);
|
||||
eq("넣은 뒤 범위", A.rangeToA1(b.시트[0].이름정의[0].범위), "A2:A4");
|
||||
eq("넣은 뒤 값", [v("C2"), v("D1", "s2")], ["10", "10"]);
|
||||
C.applyCommand(b, fx.undo);
|
||||
e.rebuild(b);
|
||||
eq("되돌린 범위", A.rangeToA1(b.시트[0].이름정의[0].범위), "A1:A3");
|
||||
C.applyCommand(b, { 종류: "행지우기", 시트: "s1", at: 1, 수: 1 });
|
||||
e.rebuild(b);
|
||||
eq("가운데 지움", [A.rangeToA1(b.시트[0].이름정의[0].범위), v("C1")], ["A1:A2", "8"]);
|
||||
C.applyCommand(b, { 종류: "열지우기", 시트: "s1", at: 0, 수: 1 });
|
||||
e.rebuild(b);
|
||||
eq("통째 지움 → 이름 빠짐", [b.시트[0].이름정의.length, v("B1")], [0, "#NAME?"]);
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify(results));
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
선행 · 종속 추적(누를 때마다 한 단계 · 범위 화살 · 오류 칸 빨강 · 다른 시트 · 더 없으면 false) ·
|
||||
수식 계산 걸음(왼쪽 먼저 · 빈 칸 0 · 범위는 함수가 · IF 첫 인자만 · 괄호 안 밑줄 · 오류 전파) ·
|
||||
오류 검사(순환 · 식 결과 오류 · 숫자처럼 보이는 글).
|
||||
오류 검사(순환 · 식 결과 오류 · 숫자처럼 보이는 글) ·
|
||||
이름 정의 풀이(범위로 · 자기 시트 먼저 · 바꾸기 · 지우기 · 행열 넣기 · 지우기 때 범위 따라감 · 없는 이름 #NAME?).
|
||||
화면(화살 겹판 · 계산 창 · ⚠ 풍선)은 harness_analysis.html 을 ORCA 로.
|
||||
"""
|
||||
|
||||
@@ -14,7 +15,7 @@ import subprocess
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
GROUPS = ["trace", "eval_steps", "errcheck"]
|
||||
GROUPS = ["trace", "eval_steps", "errcheck", "names"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* =============================================================================
|
||||
* test_stage2_data_tools.ts — spreadsheet_data_tools 시험(텍스트 나누기 · 중복 제거 · 연속 채우기).
|
||||
* 돌리기: npx tsx <이 파일> (`@ui/*` 별칭 때문에 plain node 안 됨)
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
applyFillSeries,
|
||||
applyTextToColumns,
|
||||
buildLinearSeries,
|
||||
findDuplicateRowIndexes,
|
||||
removeDuplicateRows,
|
||||
splitDelimited,
|
||||
splitFixedWidth,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_data_tools.ts";
|
||||
import { a1, assertEqual, makeBook, makeCtx, makeSheet, v } from "./test_stage2_helpers.ts";
|
||||
|
||||
// ── 텍스트 나누기 ──────────────────────────────────────────────
|
||||
assertEqual(splitDelimited("가,나,다", ["쉼표"], ""), ["가", "나", "다"], "쉼표 나누기");
|
||||
assertEqual(splitDelimited("가\t나", ["탭"], ""), ["가", "나"], "탭 나누기");
|
||||
assertEqual(
|
||||
splitDelimited("가;나,다", ["세미콜론", "쉼표"], ""),
|
||||
["가", "나", "다"],
|
||||
"복수 구분 기호",
|
||||
);
|
||||
assertEqual(splitDelimited("가|나", ["사용자"], "|"), ["가", "나"], "사용자 지정 구분 기호");
|
||||
assertEqual(splitDelimited("가나다", [], ""), ["가나다"], "구분 기호 없으면 한 칸 그대로");
|
||||
|
||||
assertEqual(splitFixedWidth("2026-09-27", [4, 7]), ["2026", "-09", "-27"], "고정 폭 나누기");
|
||||
assertEqual(splitFixedWidth("ABC", [10]), ["ABC"], "자리가 글보다 길면 한 칸");
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
applyTextToColumns(ctx, { r: 0, c: 0 }, [
|
||||
["가", "나"],
|
||||
["1", "2"],
|
||||
]);
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: "가" }, "나누기 결과 A1");
|
||||
assertEqual(book.시트[0].칸[a1(0, 1)], { 값: "나" }, "나누기 결과 B1");
|
||||
assertEqual(book.시트[0].칸[a1(1, 0)], { 값: 1 }, "나누기 결과 숫자는 수로");
|
||||
assertEqual(book.시트[0].칸[a1(1, 1)], { 값: 2 }, "나누기 결과 B2");
|
||||
}
|
||||
|
||||
// ── 중복된 항목 제거 ──────────────────────────────────────────
|
||||
assertEqual(
|
||||
findDuplicateRowIndexes(
|
||||
[
|
||||
["가", "1"],
|
||||
["나", "2"],
|
||||
["가", "1"],
|
||||
["다", "3"],
|
||||
["나", "2"],
|
||||
],
|
||||
[0, 1],
|
||||
),
|
||||
[2, 4],
|
||||
"중복 자리 찾기(모든 열 비교)",
|
||||
);
|
||||
assertEqual(
|
||||
findDuplicateRowIndexes(
|
||||
[
|
||||
["가", "1"],
|
||||
["가", "9"],
|
||||
],
|
||||
[0],
|
||||
),
|
||||
[1],
|
||||
"일부 열만 비교",
|
||||
);
|
||||
|
||||
{
|
||||
const book = makeBook([
|
||||
makeSheet("s1", {
|
||||
[a1(0, 0)]: v("가"),
|
||||
[a1(1, 0)]: v("나"),
|
||||
[a1(2, 0)]: v("가"),
|
||||
[a1(3, 0)]: v("다"),
|
||||
}),
|
||||
]);
|
||||
const ctx = makeCtx(book, "s1", {
|
||||
s1: { A1: "가", A2: "나", A3: "가", A4: "다" },
|
||||
});
|
||||
const removed = removeDuplicateRows(ctx, { r0: 0, c0: 0, r1: 3, c1: 0 }, [0], false);
|
||||
assertEqual(removed, 1, "지운 중복 행 수");
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: "가" }, "중복 제거 뒤 A1 그대로");
|
||||
assertEqual(book.시트[0].칸[a1(1, 0)], { 값: "나" }, "중복 제거 뒤 A2 그대로");
|
||||
assertEqual(book.시트[0].칸[a1(2, 0)], { 값: "다" }, "중복 제거 뒤 위로 당김");
|
||||
assertEqual(book.시트[0].칸[a1(3, 0)], undefined, "중복 제거 뒤 마지막 칸 비움");
|
||||
}
|
||||
|
||||
// ── 채우기 연속 데이터 ──────────────────────────────────────────
|
||||
assertEqual(buildLinearSeries(1, 1, null, 5), [1, 2, 3, 4, 5], "단계 1 · 5개");
|
||||
assertEqual(buildLinearSeries(0, 2, 6, 100), [0, 2, 4, 6], "끝값에서 멈춤");
|
||||
assertEqual(buildLinearSeries(10, -2, 4, 100), [10, 8, 6, 4], "내림차순 끝값에서 멈춤");
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
applyFillSeries(ctx, { r: 0, c: 0 }, "열", [1, 2, 3]);
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: 1 }, "세로 채우기 A1");
|
||||
assertEqual(book.시트[0].칸[a1(1, 0)], { 값: 2 }, "세로 채우기 A2");
|
||||
assertEqual(book.시트[0].칸[a1(2, 0)], { 값: 3 }, "세로 채우기 A3");
|
||||
}
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
applyFillSeries(ctx, { r: 0, c: 0 }, "행", [1, 2]);
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: 1 }, "가로 채우기 A1");
|
||||
assertEqual(book.시트[0].칸[a1(0, 1)], { 값: 2 }, "가로 채우기 B1");
|
||||
}
|
||||
|
||||
if (process.exitCode) process.exit(process.exitCode);
|
||||
console.log("spreadsheet_data_tools 시험 끝");
|
||||
@@ -14,7 +14,10 @@ import type {
|
||||
Sheet,
|
||||
Workbook,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { SpreadsheetContext } from "../../../A00_Common/spreadsheet/spreadsheet_view_types";
|
||||
import type {
|
||||
Selection,
|
||||
SpreadsheetContext,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_view_types";
|
||||
|
||||
export function assertEqual<T>(actual: T, expected: T, label: string): void {
|
||||
const a = JSON.stringify(actual);
|
||||
@@ -52,8 +55,9 @@ export function makeCtx(
|
||||
book: Workbook,
|
||||
sheetId: string,
|
||||
values: FakeEngineValues = {},
|
||||
): SpreadsheetContext & { applied: Command[] } {
|
||||
): SpreadsheetContext & { applied: Command[]; selects: Selection[] } {
|
||||
const applied: Command[] = [];
|
||||
const selects: Selection[] = [];
|
||||
|
||||
const apply = (cmd: Command): void => {
|
||||
applied.push(cmd);
|
||||
@@ -126,7 +130,7 @@ export function makeCtx(
|
||||
dispatch: (cmd: Command) => apply(cmd),
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
select: () => {},
|
||||
select: (sel: Selection) => selects.push(sel),
|
||||
showSheet: () => {},
|
||||
grid: {
|
||||
root: {} as HTMLElement,
|
||||
@@ -149,6 +153,7 @@ export function makeCtx(
|
||||
sync: () => {},
|
||||
},
|
||||
applied,
|
||||
selects,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* =============================================================================
|
||||
* test_stage2_insert_dialog.ts — spreadsheet_insert_dialog 시험(부분 범위 밀기 4가지).
|
||||
* 돌리기: npx tsx <이 파일>
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
deleteShiftLeft,
|
||||
deleteShiftUp,
|
||||
insertShiftDown,
|
||||
insertShiftRight,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_insert_dialog.ts";
|
||||
import { a1, assertEqual, makeBook, makeCtx, makeSheet, v } from "./test_stage2_helpers.ts";
|
||||
|
||||
// A1=1 B1=2 C1=3 한 줄 있는 시트에서 B1:B1 자리에 오른쪽으로 밀기 → B·C 가 밀리고 B 는 빔.
|
||||
{
|
||||
const book = makeBook([
|
||||
makeSheet("s1", { [a1(0, 0)]: v(1), [a1(0, 1)]: v(2), [a1(0, 2)]: v(3) }),
|
||||
]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
insertShiftRight(ctx, { r0: 0, c0: 1, r1: 0, c1: 1 });
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: 1 }, "오른쪽 밀기 · A1 안 건드림");
|
||||
assertEqual(book.시트[0].칸[a1(0, 1)], undefined, "오른쪽 밀기 · B1 빔(끼운 자리)");
|
||||
assertEqual(book.시트[0].칸[a1(0, 2)], { 값: 2 }, "오른쪽 밀기 · 옛 B1 이 C1 로");
|
||||
assertEqual(book.시트[0].칸[a1(0, 3)], { 값: 3 }, "오른쪽 밀기 · 옛 C1 이 D1 로");
|
||||
}
|
||||
|
||||
// A1=1 A2=2 A3=3 세로줄에서 A2 자리에 아래로 밀기.
|
||||
{
|
||||
const book = makeBook([
|
||||
makeSheet("s1", { [a1(0, 0)]: v(1), [a1(1, 0)]: v(2), [a1(2, 0)]: v(3) }),
|
||||
]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
insertShiftDown(ctx, { r0: 1, c0: 0, r1: 1, c1: 0 });
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: 1 }, "아래로 밀기 · A1 안 건드림");
|
||||
assertEqual(book.시트[0].칸[a1(1, 0)], undefined, "아래로 밀기 · A2 빔(끼운 자리)");
|
||||
assertEqual(book.시트[0].칸[a1(2, 0)], { 값: 2 }, "아래로 밀기 · 옛 A2 가 A3 로");
|
||||
assertEqual(book.시트[0].칸[a1(3, 0)], { 값: 3 }, "아래로 밀기 · 옛 A3 가 A4 로");
|
||||
}
|
||||
|
||||
// A1=1 B1=2 C1=3 에서 A1:A1 삭제(왼쪽으로 밀기) → B·C 가 왼쪽으로 당겨짐.
|
||||
{
|
||||
const book = makeBook([
|
||||
makeSheet("s1", { [a1(0, 0)]: v(1), [a1(0, 1)]: v(2), [a1(0, 2)]: v(3) }),
|
||||
]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
deleteShiftLeft(ctx, { r0: 0, c0: 0, r1: 0, c1: 0 });
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: 2 }, "왼쪽 당김 · A1 이 옛 B1 값");
|
||||
assertEqual(book.시트[0].칸[a1(0, 1)], { 값: 3 }, "왼쪽 당김 · B1 이 옛 C1 값");
|
||||
assertEqual(book.시트[0].칸[a1(0, 2)], undefined, "왼쪽 당김 · 마지막 자리 빔");
|
||||
}
|
||||
|
||||
// A1=1 A2=2 A3=3 에서 A1:A1 삭제(위로 밀기) → A2·A3 가 위로 당겨짐.
|
||||
{
|
||||
const book = makeBook([
|
||||
makeSheet("s1", { [a1(0, 0)]: v(1), [a1(1, 0)]: v(2), [a1(2, 0)]: v(3) }),
|
||||
]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
deleteShiftUp(ctx, { r0: 0, c0: 0, r1: 0, c1: 0 });
|
||||
assertEqual(book.시트[0].칸[a1(0, 0)], { 값: 2 }, "위로 당김 · A1 이 옛 A2 값");
|
||||
assertEqual(book.시트[0].칸[a1(1, 0)], { 값: 3 }, "위로 당김 · A2 가 옛 A3 값");
|
||||
assertEqual(book.시트[0].칸[a1(2, 0)], undefined, "위로 당김 · 마지막 자리 빔");
|
||||
}
|
||||
|
||||
if (process.exitCode) process.exit(process.exitCode);
|
||||
console.log("spreadsheet_insert_dialog 시험 끝");
|
||||
@@ -0,0 +1,65 @@
|
||||
/* =============================================================================
|
||||
* test_stage2_view_menu.ts — spreadsheet_view_menu 시험(이동 · 틀 고정 명령 짓기).
|
||||
* 돌리기: npx tsx <이 파일>
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
freezeAtActiveCell,
|
||||
freezeFirstColumn,
|
||||
freezeFirstRow,
|
||||
goTo,
|
||||
unfreezePanes,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_view_menu.ts";
|
||||
import { assertEqual, makeBook, makeCtx, makeSheet } from "./test_stage2_helpers.ts";
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
goTo(ctx, { r0: 2, c0: 3, r1: 4, c1: 5 });
|
||||
assertEqual(ctx.selects.length, 1, "이동 · select 한 번 불림");
|
||||
assertEqual(ctx.selects[0].범위, [{ r0: 2, c0: 3, r1: 4, c1: 5 }], "이동 · 범위 그대로");
|
||||
assertEqual(ctx.selects[0].활성, { r: 2, c: 3 }, "이동 · 활성 칸은 범위 왼위");
|
||||
}
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
freezeFirstRow(ctx);
|
||||
assertEqual(ctx.applied[0], { 종류: "틀고정", 시트: "s1", 행: 1, 열: 0 }, "첫 행 고정");
|
||||
}
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
freezeFirstColumn(ctx);
|
||||
assertEqual(ctx.applied[0], { 종류: "틀고정", 시트: "s1", 행: 0, 열: 1 }, "첫 열 고정");
|
||||
}
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {}, { 틀고정: { 행: 1, 열: 0 } })]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
freezeFirstColumn(ctx);
|
||||
assertEqual(
|
||||
ctx.applied[0],
|
||||
{ 종류: "틀고정", 시트: "s1", 행: 1, 열: 1 },
|
||||
"첫 열 고정 · 이미 있던 행 고정은 유지",
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
ctx.selection.활성 = { r: 3, c: 2 };
|
||||
freezeAtActiveCell(ctx);
|
||||
assertEqual(ctx.applied[0], { 종류: "틀고정", 시트: "s1", 행: 3, 열: 2 }, "현재 칸 기준 고정");
|
||||
}
|
||||
|
||||
{
|
||||
const book = makeBook([makeSheet("s1", {})]);
|
||||
const ctx = makeCtx(book, "s1");
|
||||
unfreezePanes(ctx);
|
||||
assertEqual(ctx.applied[0], { 종류: "틀고정", 시트: "s1", 행: 0, 열: 0 }, "틀 고정 풀기");
|
||||
}
|
||||
|
||||
if (process.exitCode) process.exit(process.exitCode);
|
||||
console.log("spreadsheet_view_menu 시험 끝");
|
||||
Reference in New Issue
Block a user