"""횡단 이름 규칙 한 벌 — 파일 이름 · B07 도면 id · 찾기 · 잔재 거름. `B05_Profile_Engine_Sections` 에서 나눔(700줄) · 그 모듈이 다시 내보냄.""" import re from pathlib import Path from typing import Any # ── 횡단 이름 규칙(한 벌) — 파일 이름 · B07 도면 id · 찾기가 모두 이것을 씀 ──────────── #: 이름 속 측점 글 — 정수 m `00120` · 소수 측점 `00119.80` CROSS_KEY_PATTERN = r"(\d+(?:\.\d{2})?)" def cross_station(chainage_m: float) -> int | float: """측점 → 이름 열쇠 값 — 정수 m(0.005m 안)면 int(옛 이름 · 키 그대로) · 아니면 소수 2자리.""" value = round(float(chainage_m), 2) whole = round(value) return int(whole) if abs(value - whole) < 0.005 else value def cross_key(chainage_m: float) -> str: """이름 속 측점 글 — `00120`(정수 · 옛 이름 그대로) · `00119.80`(소수 측점).""" station = cross_station(chainage_m) return f"{station:05d}" if isinstance(station, int) else f"{station:08.2f}" def parse_cross_key(text: str) -> int | float: """`cross_key` 되읽기.""" return cross_station(float(text)) def cross_filename(chainage_m: float) -> str: """측점 chainage에 대응하는 횡단면 파일명(단일 규칙).""" return f"cross_{cross_key(chainage_m)}m.json" def find_cross_file(cross_dir: Path, chainage_m: float) -> Path: """읽기용 — 지금 이름이 없으면 옛 정수 m 이름(소수 측점이 정수로 붙던 옛 프로젝트).""" path = cross_dir / cross_filename(chainage_m) legacy = cross_dir / f"cross_{int(round(float(chainage_m))):05d}m.json" return path if path.exists() or not legacy.exists() else legacy def valid_cross_names(cross_dir: Path, stations: list[Any]) -> set[str]: """stations 에 맞는 cross_*.json 파일명 집합 — 읽기 전용(지우지 않음 · GET 이 씀). 측점 정보가 비면 빈 집합(호출부는 빈 집합이면 필터를 생략한다). """ # 옛 프로젝트는 소수 측점 파일이 정수 m 이름이라 찾기 규칙(find_cross_file)으로 가림 return { find_cross_file(cross_dir, station["chainage_m"]).name for station in stations if isinstance(station, dict) and isinstance(station.get("chainage_m"), (int, float)) } def cross_files_in_order(cross_dir: Path, stations: list[Any]) -> list[Path]: """잔재를 걸러 읽을 cross_*.json — **측점 차례**(이름 차례면 `01080.16` 이 `01080` 앞).""" valid = valid_cross_names(cross_dir, stations) def chainage(path: Path) -> tuple[float, str]: match = re.fullmatch(rf"cross_{CROSS_KEY_PATTERN}m", path.stem) return (float(match.group(1)) if match else float("inf"), path.name) files = [path for path in cross_dir.glob("cross_*.json") if not valid or path.name in valid] return sorted(files, key=chainage) def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]: """stations에 없는 잔재 cross_*.json을 삭제하고 유효 파일명 집합을 반환한다. 쓰는 쪽(측점 만들기 · 측점 목록 고침)만 부른다 — 조회(GET)는 `valid_cross_names`. 측점 정보가 비어 있으면 오삭제를 피하기 위해 아무것도 지우지 않고 빈 집합을 반환한다(호출부는 빈 집합이면 필터를 생략한다). """ valid = valid_cross_names(cross_dir, stations) if not valid: return valid for path in cross_dir.glob("cross_*.json"): if path.name not in valid: path.unlink(missing_ok=True) return valid