chronaxis.core.unit_registry
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""在编译期解析版本4的有限单位目录,并推导转换及复合单位。23输入为单位文本或不可变Unit,输出单位元数据及转换系数,不处理样本值。4物理量纲和量种分别检查;有理数与π倍率保持符号形式,避免提前舍入。5解析器仅接受登记原子及有限乘除幂语法,不使用eval、不读写文件。6本模块是当前配置唯一的单位目录;未知标签不会隐式使用其他单位表。7"""89# 所有对象仅依赖标准库,登记表建成后不允许调用侧修改。10from dataclasses import dataclass11from fractions import Fraction12from math import isfinite, isqrt, pi13import re14from types import MappingProxyType151617REGISTRY_VERSION = "chronaxis-units-v4.2"18KIND_BASES = ("V", "A", "s", "N", "m", "K", "delta_K", "rad")19# 各量种在质量、长度、时间、电流、温度五个物理维度上的指数。20_PHYSICAL_BASES = (21 (1, 2, -3, -1, 0), (0, 0, 0, 1, 0), (0, 0, 1, 0, 0),22 (1, 1, -2, 0, 0), (0, 1, 0, 0, 0), (0, 0, 0, 0, 1),23 (0, 0, 0, 0, 1), (0, 0, 0, 0, 0),24)25_MAX_EXPONENT = 3226_MAX_FACTOR_BITS = 4096272829def _indices(value: tuple[int, ...], size: int, field: str, *, bounded: bool = True) -> None:30 """检查不可变指数向量,拒绝布尔值和超限幂。3132 参数: value为指数tuple,size为维数,field用于错误定位;bounded决定是否限制±32。33 返回: None。34 异常: TypeError为结构或类型错误,ValueError为指数超限。35 """36 # bool是int子类,必须按精确类型检查,防止非法元数据混入。37 if type(value) is not tuple or len(value) != size or any(type(item) is not int for item in value):38 raise TypeError(f"{field}必须为{size}个整数构成的tuple")39 if bounded and any(abs(item) > _MAX_EXPONENT for item in value):40 raise ValueError(f"{field}组合指数必须在-32到32之间")414243def _dimensions(kind: tuple[int, ...]) -> tuple[int, ...]:44 """从量种基计算物理量纲,不据量纲反猜量种。4546 参数: kind为八维量种指数。47 返回: 质量、长度、时间、电流、温度的五维指数。48 异常: 调用方负责输入结构检查。49 """50 # 同物理量纲仍可能是能量/力矩等不同量种。51 return tuple(sum(power * basis[index] for power, basis in zip(kind, _PHYSICAL_BASES)) for index in range(5))525354@dataclass(frozen=True)55class Scale:56 """保存正有理系数乘π整数次幂,只有执行前才转float64。5758 factor与pi_power不可变;负方向属于信号转换,不属于单位倍率。59 分子和分母分别最多4096位,避免无量纲嵌套幂绕过量种指数限制。60 """6162 factor: Fraction # 正有理倍率,保持十进制换算精确。63 pi_power: int = 0 # π指数,用于角度与角速度转换。6465 def __post_init__(self) -> None:66 """检查倍率结构及范围。6768 参数: 无。69 返回: None。70 异常: TypeError为类型错误,ValueError为非正倍率、指数或有理数位数超限。71 """72 # 不把传入浮点近似自动伪装成精确的单位常数。73 if type(self.factor) is not Fraction or type(self.pi_power) is not int:74 raise TypeError("Scale需要Fraction倍率和整数π指数")75 if self.factor <= 0 or abs(self.pi_power) > _MAX_EXPONENT:76 raise ValueError("单位倍率必须为正,π指数必须在-32到32之间")77 # 无量纲幂不增加kind指数,必须单独限制精确有理数的存储规模。78 if self.factor.numerator.bit_length() > _MAX_FACTOR_BITS or self.factor.denominator.bit_length() > _MAX_FACTOR_BITS:79 raise ValueError("单位倍率的分子或分母超过4096位限制")8081 def to_float(self) -> float:82 """在数值执行边界取得有限float倍率。8384 参数: 无。85 返回: 正有限float;π取math.pi的float64表示,整体倍率最后统一舍入。86 异常: ValueError为上溢、下溢到零或非有限结果。87 """88 # 先在有界有理数内组合π近似,避免factor单独溢出而最终倍率本来可表示。89 try:90 value = float(self.factor * Fraction.from_float(pi) ** self.pi_power)91 except OverflowError as error:92 raise ValueError("单位倍率不能表示为有限float64") from error93 if not isfinite(value) or value <= 0:94 raise ValueError("单位倍率不能表示为正有限float64")95 return value9697 def multiply(self, other: "Scale") -> "Scale":98 """相乘两个符号倍率。99100 参数: other为Scale。101 返回: 新Scale,不舍入。102 异常: TypeError为输入类型错误,ValueError为指数超限。103 """104 # 先核实对象,避免通过任意属性调用构造未知倍率。105 if not isinstance(other, Scale):106 raise TypeError("倍率乘法需要Scale")107 return Scale(self.factor * other.factor, self.pi_power + other.pi_power)108109 def divide(self, other: "Scale") -> "Scale":110 """相除两个正符号倍率。111112 参数: other为Scale。113 返回: 新Scale,不舍入。114 异常: TypeError为输入类型错误,ValueError为指数超限。115 """116 # 单位倍率已保证非零,因此此处不会产生正常除零分支。117 if not isinstance(other, Scale):118 raise TypeError("倍率除法需要Scale")119 return Scale(self.factor / other.factor, self.pi_power - other.pi_power)120121 def power(self, exponent: int) -> "Scale":122 """对符号倍率求有界整数幂。123124 参数: exponent为-32到32的原生整数。125 返回: 新Scale。126 异常: TypeError为非整数,ValueError为幂或结果指数超限。127 """128 # 文本幂和内部平方共用同一明确边界。129 if type(exponent) is not int:130 raise TypeError("单位幂必须为整数")131 if abs(exponent) > _MAX_EXPONENT:132 raise ValueError("单位幂必须在-32到32之间")133 return Scale(self.factor ** exponent, self.pi_power * exponent)134135 def square_root(self) -> "Scale":136 """对可精确开方的符号倍率取正平方根。137138 参数: 无。139 返回: 新Scale。140 异常: ValueError为有理系数非完全平方或π指数为奇数。141 """142 # 不引入未定义的根号常数,也不以浮点近似替代精确身份。143 numerator = isqrt(self.factor.numerator)144 denominator = isqrt(self.factor.denominator)145 if numerator * numerator != self.factor.numerator or denominator * denominator != self.factor.denominator or self.pi_power % 2:146 raise ValueError("单位倍率不能在有理数和整数π幂范围内精确开方")147 return Scale(Fraction(numerator, denominator), self.pi_power // 2)148149150@dataclass(frozen=True)151class Unit:152 """保存单位拼写、物理量纲、量种、倍率及可选温度偏移。153154 数值换到基准的公式为value*scale+offset;偏移仅用于绝对温度。155 量种指数独立区分能量、力矩、角度、比例及绝对温度/温差。156 symbol可以是中间代数描述;只有canonical_unit返回的symbol保证可重读。157 """158159 symbol: str # 当前表示的单位文本,复合表示可不同于基准名称。160 dimensions: tuple[int, ...] # 质量、长度、时间、电流、温度。161 kind: tuple[int, ...] # KIND_BASES顺序的八维量种指数。162 scale: Scale # 相对量种基准的符号倍率。163 offset: Fraction = Fraction(0) # 基准单位中的偏移,如273.15K。164 temperature_role: str | None = None # absolute、delta或非单一温度。165166 def __post_init__(self) -> None:167 """验证模型不变量,防止手工构造伪造量纲或温度角色。168169 参数: 无。170 返回: None。171 异常: TypeError为结构错误,ValueError为量纲/量种/角色不一致。172 """173 # 模型不承接任意对象转换;语法限制由文本入口负责。174 if type(self.symbol) is not str or not self.symbol.strip() or not isinstance(self.scale, Scale) or type(self.offset) is not Fraction:175 raise TypeError("Unit需要非空文本、Scale及Fraction偏移")176 _indices(self.kind, 8, "kind")177 # 物理指数由有界kind唯一推导,不再单设同样上限误拒V^32等合法单位。178 _indices(self.dimensions, 5, "dimensions", bounded=False)179 if self.dimensions != _dimensions(self.kind):180 raise ValueError("物理量纲与量种指数不一致")181 if self.temperature_role != _temperature_role(self.kind):182 raise ValueError("温度角色与量种指数不一致")183 if self.offset and (self.temperature_role != "absolute" or self.scale.pi_power):184 raise ValueError("偏移仅用于无π倍率的绝对温度")185186187def _temperature_role(kind: tuple[int, ...]) -> str | None:188 """仅为单一绝对温度或温差赋角色,复合温度仍由量种指数区分。189190 参数: kind为八维指数。191 返回: absolute、delta或None。192 异常: 无,结构由调用方检查。193 """194 # K²与delta_K²的区别留在量种基中,不伪装成单一温度。195 if kind == (0, 0, 0, 0, 0, 1, 0, 0):196 return "absolute"197 if kind == (0, 0, 0, 0, 0, 0, 1, 0):198 return "delta"199 return None200201202def _make(symbol: str, kind: tuple[int, ...], scale: Scale | None = None,203 offset: Fraction = Fraction(0)) -> Unit:204 """从量种统一构造含一致物理量纲和温度角色的单位。205206 参数: symbol为拼写,kind为八维指数,scale缺省1,offset缺省0。207 返回: 已校验Unit。208 异常: Unit验证错误直接传播。209 """210 # 所有代数运算使用同一构造步骤,避免遗漏派生温度角色。211 return Unit(symbol, _dimensions(kind), kind, scale or Scale(Fraction(1)), offset, _temperature_role(kind))212213214def _require_unit(value: Unit) -> None:215 """检查单位代数入口的typed对象。216217 参数: value为待检查对象。218 返回: None。219 异常: TypeError为非Unit对象。220 """221 # 未知单位必须由上层保留unverified,不能当成无量纲计算。222 if not isinstance(value, Unit):223 raise TypeError("单位运算需要已解析的Unit")224225226def _require_multiplicative(value: Unit) -> None:227 """拒绝带偏移温度参与乘除和幂运算。228229 参数: value为Unit。230 返回: None。231 异常: TypeError为非Unit;ValueError为带偏移单位。232 """233 # 在自动规范化之前检查,不能把摄氏度偷偷转换成K绕过限制。234 _require_unit(value)235 if value.offset:236 raise ValueError("带偏移的温度不能直接乘除或求幂,请先显式转换到K")237238239def multiply(left: Unit, right: Unit) -> Unit:240 """组合单位乘法,保留数值仍需应用的原尺度。241242 参数: left、right为已解析Unit。243 返回: 量种指数相加、倍率相乘的新Unit。244 异常: TypeError为类型错误;ValueError为偏移单位或指数超限。245 """246 # 仅组合元数据,不改数值表达式的计算结合顺序。247 _require_multiplicative(left)248 _require_multiplicative(right)249 kind = tuple(a + b for a, b in zip(left.kind, right.kind))250 return _make(f"({left.symbol})*({right.symbol})", kind, left.scale.multiply(right.scale))251252253def divide(left: Unit, right: Unit) -> Unit:254 """组合单位除法,量种相同的比值成为普通比例。255256 参数: left、right为已解析Unit。257 返回: 指数相减、倍率相除的新Unit。258 异常: TypeError为类型错误;ValueError为偏移单位或指数超限。259 """260 # 单位的除法不判断样本分母值,实际除零由数值核处理。261 _require_multiplicative(left)262 _require_multiplicative(right)263 kind = tuple(a - b for a, b in zip(left.kind, right.kind))264 return _make(f"({left.symbol})/({right.symbol})", kind, left.scale.divide(right.scale))265266267def _power(unit: Unit, exponent: int) -> Unit:268 """处理解析器和平方共用的有界单位整数幂。269270 参数: unit为Unit,exponent为-32到32整数。271 返回: 新Unit。272 异常: TypeError为类型错误;ValueError为偏移温度、幂或组合超限。273 """274 # 即使幂为0或1,也不能让偏移温度走乘法语义。275 _require_multiplicative(unit)276 scale = unit.scale.power(exponent)277 return _make(f"({unit.symbol})^{exponent}", tuple(value * exponent for value in unit.kind), scale)278279280def square(unit: Unit) -> Unit:281 """求单位平方,同时平方其倍率。282283 参数: unit为Unit。284 返回: 新Unit。285 异常: TypeError或ValueError沿单位幂检查传播。286 """287 # 实际样本平方的溢出检查属于两个数值后端。288 return _power(unit, 2)289290291def square_root(unit: Unit) -> Unit:292 """对偶数量种指数和可精确开方倍率求平方根。293294 参数: unit为Unit。295 返回: 新Unit;K²返回K量种,温差平方返回温差量种。非原子尺度的symbol296 只供内部描述;公开显示或重新解析应先取canonical_unit。297 异常: TypeError为类型错误;ValueError为偏移、奇数指数或非精确倍率。298 边界: 数值编译器先把输入转为规范基准再开方,不能据中间倍率非完全平方299 拒绝本来合法的sqrt(mA*A);本函数自身不转换样本数值。300 """301 # 只检查物理维度会把rad误认为普通比例,必须逐个检查量种基。302 _require_multiplicative(unit)303 if any(value % 2 for value in unit.kind):304 raise ValueError("开方要求所有量种指数为偶数")305 kind = tuple(value // 2 for value in unit.kind)306 scale = unit.scale.square_root()307 # 返回规范基名或与其尺度等价的原子名,避免构造不允许的sqrt文本。308 base = canonical_unit(_make("root", kind))309 for candidate in ATOMIC_UNITS.values():310 if candidate.kind == kind and candidate.scale == scale and not candidate.offset:311 return candidate312 if scale == Scale(Fraction(1)):313 return base314 return _make(f"sqrt({unit.symbol})", kind, scale)315316317def conversion(source: Unit, target: Unit) -> tuple[Scale, Fraction]:318 """计算source数值到target的乘法系数和加法偏移。319320 参数: source、target为已解析Unit。321 返回: (scale,offset),数值公式为value*scale+offset。322 异常: TypeError为非Unit;ValueError为量种、温度角色或偏移不兼容。323 """324 # 不能以物理指数相同为由把力矩、角度或温差变成另一种量。325 _require_unit(source)326 _require_unit(target)327 if source.kind != target.kind or source.temperature_role != target.temperature_role:328 raise ValueError(f"单位量种不兼容:{source.symbol}→{target.symbol}")329 difference = source.offset - target.offset330 if difference and target.scale.pi_power:331 raise ValueError("带π的目标倍率不能承接温度偏移")332 return source.scale.divide(target.scale), difference / target.scale.factor333334335def _kind_symbol(kind: tuple[int, ...]) -> str:336 """把量种指数写成可重解析的复合单位,优先显示电功率W和电能J。337338 参数: kind为已验证的八维整数指数。339 返回: 由规范单位和整数乘除幂组成的文本;不修改尺度或温度偏移。340 异常: 无,模型已限制指数。力矩等不同量种不按物理维度折成电能。341 """342 remaining = list(kind)343 shared = 0344 if remaining[0] * remaining[1] > 0:345 sign = 1 if remaining[0] > 0 else -1346 shared = sign * min(abs(remaining[0]), abs(remaining[1]))347 energy = 0348 if shared * remaining[2] > 0:349 sign = 1 if shared > 0 else -1350 energy = sign * min(abs(shared), abs(remaining[2]))351 remaining[0] -= shared352 remaining[1] -= shared353 remaining[2] -= energy354355 # 只合并相同量种的V、A、s整数指数;N*m及温差维度保持原样。356 factors = [("J", energy), ("W", shared - energy)]357 factors.extend(zip(KIND_BASES, remaining))358 numerator = []359 denominator = []360 for base, exponent in factors:361 if not exponent:362 continue363 destination = numerator if exponent > 0 else denominator364 power = abs(exponent)365 destination.append(base if power == 1 else f"{base}^{power}")366 top = "*".join(numerator) or "1"367 if not denominator:368 return top369 bottom = "*".join(denominator)370 return f"{top}/{bottom}" if len(denominator) == 1 else f"{top}/({bottom})"371372373def canonical_unit(unit: Unit) -> Unit:374 """取得该量种倍率1、偏移0的稳定计算/显示基准。375376 参数: unit为Unit。377 返回: 首选规范单位或由固定量种基构造的单位。378 异常: TypeError为非Unit,ValueError为模型不一致。379 """380 # 此函数只返回目标,调用者必须应用conversion而不是只替换标签。381 _require_unit(unit)382 if unit.kind in PREFERRED_UNITS:383 return PREFERRED_UNITS[unit.kind]384 return _make(_kind_symbol(unit.kind), unit.kind)385386387def output_label(unit: Unit, spelling: str) -> str:388 """为所选尺度返回可重新解析的输出单位名称。389390 参数: unit是已解析目标;spelling是生成它的单位文本。391 返回: 原子单位用登记名称,基准组合用规范名称,其他倍率保留原拼写。392 异常: 调用者须先解析单位;非法模型沿用注册表检查。393 """394 # 不能把非基准复合单位直接换成基准标签,否则标签和数值倍率不符。395 if unit.symbol in ATOMIC_UNITS:396 return unit.symbol397 base = canonical_unit(unit)398 return base.symbol if (unit.scale, unit.offset) == (base.scale, base.offset) else spelling.strip()399400401def _build_atoms() -> dict[str, Unit]:402 """建立有限原子表,不自动扩张SI前缀或读取外部定义。403404 参数: 无。405 返回: 供模块初始化冻结的字典。406 异常: 注册项不一致时传播Unit验证错误。407 """408 # 每项明确给出量种、尺度和偏移;这里只生成编译期少量对象。409 zero = (0,) * 8410 definitions = {"1": _make("1", zero), "%": _make("%", zero, Scale(Fraction(1, 100)))}411 for index, name in enumerate(KIND_BASES):412 kind = tuple(int(position == index) for position in range(8))413 definitions[name] = _make(name, kind)414 composites = {415 "W": (1, 1, 0, 0, 0, 0, 0, 0), "J": (1, 1, 1, 0, 0, 0, 0, 0),416 "Ah": (0, 1, 1, 0, 0, 0, 0, 0), "Ohm": (1, -1, 0, 0, 0, 0, 0, 0),417 "Hz": (0, 0, -1, 0, 0, 0, 0, 0), "rpm": (0, 0, -1, 0, 0, 0, 0, 1),418 "Pa": (0, 0, 0, 1, -2, 0, 0, 0),419 }420 for name, kind in composites.items():421 scale = Scale(Fraction(3600)) if name == "Ah" else Scale(Fraction(1, 30), 1) if name == "rpm" else Scale(Fraction(1))422 definitions[name] = _make(name, kind, scale)423 for name, base, factor in (424 ("mV", "V", Fraction(1, 1000)), ("mA", "A", Fraction(1, 1000)),425 ("kW", "W", Fraction(1000)), ("Wh", "J", Fraction(3600)),426 ("kWh", "J", Fraction(3600000)), ("ms", "s", Fraction(1, 1000)),427 ("us", "s", Fraction(1, 1000000)), ("ns", "s", Fraction(1, 1000000000)),428 ("mOhm", "Ohm", Fraction(1, 1000)), ("kOhm", "Ohm", Fraction(1000)),429 ("MOhm", "Ohm", Fraction(1000000)), ("kPa", "Pa", Fraction(1000)),430 ("bar", "Pa", Fraction(100000)),431 ):432 definitions[name] = _make(name, definitions[base].kind, Scale(factor))433 definitions["degC"] = _make("degC", definitions["K"].kind, offset=Fraction(27315, 100))434 definitions["delta_degC"] = _make("delta_degC", definitions["delta_K"].kind)435 definitions["deg"] = _make("deg", definitions["rad"].kind, Scale(Fraction(1, 180), 1))436 return definitions437438439# 别名严格大小写敏感;C不解释为摄氏度,任意未登记前缀仍未知。440ATOMIC_UNITS = MappingProxyType(_build_atoms())441UNIT_ALIASES = MappingProxyType({442 "µs": "us", "μs": "us", "°C": "degC", "℃": "degC", "ΔK": "delta_K",443 "Δ°C": "delta_degC", "Δ℃": "delta_degC", "Ω": "Ohm", "Ω": "Ohm",444 "mΩ": "mOhm", "kΩ": "kOhm", "MΩ": "MOhm", "°": "deg",445})446447448def _build_preferred() -> dict[tuple[int, ...], Unit]:449 """登记倍率1的默认名称,不取用户参数顺序决定显示单位。450451 参数: 无。452 返回: 按量种索引的首选单位字典,初始化后冻结。453 异常: 模型错误直接传播。454 """455 # 电荷、速度等复合基准无需作为单个解析原子再次登记。456 result = {ATOMIC_UNITS[name].kind: ATOMIC_UNITS[name] for name in ("1", "V", "A", "W", "J", "s", "K", "delta_K", "Ohm", "m", "Hz", "rad", "N", "Pa")}457 for symbol, kind in (458 ("A*s", (0, 1, 1, 0, 0, 0, 0, 0)), ("m/s", (0, 0, -1, 0, 1, 0, 0, 0)),459 ("m/s^2", (0, 0, -2, 0, 1, 0, 0, 0)), ("rad/s", (0, 0, -1, 0, 0, 0, 0, 1)),460 ("N*m", (0, 0, 0, 1, 1, 0, 0, 0)),461 ):462 result[kind] = _make(symbol, kind)463 return result464465466PREFERRED_UNITS = MappingProxyType(_build_preferred())467468469class _Parser:470 """用有限词元递归下降解析单位;未知原子保留None但仍检查完整语法。471472 状态仅为本次词元tuple与读取位置,不跨计划缓存,不执行输入文本。473 """474475 def __init__(self, text: str) -> None:476 """构建有限词元流。477478 参数: text为长度已检查的非空单位文本。479 返回: None。480 异常: ValueError为词元数量超限。481 """482 # 仅归一化明确批准的乘号和上标,不做任意Unicode折叠。483 normalized = text.replace("·", "*").replace("²", "^2").replace("³", "^3")484 self.tokens = tuple(re.findall(r"[*/^()]|[^\s*/^()]+", normalized))485 if len(self.tokens) > 64:486 raise ValueError("单位文本不能超过64个词元")487 self.position = 0488489 def peek(self) -> str | None:490 """查看当前词元,不推进位置。491492 参数: 无。493 返回: 当前文本或流尾None。494 异常: 无。495 """496 # 显式流尾使缺右括号与缺运算数得到语法错误。497 return self.tokens[self.position] if self.position < len(self.tokens) else None498499 def factor(self) -> Unit | None:500 """解析原子或括号表达式及可选整数幂。501502 参数: 无。503 返回: 已知Unit或未知原子的None。504 异常: ValueError为语法、幂或已知单位运算错误。505 """506 token = self.peek()507 # 括号不接受隐式乘法,也不能用空括号代替无量纲1。508 if token == "(":509 self.position += 1510 result = self.expression()511 if self.peek() != ")":512 raise ValueError("单位表达式缺少右括号")513 self.position += 1514 elif token is None or token in ("*", "/", "^", ")"):515 raise ValueError("单位表达式缺少原子或括号内表达式")516 else:517 self.position += 1518 # 不把表达式运算或Python对象访问文本吞为一个未知原子。519 if any(character in token for character in "+-.[]{}=,:;!&|\\'\"<>"):520 raise ValueError(f"单位原子含不允许的语法:{token}")521 result = ATOMIC_UNITS.get(UNIT_ALIASES.get(token, token))522 # 单项最多一个幂;连续幂必须写成明确括号结构。523 if self.peek() == "^":524 self.position += 1525 exponent_text = self.peek()526 if exponent_text is None or re.fullmatch(r"[+-]?[0-9]+", exponent_text) is None:527 raise ValueError("单位幂必须为整数文本")528 exponent = int(exponent_text)529 if abs(exponent) > _MAX_EXPONENT:530 raise ValueError("单位幂必须在-32到32之间")531 self.position += 1532 if result is not None:533 result = _power(result, exponent)534 return result535536 def expression(self) -> Unit | None:537 """按同级左结合解析乘除,完整检查未知项旁的已知非法温度。538539 参数: 无。540 返回: 已知Unit,含未知原子时为None。541 异常: ValueError为语法或单位组合错误。542 """543 result = self.factor()544 while self.peek() in ("*", "/"):545 operator = self.peek()546 self.position += 1547 right = self.factor()548 # 未知标签不掩盖已知摄氏度的非法乘除。549 if result is not None:550 _require_multiplicative(result)551 if right is not None:552 _require_multiplicative(right)553 if result is None or right is None:554 result = None555 else:556 result = multiply(result, right) if operator == "*" else divide(result, right)557 return result558559560def resolve_unit(text: str) -> Unit | None:561 """解析登记单位和受限复合文本,未知原子返回None。562563 参数: text为最多256字符的非空文本,允许首尾空白。564 返回: Unit或None;语法正确的未知单位不伪装为verified。565 异常: TypeError为非文本;ValueError为空、语法、资源或非法单位组合。566 """567 # 原始长度先限额,不能靠大量空白绕过入口限制。568 if type(text) is not str:569 raise TypeError("单位必须为文本")570 if len(text) > 256 or not text.strip():571 raise ValueError("单位必须为非空且不超过256字符的文本")572 # 无运算标记的自定义多词标签沿旧unknown政策保留;已知原子并列仍是坏语法。573 stripped = text.strip()574 if not any(character in stripped for character in "*/^()·²³"):575 words = stripped.split()576 if len(words) > 1 and not any(character in stripped for character in "+-.[]{}=,:;!&|\\'\"<>") and all(UNIT_ALIASES.get(word, word) not in ATOMIC_UNITS for word in words):577 return None578 parser = _Parser(stripped)579 result = parser.expression()580 if parser.peek() is not None:581 raise ValueError(f"单位表达式存在多余词元:{parser.peek()}")582 return result583