Chronaxis用户手册English
本页目录

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
Chronaxis 用户手册 · 离线阅读