CharlesCNorton commited on
Commit
7b8a56c
·
1 Parent(s): ce521af

Add implicit multiplication to expression parser

Browse files

- Tokenize infix expressions with identifiers and scientific notation\n- Insert implicit '*' between adjacent values (e.g., 2pi, 3(4+5))\n- Keep function calls intact when followed by parentheses

Files changed (1) hide show
  1. calculator.py +71 -0
calculator.py CHANGED
@@ -11,6 +11,7 @@ import argparse
11
  import ast
12
  import json
13
  import math
 
14
  import struct
15
  import time
16
  from dataclasses import dataclass, field
@@ -91,12 +92,82 @@ def resolve_alias_target(name: str, gates: set) -> Optional[str]:
91
  return None
92
 
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def normalize_expr(expr: str) -> str:
95
  """Normalize user-facing calculator syntax to Python AST syntax."""
96
  expr = expr.replace("\u03c0", "pi")
97
  expr = expr.replace("\u00d7", "*").replace("\u00f7", "/").replace("\u2212", "-")
98
  if "^" in expr:
99
  expr = expr.replace("^", "**")
 
100
  return expr
101
 
102
 
 
11
  import ast
12
  import json
13
  import math
14
+ import re
15
  import struct
16
  import time
17
  from dataclasses import dataclass, field
 
92
  return None
93
 
94
 
95
+ _NUM_RE = re.compile(r"(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?")
96
+ _IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
97
+ _FUNC_NAMES = {
98
+ "sqrt", "rsqrt", "exp", "ln", "log", "log2", "log10",
99
+ "deg2rad", "rad2deg",
100
+ "isnan", "is_nan", "isinf", "is_inf", "isfinite", "is_finite",
101
+ "iszero", "is_zero", "issubnormal", "is_subnormal",
102
+ "isnormal", "is_normal", "isneg", "is_negative", "signbit",
103
+ "sin", "cos", "tan", "tanh",
104
+ "asin", "acos", "atan", "sinh", "cosh",
105
+ "floor", "ceil", "round", "abs", "neg",
106
+ }
107
+
108
+
109
+ def _tokenize_expr(expr: str) -> List[str]:
110
+ tokens: List[str] = []
111
+ i = 0
112
+ while i < len(expr):
113
+ ch = expr[i]
114
+ if ch.isspace():
115
+ i += 1
116
+ continue
117
+ if expr.startswith("**", i):
118
+ tokens.append("**")
119
+ i += 2
120
+ continue
121
+ if ch in "+-*/(),":
122
+ tokens.append(ch)
123
+ i += 1
124
+ continue
125
+ num_match = _NUM_RE.match(expr, i)
126
+ if num_match:
127
+ tokens.append(num_match.group(0))
128
+ i = num_match.end()
129
+ continue
130
+ ident_match = _IDENT_RE.match(expr, i)
131
+ if ident_match:
132
+ tokens.append(ident_match.group(0))
133
+ i = ident_match.end()
134
+ continue
135
+ raise RuntimeError(f"bad token near: {expr[i:]}")
136
+ return tokens
137
+
138
+
139
+ def _needs_implicit_mul(left: str, right: str) -> bool:
140
+ if left in {"+", "-", "*", "/", "**", ",", "("}:
141
+ return False
142
+ if right in {"+", "-", "*", "/", "**", ",", ")"}:
143
+ return False
144
+ if left in _FUNC_NAMES and right == "(":
145
+ return False
146
+ return True
147
+
148
+
149
+ def _insert_implicit_mul(expr: str) -> str:
150
+ tokens = _tokenize_expr(expr)
151
+ if not tokens:
152
+ return expr
153
+ out: List[str] = []
154
+ for idx, tok in enumerate(tokens):
155
+ out.append(tok)
156
+ if idx + 1 >= len(tokens):
157
+ continue
158
+ nxt = tokens[idx + 1]
159
+ if _needs_implicit_mul(tok, nxt):
160
+ out.append("*")
161
+ return "".join(out)
162
+
163
+
164
  def normalize_expr(expr: str) -> str:
165
  """Normalize user-facing calculator syntax to Python AST syntax."""
166
  expr = expr.replace("\u03c0", "pi")
167
  expr = expr.replace("\u00d7", "*").replace("\u00f7", "/").replace("\u2212", "-")
168
  if "^" in expr:
169
  expr = expr.replace("^", "**")
170
+ expr = _insert_implicit_mul(expr)
171
  return expr
172
 
173