"""型号→口径/压力 解析器(消减导入黄标,TAPD #1000192 / BLUEPRINT §8.2 第③处缺口)
VBA 时代空白口径靠人工 inputbox 补录(对标 function.vb),PySide6 版当前
``normalize_spec`` 仅解析 ``product_spec`` 字符串、``normalize_pn`` 仅从型号后缀/字典
取压力,导致「合同口径空白」「型号内嵌 DN/PN 未被提取」两类行直接黄标。
本解析器从**产品型号**内嵌的显式标识反推口径/压力,作为
``DNService.resolve_caliber`` / ``PNService.resolve_pn`` 的**兜底**:
- 口径:型号内嵌 ``DN150`` / ``DN100*80`` → 口径值(``150`` / ``100*80``);
- 压力:显式 ``PN16``(PN 为 bar,MPa=PN/10)、英制 ``CLASS1500`` / ``CL1500``
(磅级→Lb)、以及型号后缀嵌入的**标准公称压力代号**(如 ``J61Y-160`` 的 ``160`` → 16MPa、
``EF/PZ-320P`` 的 ``320`` → 32MPa、``BEDDZY-250`` 的 ``250`` → 25MPa)。仅白名单内标准
代号采信,杜绝流水号误判(如 ``...-13`` 不再误作 1.3MPa)。
设计约束(对齐验收标准):
- **不改写 ``product_spec``(合同口径)**,只供 ``product_spec_norm`` / ``pressure_value``;
- 无型号源 / 未命中显式标识时**返回空、不伪造**,仍走黄标 + 校正队列;
- 纯正则解析、无副作用,便于单测锁定。
"""
from __future__ import annotations
import re
# 显式内嵌口径:DN150 / DN 100 / DN100*80 / DN100x80 / DN100×80
_DN_RE = re.compile(r"DN\s*(\d+(?:\s*[*x×]\s*\d+)?)", re.IGNORECASE)
# 显式内嵌公称压力:PN16 / PN 25 / PN2.5(PN 为 bar,MPa = PN/10)
_PN_RE = re.compile(r"PN\s*(\d+(?:\.\d+)?)", re.IGNORECASE)
# 英制磅级:CLASS1500 / CL1500 / CLASS 900
_CLASS_RE = re.compile(r"(?:CLASS|CL)\s*(\d+)", re.IGNORECASE)
# 型号内嵌压力代号:紧跟 - 或 / 的数字(如 J61Y-160 的 160、EF/PZ-320P 的 320、
# BEDDZY-250 的 250)。仅当数值属于「标准公称压力代号」白名单才采信,避免误吃
# 口径(如 DN150)或流水号(如 ...-13 → 1.3MPa 的误判)。
_EMBED_PN_RE = re.compile(r"[-/](\d+(?:\.\d+)?)(?=[A-Za-z]?(?:[-/\s]|$))")
# 标准公称压力代号(GB/T 阀门口径压力系列,代号 = 10 × MPa)。
# 仅这些数值视为合法型号内嵌压力代号:消除 13/80 等流水号误判,同时覆盖
# 常用标准代号(6, 10, 16, 25, 40, 64, 100, 160, 200, 250, 320, 400, 500, 630, 800, 1000)。
# 注:1(PN1=0.1MPa)极罕见且 /1 多为版本号,排除;2.5 等小数代号歧义大,排除,宁缺毋滥。
_STANDARD_PN_CODES = {6, 10, 16, 25, 40, 64, 100, 160, 200, 250, 320, 400, 500, 630, 800, 1000}
# 标准英制磅级(与 PNService.LB_TO_MPA_MAP 对齐;仅这些视为合法 CLASS)
_KNOWN_LB_LEVELS = {150, 300, 600, 800, 900, 1500, 2500}
def _fmt_mpa(mpa: float) -> str:
"""格式化 MPa 显示值:去尾零(2.0→'2',1.6→'1.6',10.0→'10')。"""
return f"{mpa:.2f}".rstrip("0").rstrip(".")
[文档]
class ModelSpecParser:
"""从产品型号提取口径/压力的纯解析器(兜底 DNService/PNService)。
所有方法均为无副作用的静态/类方法,输入型号字符串,输出解析结果;
无法解析时返回空串/空 ``pn_display``,绝不伪造。
"""
# ============================================================
# 口径
# ============================================================
[文档]
@classmethod
def parse_caliber(cls, model: str | None) -> str:
"""从型号内嵌的 ``DNxxx`` 提取口径值。
Args:
model: 产品型号,如 ``"BQ647Y-25P/DN150"`` / ``"DN100*80"``
Returns:
str: 口径值(``"150"`` / ``"100*80"``);无内嵌 DN 时返回 ``""``。
"""
if not model:
return ""
# 归一:多行/制表符合并为空格,避免 \n 切断型号导致漏判
m = _DN_RE.search(re.sub(r"\s+", " ", str(model)).strip())
if not m:
return ""
# 归一:去空格,分隔符统一为 "*"
raw = m.group(1)
return re.sub(r"\s+", "", raw).replace("x", "*").replace("×", "*").replace("X", "*")
# ============================================================
# 压力
# ============================================================
[文档]
@classmethod
def parse_pressure(cls, model: str | None) -> dict[str, str]:
"""从型号内嵌标识反推公称压力。
优先级:显式 ``PNxx`` > 英制 ``CLASSxx`` > 型号后缀嵌入压力编码。
Args:
model: 产品型号,如 ``"KHBF-25-PN320"`` / ``"CLASS1500-150A"``
Returns:
dict: ``{"pn_value", "pn_unit", "pn_display", "source"}``;
无法解析时 ``pn_display`` 为 ``""``、``source`` 为 ``"unknown"``。
"""
result = {"pn_value": "", "pn_unit": "", "pn_display": "", "source": "unknown"}
if not model:
return result
# 归一:多行/制表符合并为空格,避免 \n 切断型号导致漏判(如 BEDDZY-250\n(...))
text = re.sub(r"\s+", " ", str(model)).strip()
# === 显式 PN:PN 为 bar,MPa = PN/10 ===
pn = _PN_RE.search(text)
if pn:
try:
mpa = float(pn.group(1)) / 10.0
except ValueError:
mpa = 0.0
if mpa > 0:
val = _fmt_mpa(mpa)
result.update(
pn_value=val, pn_unit="MPa", pn_display=f"{val}MPa", source="model_pn"
)
return result
# === 英制 CLASS/CL → 磅级(Lb) ===
cl = _CLASS_RE.search(text)
if cl:
level = int(cl.group(1))
if level in _KNOWN_LB_LEVELS:
result.update(
pn_value=str(level),
pn_unit="Lb",
pn_display=f"{level}Lb",
source="model_class",
)
return result
# === 型号后缀/中段嵌入标准压力代号(白名单校验,÷10 得 MPa) ===
embed = cls._parse_embed_pn(text)
if embed:
return embed
return result
@classmethod
def _parse_embed_pn(cls, text: str) -> dict[str, str] | None:
"""从型号内嵌标准压力代号反推压力(白名单校验)。
仅采信 ``_STANDARD_PN_CODES`` 内的标准公称压力代号(÷10 得 MPa),
杜绝流水号/口径误判(如 ``...-13`` 不再误作 1.3MPa)。
Returns:
命中时返回压力 dict;否则返回 ``None``。
"""
for m in _EMBED_PN_RE.finditer(text):
try:
code = float(m.group(1))
except ValueError:
continue
# 小数代号歧义大(如 2.5 可能直接表示 MPa),跳过
if code != int(code):
continue
if int(code) in _STANDARD_PN_CODES:
val = _fmt_mpa(code / 10.0)
return {
"pn_value": val,
"pn_unit": "MPa",
"pn_display": f"{val}MPa",
"source": "model_suffix",
}
return None