- 새 `M02_Machine_Find` — 기종 = 기계표 이름 · 괄호 붙임 · 상세구분(낱말표 24 안 씀) · 소리 표기 한 글(로우더 = 로더 · 로울러 = 롤러 · 브레이카 = 브레이커 · 굴삭기 = 굴착기 · 램 머 = 래머 …) · 기계표 괄호 낱말로 좁힘(표에 없는 낱말 발파 · 리핑 = 안 봄) · 규격 = 수 + 단위 전부(범위 · 복수 · 인치 = ㎝) · 하나 = 이음 · 여럿 = 「기계 후보 n — 고르기」 · 0 = 「기계 못 찾음」 표지(가까운 규격으로 대신 잇지 않음) - 별칭 먼저 — 마스터 층 + 지금 층 별칭 장부 기계 짝(이름 + 규격 · 이름만 · 규격 속 할증 글 뺌) · `M02_Doc_Match_Note._ALIAS` 여섯 쌍 걷음(소리 표기 = 정규화 · 같은 기종 = 기계표 · 별칭 장부) - 조합 기계 = 기계마다 항목 하나(요구 738-3 · 제목 = 그 기계 · 줄 쌓기 차림 `M02_Doc_NativeLayout`) · 줄 둘(「+」 · 괄호 속 다른 기종 · 「…조합」 · 덤프트럭(자동덮개) = 덤프트럭 + 자동덮개시설) — 같은 수량 식 · 엑셀 값은 첫 줄 · 다음 줄 0 + 표지 「조합 기계 — 경비는 한 덩이」 · 우리 단가로 = 각자 시간당 경비 - 원파일 EST · STmate · 엑셀 읽기기 · 일위대가 호표(`Hopyo_Elem` · `Hopyo_Fixed._eqs` · `Hopyo_Row.m01_picks`)가 같은 찾기 · STmate 묶음 기계 줄도 시간당 경비 이음 - 문서 머리 `가져온.기계`(기호마다 판정 · 후보 · 찾기어) · 못 이은 기계 줄도 기계 줄(이름 단추 · `owned_of` 파이썬 · TS 짝) · 이름 단추 조회어 = 기종 이름만(규격 뺌) · 고르면 엑셀 값 기억 그대로(금액 안 바뀜) · 문서 [저장] = 고른 EQ → 이 층 별칭 장부 줄 → M01 별칭 장부 후보(승인 흐름 그대로) · 장부 짝 글 = 「기계 EQ…」 - 마스터 층 별칭 장부(resources/master_template/실무자료/맞춤.json) = 사용자 승인 다섯(요구 735-5 · 736) — 아스팔트페이버(피니셔) = 아스팔트 피니셔 · 디젤 엔진 = 엔진(디젤) · 워터제트 = 워터젯트 · 버킷준설기 = 버킷식준설기 · 바이브레이트 봉상후렉시블 = 콘크리트 진동기 엔진식 ø45(2.6㎾) - 시험 — 새 test_812_9(예 전부 · 정규화 · 괄호 · 범위 · 조합 둘 · 별칭 먼저 · 표지 · 고르면 별칭 줄 · 금액 전후) · test_812_3 전석쌓기 · 측구터파기 연암 항목을 기계마다 항목으로 고침(조합 기계 둘로 가름 · 요구 738-3 이 바꾼 까닭) - 전수 목록 docs/raw/요구_1104/_목록_812-9_기계이음.xlsx Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
463 lines
20 KiB
TypeScript
463 lines
20 KiB
TypeScript
/* =============================================================================
|
|
* common_util_line_stack.ts
|
|
* 줄 쌓기 모델(PLAN 804-2 · 요구 670~677) — 수식 문서 ↔ 줄 쌓기 한 벌 왕복 · 화면 · DOM · fetch 없음(시험이 Node 로 돌림)
|
|
* 정본 = 수식 문서 그대로(변수 · 식 · 금액 · 끝수 …) · 줄 쌓기는 그 위의 차림 — 항목(제목 · 가나다) 안 줄:
|
|
* 금액 줄(문서 `금액` 차례) · 공식 묶음(기호 목록 · 틀) · 글 줄
|
|
* 차림 저장 = 문서 칸 `쌓기`(기본 차림과 다를 때만 · 계산기는 안 봄) — 문서 → 줄 → 문서 = 같은 문서(1,518 전수 시험)
|
|
* 기본 차림 = 금액 줄을 `묶음` 이 같은 이어진 줄끼리 항목으로 · 기호는 처음 쓰는 항목의 공식 묶음(쓰는 차례 = 입력 먼저)
|
|
* 옵션(선택 · 입력 열쇠) · 금액 줄 단가 하나만 쓰는 연결 기호(요소 · 단가산출)는 공식 묶음에 안 넣음(옵션 가로 · 그 줄이 보임)
|
|
* 금액 차례 = 줄 쌓기 차례(율 줄 「노무비」 = 앞 줄 누계라 차례가 셈을 바꿈)
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
DocError,
|
|
names,
|
|
parse,
|
|
rename,
|
|
splitId,
|
|
symId,
|
|
textOf,
|
|
linkNames,
|
|
varId,
|
|
type DocVar,
|
|
type FormulaDoc,
|
|
type Link,
|
|
type MoneyLine,
|
|
type Node,
|
|
} from "./common_util_formula_doc";
|
|
import { evalNode } from "./common_util_formula_doc_view";
|
|
|
|
export type MoneyKind = "인력" | "재료" | "기계" | "단가산출" | "율";
|
|
export type StackLine =
|
|
| { 꼴: "금액"; i: number }
|
|
| { 꼴: "공식"; 기호: string[]; 틀?: string; 틀꼴?: string }
|
|
| { 꼴: "글"; 글: string };
|
|
export interface StackItem {
|
|
제목: string;
|
|
가나다?: boolean;
|
|
줄: StackLine[];
|
|
}
|
|
type SaveLine =
|
|
{ 금액: number; 이름: string } | { 공식: string[]; 틀?: string; 틀꼴?: string } | { 글: string };
|
|
export interface SaveItem {
|
|
제목?: string;
|
|
가나다?: boolean;
|
|
줄: SaveLine[];
|
|
}
|
|
export type StackDoc = FormulaDoc & { 쌓기?: SaveItem[] };
|
|
|
|
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
|
|
|
/** 식 글 → 나무(못 풀면 null) */
|
|
export function nodeOf(text: unknown): Node | null {
|
|
if (text === null || text === undefined || String(text).trim() === "") return null;
|
|
try {
|
|
return parse(text);
|
|
} catch (e) {
|
|
if (e instanceof DocError) return null;
|
|
throw e;
|
|
}
|
|
}
|
|
const namesIn = (text: unknown): string[] => {
|
|
const n = nodeOf(text);
|
|
return n ? names(n) : [];
|
|
};
|
|
|
|
/** 문서 기호 전부 — 변수 차례 · 변수 없는 식 줄 기호 */
|
|
export function symbols(doc: FormulaDoc): string[] {
|
|
const out = (doc.변수 ?? []).map(varId);
|
|
for (const l of doc.식 ?? []) {
|
|
const id = String(l.기호 ?? "");
|
|
if (id && !out.includes(id)) out.push(id);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export const isOption = (v: DocVar | undefined): boolean =>
|
|
!!v?.열쇠 && (v.연결?.갈래 === "선택" || v.연결?.갈래 === "입력");
|
|
|
|
/** 기호가 부르는 기호 — 식 줄 · 연결(조건 · 넘길 입력 · 구멍) */
|
|
export function usesOf(doc: FormulaDoc): Map<string, string[]> {
|
|
const out = new Map<string, string[]>();
|
|
const add = (id: string, more: string[]): void => {
|
|
const now = out.get(id) ?? [];
|
|
for (const n of more) if (!now.includes(n)) now.push(n);
|
|
out.set(id, now);
|
|
};
|
|
for (const v of doc.변수 ?? []) add(varId(v), v.연결 ? linkNames(v.연결) : []);
|
|
for (const l of doc.식 ?? []) add(String(l.기호 ?? ""), namesIn(l.식));
|
|
return out;
|
|
}
|
|
|
|
/** 단가 식이 기호 하나(또는 그 칸 꺼냄)면 그 기호 */
|
|
export function priceSym(line: MoneyLine): string | null {
|
|
const n = nodeOf(line.단가);
|
|
if (!n) return null;
|
|
if (n[0] === "var") return n[1];
|
|
if (n[0] === "field" && n[1][0] === "var") return n[1][1];
|
|
return null;
|
|
}
|
|
|
|
/** 금액 줄 하나만 단가로 쓰는 연결 기호(요소 · 단가산출) — {줄 차례: 기호} */
|
|
/** 가져온 기계 줄 단가 기호(812-9 `가져온.기계`) — 못 이어 숫자 연결이어도 그 줄 기계(이름 단추 · 고르기) */
|
|
export function machineSyms(doc: FormulaDoc): Set<string> {
|
|
const got = (doc as { 가져온?: { 기계?: { 기호?: string }[] } }).가져온?.기계;
|
|
return new Set((Array.isArray(got) ? got : []).map((x) => String(x?.기호 ?? "")));
|
|
}
|
|
|
|
export function ownedOf(doc: FormulaDoc): Map<number, string> {
|
|
const count = new Map<string, number>();
|
|
const bump = (ns: string[]): void => ns.forEach((n) => count.set(n, (count.get(n) ?? 0) + 1));
|
|
for (const l of doc.금액 ?? []) bump([...namesIn(l.수량), ...namesIn(l.단가)]);
|
|
for (const ns of usesOf(doc).values()) bump(ns);
|
|
if (doc.결과) bump([doc.결과]);
|
|
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
|
|
const lines = new Set((doc.식 ?? []).map((l) => String(l.기호 ?? "")));
|
|
const mach = machineSyms(doc);
|
|
const out = new Map<number, string>();
|
|
(doc.금액 ?? []).forEach((l, i) => {
|
|
const s = priceSym(l);
|
|
const kind = s ? vars.get(s)?.연결?.갈래 : undefined;
|
|
const mine = kind === "요소" || kind === "단가산출" || (!!s && mach.has(s));
|
|
if (s && mine && count.get(s) === 1 && !lines.has(s)) out.set(i, s);
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/** 금액 줄 갈래 — 인력 · 재료(요소) · 기계(기계 시간당 경비 단가산출) · 단가산출 · 율(식) */
|
|
export function kindOf(
|
|
doc: FormulaDoc,
|
|
i: number,
|
|
look?: (key: string) => FormulaDoc | null,
|
|
): MoneyKind {
|
|
const line = (doc.금액 ?? [])[i] ?? {};
|
|
const s = priceSym(line);
|
|
const link = s ? (doc.변수 ?? []).find((v) => varId(v) === s)?.연결 : undefined;
|
|
if (link?.갈래 === "단가산출") {
|
|
const sub = link.키 && look ? look(link.키) : null;
|
|
const machine =
|
|
(link.입력 && "기계" in link.입력) ||
|
|
!!sub?.변수?.some((v) => v.열쇠 === "기계" && isOption(v));
|
|
return machine ? "기계" : "단가산출";
|
|
}
|
|
if (link?.갈래 === "요소") {
|
|
const lb = link.키 ? String(link.키).startsWith("LB") : line.비목 === "노무비"; // 고르기 전 = 비목으로
|
|
return lb ? "인력" : "재료";
|
|
}
|
|
if (s && machineSyms(doc).has(s)) return "기계"; // 못 이은 가져온 기계 줄(812-9)
|
|
return "율";
|
|
}
|
|
|
|
/** 가져온 식 값 옵션 칸 꼬리표(810-3 · 요구 705) — 경쟁사 파일에 값으로 드러난 수(q · K · E · Cm …)는 옵션 가로 칸 ·
|
|
* 수 하나 숫자 연결(엑셀 단가 기억 `엑셀` 칸 · 금액 줄 단가로 쓰는 수 빼고) · 원파일 = 「원파일」 · 엑셀 글 = 「엑셀」 · 그 밖 null */
|
|
export function sourceOf(doc: FormulaDoc, v: DocVar): string | null {
|
|
const head = (doc as { 가져온?: { 원파일?: boolean } }).가져온;
|
|
const link = (v.연결 ?? {}) as Link & Record<string, unknown>;
|
|
if (!head || link.갈래 !== "숫자" || "엑셀" in link || typeof link.값 === "object") return null;
|
|
if ((doc.금액 ?? []).some((l) => priceSym(l) === varId(v))) return null; // 손 단가 = 금액 줄 그대로
|
|
return String((v as { 출처?: string }).출처 ?? (head.원파일 ? "원파일" : "엑셀"));
|
|
}
|
|
|
|
/** 차림에 안 넣는 기호 — 옵션 · 금액 줄이 가진 연결(가져온 식 값 옵션 칸은 공식 묶음에도 보임 · 같은 값 · 812-3)
|
|
* 파이썬 거울 = `M02_Doc_NativeLayout.py`(stackOf · saveOf · hiddenOf · 가져온 단가산출 차림) — 고칠 때 같이 고침 */
|
|
export function hiddenOf(doc: FormulaDoc): Set<string> {
|
|
const out = new Set(ownedOf(doc).values());
|
|
for (const v of doc.변수 ?? []) if (isOption(v)) out.add(varId(v));
|
|
return out;
|
|
}
|
|
|
|
/** 쓰는 차례(입력 먼저) — 기호마다 부르는 기호를 먼저 */
|
|
function postOrder(doc: FormulaDoc, roots: string[], uses: Map<string, string[]>): string[] {
|
|
const out: string[] = [];
|
|
const seen = new Set<string>();
|
|
const go = (id: string): void => {
|
|
if (seen.has(id)) return;
|
|
seen.add(id);
|
|
for (const n of uses.get(id) ?? []) go(n);
|
|
out.push(id);
|
|
};
|
|
roots.forEach(go);
|
|
symbols(doc).forEach(go);
|
|
return out;
|
|
}
|
|
|
|
/** 금액 줄 i 가 (거쳐서) 부르는 기호 */
|
|
function reachOf(line: MoneyLine, uses: Map<string, string[]>): Set<string> {
|
|
const out = new Set<string>();
|
|
const go = (id: string): void => {
|
|
if (out.has(id)) return;
|
|
out.add(id);
|
|
(uses.get(id) ?? []).forEach(go);
|
|
};
|
|
[...namesIn(line.수량), ...namesIn(line.단가)].forEach(go);
|
|
return out;
|
|
}
|
|
|
|
/** 문서 → 줄 쌓기(저장 차림 + 안 놓인 것은 기본 자리) */
|
|
export function stackOf(doc: StackDoc): StackItem[] {
|
|
const money = doc.금액 ?? [];
|
|
const all = new Set(symbols(doc));
|
|
const hidden = hiddenOf(doc);
|
|
const items: StackItem[] = [];
|
|
const where = new Map<number, StackItem>();
|
|
const placed = new Set<string>();
|
|
const saved = Array.isArray(doc.쌓기) ? doc.쌓기 : [];
|
|
const taken = new Set<number>();
|
|
for (const s of saved) {
|
|
const it: StackItem = { 제목: String(s?.제목 ?? ""), 줄: [] };
|
|
if (s?.가나다) it.가나다 = true;
|
|
for (const l of Array.isArray(s?.줄) ? s.줄 : []) {
|
|
if (l && "금액" in l) {
|
|
let i = Number(l.금액);
|
|
if (!(money[i] && (money[i].이름 ?? "") === l.이름 && !taken.has(i)))
|
|
i = money.findIndex((m, k) => !taken.has(k) && (m.이름 ?? "") === l.이름);
|
|
if (i < 0 || taken.has(i)) continue;
|
|
taken.add(i);
|
|
where.set(i, it);
|
|
it.줄.push({ 꼴: "금액", i });
|
|
} else if (l && "공식" in l) {
|
|
const ids = (Array.isArray(l.공식) ? l.공식 : []).filter(
|
|
(x) => all.has(x) && !hidden.has(x) && !placed.has(x),
|
|
);
|
|
ids.forEach((x) => placed.add(x));
|
|
const one: StackLine = { 꼴: "공식", 기호: ids };
|
|
if (l.틀) Object.assign(one, { 틀: String(l.틀) }, l.틀꼴 ? { 틀꼴: String(l.틀꼴) } : {});
|
|
it.줄.push(one);
|
|
} else if (l && "글" in l) it.줄.push({ 꼴: "글", 글: String(l.글 ?? "") });
|
|
}
|
|
items.push(it);
|
|
}
|
|
// 안 놓인 금액 줄 — 앞 줄 항목(묶음 같으면) › 같은 제목 항목 › 새 항목
|
|
money.forEach((m, i) => {
|
|
if (taken.has(i)) return;
|
|
const title = String(m.묶음 ?? "");
|
|
const prev = where.get(i - 1);
|
|
let it = prev && prev.제목 === title ? prev : undefined;
|
|
if (it && prev) {
|
|
const at = prev.줄.findIndex((x) => x.꼴 === "금액" && x.i === i - 1);
|
|
prev.줄.splice(at + 1, 0, { 꼴: "금액", i });
|
|
} else {
|
|
it = saved.length ? items.find((x) => x.제목 === title) : undefined;
|
|
if (!it) items.push((it = { 제목: title, 줄: [] }));
|
|
it.줄.push({ 꼴: "금액", i });
|
|
}
|
|
taken.add(i);
|
|
where.set(i, it);
|
|
});
|
|
// 안 놓인 기호 — 처음 쓰는 금액 줄 항목의 첫 공식 묶음(없으면 맨 앞에 새로) · 아무도 안 쓰면 첫 항목
|
|
const uses = usesOf(doc);
|
|
const reach = money.map((m) => reachOf(m, uses));
|
|
const order = postOrder(doc, doc.결과 ? [doc.결과] : [], uses);
|
|
for (const id of order) {
|
|
if (!all.has(id) || hidden.has(id) || placed.has(id)) continue;
|
|
const i = reach.findIndex((r) => r.has(id));
|
|
let it = i >= 0 ? where.get(i) : items[0];
|
|
if (!it) items.push((it = { 제목: "", 줄: [] }));
|
|
let block = it.줄.find((x) => x.꼴 === "공식");
|
|
if (!block) it.줄.unshift((block = { 꼴: "공식", 기호: [] }));
|
|
(block as { 기호: string[] }).기호.push(id);
|
|
placed.add(id);
|
|
}
|
|
return items;
|
|
}
|
|
|
|
const saveOf = (items: StackItem[], at: (i: number) => number, money: MoneyLine[]): SaveItem[] =>
|
|
items.map((it) => ({
|
|
...(it.제목 ? { 제목: it.제목 } : {}),
|
|
...(it.가나다 ? { 가나다: true } : {}),
|
|
줄: it.줄.map((l): SaveLine => {
|
|
if (l.꼴 === "금액") return { 금액: at(l.i), 이름: money[l.i]?.이름 ?? "" };
|
|
if (l.꼴 === "글") return { 글: l.글 };
|
|
return {
|
|
공식: [...l.기호],
|
|
...(l.틀 ? { 틀: l.틀 } : {}),
|
|
...(l.틀꼴 ? { 틀꼴: l.틀꼴 } : {}),
|
|
};
|
|
}),
|
|
}));
|
|
|
|
/** 줄 쌓기 → 문서 — 금액 = 쌓은 차례(안 쌓인 금액 줄은 지움) · 차림이 기본과 다를 때만 `쌓기` */
|
|
export function docOf(doc: StackDoc, items: StackItem[]): StackDoc {
|
|
const money = doc.금액 ?? [];
|
|
const order: number[] = [];
|
|
for (const it of items)
|
|
for (const l of it.줄)
|
|
if (l.꼴 === "금액" && money[l.i] && !order.includes(l.i)) order.push(l.i);
|
|
const out = clone(doc);
|
|
delete out.쌓기;
|
|
if (doc.금액 !== undefined || order.length) out.금액 = order.map((i) => clone(money[i]));
|
|
const pos = new Map(order.map((i, k) => [i, k]));
|
|
const live = new Set(symbols(out));
|
|
const kept = items.map((it) => ({
|
|
...it,
|
|
줄: it.줄
|
|
.filter((l) => l.꼴 !== "금액" || pos.has(l.i))
|
|
.map((l) => (l.꼴 === "공식" ? { ...l, 기호: l.기호.filter((x) => live.has(x)) } : l)),
|
|
}));
|
|
const save = saveOf(kept, (i) => pos.get(i)!, money);
|
|
const plain = saveOf(stackOf(out), (i) => i, out.금액 ?? []);
|
|
if (JSON.stringify(save) !== JSON.stringify(plain)) out.쌓기 = save;
|
|
return out;
|
|
}
|
|
|
|
/** 줄 쌓기가 못 고치는 문서 칸(보존은 됨 · 시험이 봄 · 화면 안내 줄은 804-8 에 걷음) */
|
|
export function notesOf(doc: FormulaDoc): string[] {
|
|
const out: string[] = [];
|
|
if (doc.단계?.length) out.push("단계");
|
|
if (doc.조정 && Object.keys(doc.조정).length) out.push("조정");
|
|
const money = doc.금액 ?? [];
|
|
if (money.some((l) => l.번지 && !/^복사\d+$/.test(l.번지))) out.push("번지"); // 복사N = 복사해 펼친 조각(804-4)
|
|
if (money.some((l) => l.합밖)) out.push("합밖");
|
|
if (money.some((l) => l.자리)) out.push("줄 자리");
|
|
if (money.some((l) => l.기호)) out.push("금액 줄 기호");
|
|
return out;
|
|
}
|
|
|
|
// ── 고침 ───────────────────────────────────────────────────────────────
|
|
|
|
/** 안 쓰는 새 기호 — `기호_아래N` */
|
|
export function freshId(doc: FormulaDoc, sign: string, low = ""): string {
|
|
const have = new Set(symbols(doc));
|
|
if (low && !have.has(symId(sign, low))) return symId(sign, low);
|
|
for (let n = 1; ; n++) {
|
|
const id = symId(sign, `${low}${n}`);
|
|
if (!have.has(id)) return id;
|
|
}
|
|
}
|
|
|
|
export function newVar(id: string, link: DocVar["연결"], name = ""): DocVar {
|
|
const [기호, 아래, 위] = splitId(id);
|
|
return { 기호, 아래, 위, 이름: name, 단위: "", 연결: link };
|
|
}
|
|
|
|
/** 기호 지움 — 변수 · 식 줄(다른 식이 부르면 「정의 필요」 표지 · 오류 없음) */
|
|
export function dropSyms(doc: FormulaDoc, ids: string[]): void {
|
|
const gone = new Set(ids);
|
|
doc.변수 = (doc.변수 ?? []).filter((v) => !gone.has(varId(v)));
|
|
doc.식 = (doc.식 ?? []).filter((l) => !gone.has(String(l.기호 ?? "")));
|
|
}
|
|
|
|
/** 끌어 옮김(804-5) — 줄 `[i, j]` → 항목 k 의 p 자리(빼기 전 차례 · 같은 항목 아래로면 하나 당김) */
|
|
export function moveLine(items: StackItem[], i: number, j: number, k: number, p: number): void {
|
|
const [line] = items[i].줄.splice(j, 1);
|
|
if (!line || !items[k]) return void (line && items[i].줄.splice(j, 0, line));
|
|
items[k].줄.splice(Math.min(i === k && p > j ? p - 1 : p, items[k].줄.length), 0, line);
|
|
}
|
|
|
|
/** 끌어 옮김(804-5) — 항목 i → p 자리(빼기 전 차례) */
|
|
export function moveItem<T>(items: T[], i: number, p: number): void {
|
|
const [it] = items.splice(i, 1);
|
|
if (it !== undefined) items.splice(Math.min(p > i ? p - 1 : p, items.length), 0, it);
|
|
}
|
|
|
|
/** 공식 줄 값 · 식 고침 — 수 = 숫자 연결 · 식 = 식 줄 */
|
|
export function setRow(doc: FormulaDoc, id: string, text: string): void {
|
|
const t = text.trim().replace(/,/g, "");
|
|
const n = nodeOf(t);
|
|
const plain = n && (n[0] === "num" || (n[0] === "neg" && n[1][0] === "num"));
|
|
let v = (doc.변수 ?? []).find((x) => varId(x) === id);
|
|
const from = v?.연결?.표 ? { 표: v.연결.표, 칸: v.연결.칸, 조건: v.연결.조건 } : {}; // 표에서 온 기호 = 별칭 · 꼬리표 남김(요구 692)
|
|
if (plain || !t) {
|
|
doc.식 = (doc.식 ?? []).filter((l) => l.기호 !== id);
|
|
if (!v) (doc.변수 ??= []).push((v = newVar(id, { 갈래: "숫자" })));
|
|
const value = t ? Number(t) : null;
|
|
v.연결 = { ...from, 갈래: "숫자", 값: Number.isFinite(value) ? value : t };
|
|
return;
|
|
}
|
|
if (v && v.연결?.갈래 !== "식") v.연결 = { ...from, 갈래: "식" };
|
|
const line = (doc.식 ?? []).find((l) => l.기호 === id);
|
|
if (line) line.식 = text.trim();
|
|
else (doc.식 ??= []).push({ 기호: id, 식: text.trim() });
|
|
}
|
|
|
|
/** 기호 이름 바꿈 — 문서 전부(`rename`) + 차림 */
|
|
export function renameSym(doc: StackDoc, old: string, now: string): void {
|
|
const [s, lo, up] = splitId(now);
|
|
rename(doc, old, s, lo, up);
|
|
for (const it of doc.쌓기 ?? [])
|
|
for (const l of it.줄) if ("공식" in l) l.공식 = l.공식.map((x) => (x === old ? now : x));
|
|
}
|
|
|
|
/** 공식 묶음 꼴 — 틀과 견줌(이름 · 식 · 연결 갈래 · 숫자 값은 안 봄) */
|
|
export function shapeOf(doc: FormulaDoc, ids: string[]): string {
|
|
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
|
|
const lines = new Map((doc.식 ?? []).map((l) => [String(l.기호 ?? ""), l.식]));
|
|
return JSON.stringify(
|
|
ids.map((id) => {
|
|
const t = lines.get(id);
|
|
const n = t === undefined ? null : nodeOf(t);
|
|
return [id, n ? textOf(n) : t !== undefined ? String(t) : (vars.get(id)?.연결?.갈래 ?? "식")];
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** 틀 펼침 — 다른 문서(틀 · 단가산출 결과)의 기호 · 식을 이 문서로 옮김(겹치는 기호 = 아래첨자 끝 수)
|
|
* `target` = 결과를 받을 이 문서 기호(단가산출 연결 → 식) · 없으면 결과 기호 그대로 · 돌려줌 = 옮긴 기호(입력 먼저) */
|
|
export function unfold(doc: FormulaDoc, src: FormulaDoc, target?: string): string[] {
|
|
const sub = clone(src);
|
|
const result = sub.결과 ?? "";
|
|
const mine = new Set(symbols(doc).filter((x) => x !== target));
|
|
const own = new Set(symbols(sub));
|
|
const taken = new Set([...mine, ...own]);
|
|
for (const id of symbols(sub)) {
|
|
if (id === result && target) continue;
|
|
if (!mine.has(id) && id !== target) continue;
|
|
const [s, lo, up] = splitId(id);
|
|
let n = 2;
|
|
while (taken.has(symId(s, `${lo}${n}`, up))) n++;
|
|
rename(sub, id, s, `${lo}${n}`, up);
|
|
taken.add(symId(s, `${lo}${n}`, up));
|
|
}
|
|
const given = target ? ((doc.변수 ?? []).find((v) => varId(v) === target)?.연결?.입력 ?? {}) : {};
|
|
let resultId = sub.결과 ?? "";
|
|
if (target && resultId && resultId !== target) {
|
|
const [s, lo, up] = splitId(target);
|
|
rename(sub, resultId, s, lo, up);
|
|
resultId = target;
|
|
}
|
|
const keys = new Set((doc.변수 ?? []).map((v) => v.열쇠).filter(Boolean));
|
|
const moved: string[] = [];
|
|
for (const v of sub.변수 ?? []) {
|
|
const id = varId(v);
|
|
if (id === resultId && target) continue;
|
|
if (v.열쇠 && v.열쇠 in given) {
|
|
v.연결 = { 갈래: "식" };
|
|
(sub.식 ??= []).push({ 기호: id, 식: String(given[v.열쇠]) });
|
|
delete v.열쇠;
|
|
} else if (v.열쇠 && keys.has(v.열쇠)) delete v.열쇠;
|
|
(doc.변수 ??= []).push(v);
|
|
}
|
|
for (const l of sub.식 ?? []) (doc.식 ??= []).push(l);
|
|
if (target) {
|
|
const t = (doc.변수 ?? []).find((v) => varId(v) === target);
|
|
const r = (src.변수 ?? []).find((v) => varId(v) === src.결과);
|
|
if (t) {
|
|
t.연결 = { 갈래: "식" };
|
|
if (!t.단위) t.단위 = r?.단위 || src.결과단위 || "";
|
|
}
|
|
}
|
|
const uses = usesOf(sub);
|
|
for (const id of postOrder(sub, resultId ? [resultId] : [], uses))
|
|
if (
|
|
symbols(sub).includes(id) &&
|
|
!moved.includes(id) &&
|
|
!isOption(sub.변수?.find((v) => varId(v) === id))
|
|
)
|
|
moved.push(id);
|
|
return moved;
|
|
}
|
|
|
|
/** 단가산출 연결이 넘길 입력 값 — 부른 쪽 값으로 셈(펼침이 같은 계산기로 그 문서를 셈) */
|
|
export function subInputs(
|
|
link: Link | undefined,
|
|
vals: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
const out: Record<string, unknown> = {};
|
|
for (const [name, t] of Object.entries(link?.입력 ?? {})) {
|
|
const n = nodeOf(t);
|
|
const v = n ? evalNode(n, vals) : undefined;
|
|
if (v !== undefined) out[name] = v;
|
|
}
|
|
return out;
|
|
}
|