"""PinMAP structure parser. Reads a dict of {(row, col): str} cells (as produced by xls_reader / xlsx_reader), detects the rectangular PinMAP boundary, and extracts pins in counter-clockwise order starting from the top-left corner. Usage ----- >>> from pinmap_parser import parse_pinmap >>> pinmap = parse_pinmap(cells) """ from models import Pin, PinMAP, StructureError def _try_int(value: str) -> int | None: """Try to parse a cell value as an integer pin number. Returns the int or None if the value is not a valid pin number. """ if not value or not str(value).strip(): return None try: return int(float(str(value).strip())) except (ValueError, TypeError): return None def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP: """Parse a PinMAP from a cell dictionary and return a PinMAP object. Algorithm --------- 1. Scan all non-empty cells to determine the rectangular boundary [min_row..max_row] × [min_col..max_col]. 2. Read A1 (0,0) as the package-info string. 3. For each of the four edges, collect pin numbers from the boundary cell and pin names from the adjacent inner cell. 4. Walk the edges counter-clockwise (left → bottom → right → top), deduplicating corner pins by number. Parameters ---------- cells : dict mapping (row, col) → cell text (0-based). Returns ------- PinMAP Raises ------ StructureError If the cell map is empty, the boundary is too small, A1 is missing, or no pins are detected. """ if not cells: raise StructureError("文件为空,无单元格数据") # ── Step 1: determine rectangular boundary ─────────────────── # Exclude (0,0) — it holds the package-info label, not PinMAP data. pin_cells = { rc: v for rc, v in cells.items() if rc != (0, 0) and v and str(v).strip() } if not pin_cells: raise StructureError("未检测到任何 Pin 数据") rows = {r for r, _ in pin_cells} cols = {c for _, c in pin_cells} min_row, max_row = min(rows), max(rows) min_col, max_col = min(cols), max(cols) width = max_col - min_col + 1 height = max_row - min_row + 1 if width < 2 or height < 2: raise StructureError( f"方形区域太小: {width}x{height},至少需要 2x2" ) # ── Step 2: package info from A1 ───────────────────────────── package_info = cells.get((0, 0), "") if not package_info or not str(package_info).strip(): raise StructureError("A1 单元格为空,缺少封装信息") # ── Step 3: build name lookup ──────────────────────────────── # For each edge, pin names live in the cell *adjacent inward* # from the boundary cell that holds the pin number. # # left : number at (r, min_col), name at (r, min_col+1) # bottom : number at (max_row, c), name at (max_row-1, c) # right : number at (r, max_col), name at (r, max_col-1) # top : number at (min_row, c), name at (min_row+1, c) name_map: dict[tuple[int, int], str] = {} # left edge names for r in range(min_row, max_row + 1): name = cells.get((r, min_col + 1), "") if name and str(name).strip(): name_map[(r, min_col)] = str(name).strip() # bottom edge names for c in range(min_col, max_col + 1): name = cells.get((max_row - 1, c), "") if name and str(name).strip(): name_map[(max_row, c)] = str(name).strip() # right edge names for r in range(min_row, max_row + 1): name = cells.get((r, max_col - 1), "") if name and str(name).strip(): name_map[(r, max_col)] = str(name).strip() # top edge names for c in range(min_col, max_col + 1): name = cells.get((min_row + 1, c), "") if name and str(name).strip(): name_map[(min_row, c)] = str(name).strip() # ── Step 4: walk edges counter-clockwise ───────────────────── # Deduplicate by *cell position* (corners are shared cells), # NOT by pin number — duplicate numbers are a data error for # the validator to catch. pins: list[Pin] = [] seen_cells: set[tuple[int, int]] = set() def _add_pin(r: int, c: int, edge: str, pos: int) -> None: if (r, c) in seen_cells: return # corner cell already processed seen_cells.add((r, c)) num = _try_int(cells.get((r, c), "")) if num is None: return pins.append(Pin( number=num, name=name_map.get((r, c), ""), edge=edge, position_on_edge=pos, )) # 4a. Left edge: top → bottom for r in range(min_row, max_row + 1): _add_pin(r, min_col, "left", r - min_row) # 4b. Bottom edge: left → right (skip min_col corner already done) for c in range(min_col + 1, max_col + 1): _add_pin(max_row, c, "bottom", c - min_col) # 4c. Right edge: bottom → top (skip max_row corner already done) for r in range(max_row - 1, min_row - 1, -1): _add_pin(r, max_col, "right", max_row - r) # 4d. Top edge: right → left (skip max_col corner already done) for c in range(max_col - 1, min_col - 1, -1): _add_pin(min_row, c, "top", max_col - c) if not pins: raise StructureError("未检测到任何 Pin 数据") return PinMAP( package_info=str(package_info).strip(), pins=pins, width=width, height=height, grid_origin=(min_row, min_col), raw_cells=cells, )