Files
Aislo/B08_Quantity/B08_Quantity_Options_Calc.ts
T

178 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B08_Quantity_Options_Calc.ts
* 좌측 패널 옵션 — 조작 중 브라우저가 바로 다시 세는 식(화면 없음 · PLAN 122).
*
* 짝(파이썬 · 같은 식) — 거울 시험 `test_122_b08_options_mirror`:
* couplings · recountCouplings ↔ `B08_Quantity_Router_Summary.py` couplings(관마다 `pipes`)
* rockSplit · splitRockColumns · splitRockSummary
* ↔ `B08_Quantity_Engine_Earthwork_Summary.py` rock_split ·
* split_rock_columns · summary_rows(연암 · 경암 네 줄)
* mixedRockC ↔ `common_util_project_settings.py` mixed_rock_c(125-1)
* recountRockC · recountAdjustedSummary
* ↔ `B08_Quantity_Engine_Earthwork.py` build_rows · totals ·
* summary_rows(보정량계 · 유용토 · 잔토처리 줄)
* [저장] 뒤에는 서버가 정본으로 다시 셈 — 여기 값을 받아 적지 않음.
* import 없음 — 시험이 이 파일만 옮겨 돌림.
* ========================================================================== */
/** 커플링 = 올림(L ÷ 1본) − 1(0 아래 없음) · 길이나 1본이 빈칸이면 null. */
export function couplings(lengthM: number | null | undefined, oneM: number | null): number | null {
if (!lengthM || !oneM) return null;
return Math.max(Math.ceil(lengthM / oneM - 1e-9) - 1, 0);
}
export interface PipeLength {
chainage_m: number;
/** 커플링 열 id(`pipe:cp|관경`) */
column: string;
length_m: number | null;
}
interface CouplingSummary {
rows: Array<{ chainage_m: number; values: Record<string, number> }>;
total: Record<string, number>;
pipes: PipeLength[];
}
/** 커플링 열만 새 1본 길이로 — 같은 측점(1 cm)의 같은 열은 더함 · 계 = 줄 합. */
export function recountCouplings<S extends CouplingSummary>(summary: S, oneM: number | null): S {
const at = (chainage: number): number => Math.round(chainage * 100);
const columns = new Set(summary.pipes.map((pipe) => pipe.column));
const sums = new Map<string, number>();
for (const pipe of summary.pipes) {
const count = couplings(pipe.length_m, oneM);
if (count === null) continue;
const key = `${at(pipe.chainage_m)}|${pipe.column}`;
sums.set(key, (sums.get(key) ?? 0) + count);
}
const keep = (values: Record<string, number>): Record<string, number> =>
Object.fromEntries(Object.entries(values).filter(([id]) => !columns.has(id)));
const rows = summary.rows.map((row) => {
const values = keep(row.values);
for (const column of columns) {
const count = sums.get(`${at(row.chainage_m)}|${column}`);
if (count !== undefined) values[column] = count;
}
return { ...row, values };
});
const total = keep(summary.total);
for (const row of rows)
for (const column of columns)
if (row.values[column] !== undefined)
total[column] = (total[column] ?? 0) + row.values[column];
return { ...summary, rows, total };
}
/** 0~100 으로 자름 · 숫자가 아니면 null. */
export function clampPct(value: number): number | null {
return Number.isFinite(value) ? Math.min(Math.max(value, 0), 100) : null;
}
/** 암 → [연암, 경암] — 연암 = 암 × 비율(0~100 자름) · 경암 = 암 − 연암. */
export function rockSplit(value: number, softPct: number): [number, number] {
const soft = (value * Math.min(Math.max(softPct, 0), 100)) / 100;
return [soft, value - soft];
}
const ROCK_SPLIT = [
["cut_rock", "cut_soft_rock", "cut_hard_rock"],
["ditch_rock", "ditch_soft_rock", "ditch_hard_rock"],
] as const;
const ROCK_SUFFIXES = ["area_m2", "volume_m3", "adjusted_m3"] as const;
/** 줄 · 합계의 암 칸마다 연암 · 경암 칸을 덮어씀(숫자인 칸만). */
export function splitRockColumns(record: Record<string, unknown>, softPct: number): void {
for (const [base, softKey, hardKey] of ROCK_SPLIT)
for (const suffix of ROCK_SUFFIXES) {
const value = record[`${base}_${suffix}`];
if (typeof value !== "number") continue;
const [soft, hard] = rockSplit(value, softPct);
record[`${softKey}_${suffix}`] = soft;
record[`${hardKey}_${suffix}`] = hard;
}
}
/** 토공집계표 연암 · 경암 줄 — 흙깍기 = 절토 암 · 측구 터파기 = 측구 암 합계 × 비율 · 반올림 정수. */
const ROCK_ROWS: Record<string, string> = {
흙깍기: "cut_rock_volume_m3",
"측구 터파기": "ditch_rock_volume_m3",
};
export function splitRockSummary<R extends { group: string; kind: string; value: number | null }>(
rows: readonly R[],
totals: Record<string, number>,
softPct: number,
): R[] {
return rows.map((row) => {
const base = ROCK_ROWS[row.group];
if (!base || (row.kind !== "연암" && row.kind !== "경암")) return row;
const [soft, hard] = rockSplit(totals[base] ?? 0, softPct);
return { ...row, value: Math.round(row.kind === "연암" ? soft : hard) };
});
}
/** 암 C = 연암 % × 리핑암 C + 경암 % × 발파암 C(0~100 자름) — 연암 = 리핑 · 경암 = 발파 고정(125-1). */
export function mixedRockC(softPct: number, rippingC: number, blastingC: number): number {
const soft = Math.min(Math.max(softPct, 0), 100) / 100;
return soft * rippingC + (1 - soft) * blastingC;
}
const num = (value: unknown): number => (typeof value === "number" ? value : 0);
const RECOUNT_TOTALS = [
"cut_rock_adjusted_m3",
"ditch_rock_adjusted_m3",
"adjusted_total_m3",
"diverted_m3",
"balance_m3",
] as const;
/** 토적표를 새 암 C 로 — 암 보정량 · 보정량계 · 유용토 · 차인 · 누가 · 합계(줄 · 합계 덮어씀). */
export function recountRockC(
rows: Array<Record<string, unknown>>,
totals: Record<string, number>,
c: number,
): void {
let cumulative = 0;
for (const row of rows) {
const cutRock = num(row.cut_rock_volume_m3) * c;
const adjusted = num(row.cut_soil_adjusted_m3) + cutRock;
const fill = num(row.fill_volume_m3);
cumulative += adjusted - fill;
Object.assign(row, {
cut_rock_adjusted_m3: cutRock,
ditch_rock_adjusted_m3: num(row.ditch_rock_volume_m3) * c,
adjusted_total_m3: adjusted,
diverted_m3: Math.min(adjusted, fill),
balance_m3: adjusted - fill,
cumulative_m3: cumulative,
});
}
const sum = (key: string, picked = rows): number =>
picked.reduce((total, row) => total + num(row[key]), 0);
for (const key of RECOUNT_TOTALS) totals[key] = sum(key);
for (const kind of ["ripping_rock", "blasting_rock"])
totals[`cut_${kind}_adjusted_m3`] = sum(
"cut_rock_adjusted_m3",
rows.filter((row) => row.cut_rock_kind === kind),
);
}
/** 엑셀 ROUND(정수) — 반은 0 에서 먼 쪽. */
const excelRound = (value: number): number => Math.sign(value) * Math.round(Math.abs(value));
/** 토공집계표 보정량계 · 유용토 소계 · 횡방향유용토 · 잔토처리 줄 — 합계에서 다시(종방향 무대 Q 는 그대로). */
export function recountAdjustedSummary<R extends { group: string; value: number | null }>(
rows: readonly R[],
totals: Record<string, number>,
freeQ: number,
): R[] {
const diverted = totals.diverted_m3 ?? 0;
const values: Record<string, number> = {
보정량계: excelRound(totals.adjusted_total_m3 ?? 0),
"유용토 소계": diverted + freeQ,
횡방향유용토: diverted,
잔토처리: excelRound(totals.balance_m3 ?? 0),
};
return rows.map((row) => (row.group in values ? { ...row, value: values[row.group] } : row));
}