Files
GostGenerator/web/backend/app/services/designators.py
T
2026-09-02 14:25:22 +03:00

231 lines
7.6 KiB
Python

"""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)