"""EST 식 글 토큰(PLAN 793-1 · 812-1 · eval 없음) — 원파일 산근 변환(`M02_Doc_NativeEst`)이 쓰는 수 · 토큰 읽기만. 옛 엑셀 글 추정 사슬(줄 꼴 · 역할 · 값 역색인 · `parse_block`)은 812-1 엑셀 읽기기(`M02_San_Xlsx`)로 바뀌어 걷음. """ from __future__ import annotations import re from M02_MasterTemplete import M02_San_Expr as ex NUMRE = r"\d{1,3}(?:,\d{3})+(?:\.\d*)?|\d+(?:\.\d*)?|\.\d+" EPS = ex.EPS OPS = {"×": "*", "x": "*", "X": "*", "*": "*", "÷": "/", "/": "/", "+": "+", "-": "-"} OPS |= {"-": "-", "−": "-"} _UNIT = re.compile(r"\s*(?:[A-Za-z가-힣㎡㎥㎏ℓ㎝㎜°˚’'\"Øø]+(?:\s*/\s*[A-Za-z가-힣㎡㎥㎏ℓ]+)?)") def num(s: str) -> float: return float(s.replace(",", "").rstrip(".") or 0) def tokens(seg: str) -> list[tuple] | None: """식 조각 → Expr 토큰 · 숫자 식이 아니면(기호 · 수 둘 잇달아 · 연산 없음) None.""" s = seg.replace("(", "(").replace(")", ")").replace("x", "x") prev = None while prev != s: # 수 없는 괄호 글 「(품할증)」 prev = s s = re.sub(r"\([^()\d]*\)", " ", s) out: list[tuple] = [] i, want, depth = 0, True, 0 while i < len(s): ch = s[i] if ch.isspace(): i += 1 continue m = re.match(NUMRE, s[i:]) if m and (i == 0 or not (s[i - 1].isalpha() and s[i - 1].isascii())): if not want: return None out.append(("수", num(m.group(0)))) i += len(m.group(0)) want = False while i < len(s): # 단위 글 「인」 「㎡」 「본/일」 m2 = _UNIT.match(s[i:]) if m2 and m2.group(0).strip() and not re.fullmatch(r"\s*[xX]", m2.group(0)): i += len(m2.group(0)) continue break continue if ch == "%": if want: return None out.append(("%",)) i += 1 continue if ch in OPS: if ch in "xX" and want: return None if ch == "/" and not re.match(r"\s*[\d(.]", s[i + 1 :]): return None if ch in "--−" and want: out.append(("연산", "-")) i += 1 continue if want: return None out.append(("연산", OPS[ch])) i += 1 want = True continue if ch == "(": if not want: return None depth += 1 out.append(("(",)) i += 1 continue if ch == ")": depth -= 1 if depth < 0 or want: return None out.append((")",)) i += 1 continue if ch.isalpha() or ch == "_": return None i += 1 # 장식(₩ 등) if depth != 0 or want or not any(t[0] in ("연산", "%") for t in out): return None return out