"""Default designator type names (singular/plural) by letter prefix.""" from __future__ import annotations import re # Minimal useful defaults; projects can override via DB DEFAULT_MAPPINGS: dict[str, tuple[str, str]] = { "C": ("Конденсатор", "Конденсаторы"), "R": ("Резистор", "Резисторы"), "L": ("Катушка индуктивности", "Катушки индуктивности"), "D": ("Диод", "Диоды"), "VD": ("Диод", "Диоды"), "VT": ("Транзистор", "Транзисторы"), "Q": ("Транзистор", "Транзисторы"), "DA": ("Микросхема", "Микросхемы"), "DD": ("Микросхема", "Микросхемы"), "U": ("Микросхема", "Микросхемы"), "X": ("Соединитель", "Соединители"), "XP": ("Соединитель", "Соединители"), "XS": ("Соединитель", "Соединители"), "FU": ("Предохранитель", "Предохранители"), "F": ("Предохранитель", "Предохранители"), "SA": ("Переключатель", "Переключатели"), "SB": ("Кнопка", "Кнопки"), "HL": ("Индикатор", "Индикаторы"), "HG": ("Индикатор", "Индикаторы"), "G": ("Генератор", "Генераторы"), "T": ("Трансформатор", "Трансформаторы"), "TV": ("Трансформатор", "Трансформаторы"), "K": ("Реле", "Реле"), "B": ("Пьезоэлемент", "Пьезоэлементы"), "Z": ("Фильтр", "Фильтры"), } def split_designator(designator: str) -> tuple[str, str]: m = re.match(r"^([A-Za-z]+)(\d*)$", designator.strip()) if m: return m.group(1), m.group(2) for i, ch in enumerate(designator): if ch.isdigit(): return designator[:i], designator[i:] return designator, "" def designator_sort_key(designator: str) -> tuple[str, int, str]: letter, num = split_designator(designator) try: n = int(num) if num else 0 except ValueError: n = 0 return letter, n, num return letter.upper(), n, num def prefix_from_designator(designator: str) -> str: letter, _ = split_designator(designator) return letter.upper() def get_type_names( designator: str, mappings: dict[str, tuple[str, str]] | None = None ) -> tuple[str, str]: maps = {**DEFAULT_MAPPINGS, **(mappings or {})} prefix = prefix_from_designator(designator) if prefix in maps: return maps[prefix] # try 2-letter then 1-letter if len(prefix) >= 2 and prefix[:2] in maps: return maps[prefix[:2]] if prefix[:1] in maps: return maps[prefix[:1]] return (prefix, prefix) def format_designator_range(designators: list[str]) -> str: if not designators: return "" if len(designators) == 1: return designators[0] if len(designators) == 2: return f"{designators[0]}, {designators[1]}" return f"{designators[0]}-{designators[-1]}" def format_consecutive_ranges(designators: list[str]) -> str: """Group consecutive numbered designators into ranges.""" if not designators: return "" parsed = [] for d in designators: letter, num = split_designator(d) try: n = int(num) if num else None except ValueError: n = None parsed.append((d, letter, n)) segments: list[list[str]] = [] current: list[str] = [] last_letter = None last_num = None for d, letter, n in parsed: if not current: current = [d] last_letter, last_num = letter, n continue if letter == last_letter and n is not None and last_num is not None and n == last_num + 1: current.append(d) last_num = n else: segments.append(current) current = [d] last_letter, last_num = letter, n if current: segments.append(current) parts = [] for seg in segments: if len(seg) == 1: parts.append(seg[0]) elif len(seg) == 2: parts.append(f"{seg[0]}, {seg[1]}") else: parts.append(f"{seg[0]}-{seg[-1]}") return ", ".join(parts) def split_long_text(text: str, max_length: int) -> list[str]: if not text or len(text) <= max_length: return [text] if text is not None else [""] parts: list[str] = [] remaining = text while len(remaining) > max_length: chunk = remaining[:max_length] split_at = chunk.rfind(",") if split_at < max_length // 3: split_at = chunk.rfind(" ") if split_at < max_length // 3: split_at = max_length parts.append(remaining[:split_at].rstrip(", ").strip()) remaining = remaining[split_at:].lstrip(", ").strip() if remaining: parts.append(remaining) return parts or [""] def _split_expression_parts(expr: str) -> list[str]: """Split Altium expression by + outside quotes.""" parts: list[str] = [] current: list[str] = [] in_double = False in_single = False for ch in expr: if ch == '"' and not in_single: in_double = not in_double current.append(ch) elif ch == "'" and not in_double: in_single = not in_single current.append(ch) elif ch == "+" and not in_double and not in_single: if current: parts.append("".join(current).strip()) current = [] else: current.append(ch) if current: parts.append("".join(current).strip()) return parts def _resolve_property( name: str, props: dict[str, str], designator: str = "", resolving: frozenset[str] | None = None, ) -> str: if resolving is None: resolving = frozenset() if name in resolving: return "" if name == "Designator": raw = designator or props.get("Designator", "") else: raw = props.get(name, "") if not raw: return "" if raw.startswith("="): return resolve_expression(raw, props, designator, resolving | {name}) return raw def resolve_expression( value: str, props: dict[str, str], designator: str = "", resolving: frozenset[str] | None = None, ) -> str: """ Altium complex string: ='literal'+"Property"+Name - single quotes = literal text - double quotes = component property reference - bare identifiers = property reference """ if resolving is None: resolving = frozenset() if len(resolving) > 64: return "" if not value: return "" value = value.strip() if value.startswith('"') and value.endswith('"') and not value.startswith('="'): return _resolve_property(value[1:-1], props, designator, resolving) if not value.startswith("="): return value result: list[str] = [] for part in _split_expression_parts(value[1:]): if not part: continue if part.startswith("'") and part.endswith("'") and len(part) >= 2: result.append(part[1:-1]) elif part.startswith('"') and part.endswith('"') and len(part) >= 2: result.append(_resolve_property(part[1:-1], props, designator, resolving)) elif part == "Designator": result.append(designator or props.get("Designator", "")) else: result.append(_resolve_property(part, props, designator, resolving)) return "".join(result)