Calculator Engine

July 17, 2026 · View on GitHub

The calculator engine (app/calculator/) is a complete natural-language arithmetic evaluator built in Go, split across six files for maintainability.

FileResponsibility
engine.goCore Engine struct, PEMDAS recursive descent parser, lexer, naturalize pipeline, EvaluateLine/EvaluateAll, history tracking, helpers
units.goUnit conversion database (unitDB), convertUnit, RegisterUnit
functions.goBuilt-in math function dispatch (sin, cos, sqrt, etc.)
variables.goGetVariables, SetVariable, ClearVariables
steps.goStep and EvalDetail types, GetSteps method (read-only evaluation with intermediate steps)
graph.goPoint, GraphResult types, EvaluateGraph method (function plotting via sampled points)

Overview

Input → Naturalize → Pattern Matching → Recursive Descent Parser → Result

Natural Language Pipeline (naturalize)

Before arithmetic parsing, the input is preprocessed through a multi-step pipeline. A normalize() pass runs at the very top to handle Unicode normalisation and noise words before any pattern matching.

| Step | What it does | Example | |---|---|---|---|---|---| | 0. normalize | Unicode normalisation, noise word stripping, whitespace normalisation | 5 × 35 * 3, 5 ÷ 25 / 2, exactly 2+22+2 | | 1. Prefix stripping | Removes query prefixes, conversation fillers, pronouns (loop) | what is 2+22+2, can you calculate 5+35+3, maybe 2+22+2 | | 2. Trailing fluff | Removes please, thanks, for me, if possible, yrs old | 2+2 for me2+2, 25 yrs old25 | | 2b. Trailing punctuation | Strips ? and . early (before word-to-number) | one plus one?one plus one | | 3. Currency conversion | Strips symbols, handles cross-rates, code prefixes | $5k in EUR5000 usd in EUR | | 3b. Compact time | Expands h/m notation | 2h30m(2 + 30/60.0) | | 4. Fraction words | Converts fraction words to decimals | one half0.5 | | 5. Word-to-number | Converts word numbers to digits | twenty five25 | | 6. Ordinal suffixes | Strips st, nd, rd, th | 1st1 | | 7. "how many times" | Division via how-many-times (before SI) | how many times does 5 go into 2020 / 5 | | 8. SI notation | Expands k/M/B/T suffixes | 5k5000 | | 8b. Mixed numbers | Whole + fraction → addition | 2 1/22 + (1/2) | | 9. Possessive plurals | Expands plural number words | 3 tens(3 * 10) | | 9b. "half N" pattern | half N0.5 * N | half 10000000.5 * 1000000 | | 10. Context references | Replaces that, then, result, prev, my age | then * 242 * 2, prev + 542 + 5 | | 11. Multiplicative prefixes | Handles double, twice, half of | double 52 * 5 | | 12. Power words | Converts squared, cubed | 5 squared5 ^ 2 | | 12b–12e. Comparison/ratio | times more/less, % more/less, added to | 3 times more than 55 + 5 * 3 | | 13. Complex phrases | Comparison, division, multiplication, half as much as, how many X in Y | 10 increased by 510 + 5, half as much as 105 | | 13b. Shape patterns | Rectangle/circle area, cube/cylinder/sphere volume, by/x multiply | area of rectangle 10 by 20200 | | 13c. Purchase math | N items at $P each(N * P), full expression with discount + tax | 5 items at \$20 each100, 5 items at \$20 each with a 15% discount and 8% sales tax added on top91.8 | | 14. Natural functions | square root of, cube root of, absolute value of, sine of, log of | square root of 144sqrt(144), sine of 0sin(0) | | 14b. Natural trig/log | sine of X, cosine of X, log of X, ln of X | sine of 0sin(0), log of 100ln(100) | | 14c. "per cent" / "pct" | Converts per cent / pctpercent | 10 per cent of 20010 percent of 200, 10 pct of 20010 percent of 200 | | 14d. Tip/discount | X plus Y% tip, X minus Y% discount | 40 plus 15% tip(40 + 40 * 15 / 100) | | 15. Word operators | Replaces English operators with symbols (expanded) | plus+, combined with+, subtract-, lots of*, split between/, exponent^, to the N^ N | | 16. "X from Y" | Converts subtraction-by-from (after word operators) | 10 from 100100 - 10 | | 17. Percentage relations | is what % of, as a % of, % of what is, what % of Y is X | 10 is what % of 50((10 / 50) * 100) | | 18. Advanced math | log base, choose | log base 2 of 8(ln(8) / ln(2)) | | 18b. Trig shorthand | sin 45sin(45), sin thetasin(theta) | sin 450.8509 | | 18c. "sin of X" | sin of Xsin(X), cos of Xcos(X) | sin of 0sin(0) | | 18d. Verb-square/cube | square XX ^ 2, cube XX ^ 3 | square 525 | | 19. Percent word | Converts percent% | 10 percent of 20010% of 200 | | 20. Comma cleanup | Removes commas from numbers | 1,0001000 | | 21. Collapse spaces | Normalises multiple spaces to one | 5 + 35 + 3 |

Query Prefixes

Stripped prefixes include:

  • Standard: what is, what's, what are, calculate, compute, find, solve, the value of, evaluate, result of, how much is, how many is
  • Conversational: can you, could you, would you, will you, do you, does
  • Request: i need to, i want, i would like, i'd like, we need, we want
  • Filler: i think, i guess, maybe, perhaps, probably, so, well, ok, okay, alright, like
  • Action: let's, lets, determine, work out, figure out, give me
  • Greeting: hi, hello, hey (optional there)
  • Pronouns: i, we, you, your (stripped when leading; my guarded against age refs)

Word-to-Number

Supports numbers from zero through billions:

  • Simple: one1, twenty20
  • Compound: twenty five25, two hundred thirty230
  • Large: two million three hundred thousand2300000
  • Hyphenated: twenty-one21
  • "And" is ignored: one hundred and five105

Collective nouns are mapped in wordNumMap and handled by the word-to-number step:

a couple  →  2
a dozen   →  12
a score   →  20

Phase 0 — Input Normalisation

New normalize() pass at the top of the pipeline handles character-level issues before any pattern matching:

PatternTransformationExample
Unicode quotesSmart/curly quotes → ASCII\u2018 \u2019', \u201c \u201d"
Unicode dashesEn/em dash, minus → -5 \u2013 35 - 3
Unicode multiply/divide×/·*, ÷/5 × 35 * 3, 10 ÷ 210 / 2
Unicode spacesNBSP, thin spaces → regular space
Multiple punctuation??, !!!, ... → stripped
Noise wordsexactly, roughly, about, approximately, say → stripped

Phase 1 — Prefix / Suffix / Notation Patterns

These patterns clean up formatting and expand common shorthand before arithmetic parsing:

PatternStepExample
Unicode normalisation05 × 35 * 3, 5 ÷ 25 / 2
Expanded prefixes1can you find 5+35+3, maybe 2+22+2, i'd like 2*32*3
Expanded trailing fluff22+2 for me2+2, 25 yrs old25
Currency conversion3$1010, $5k in EUR5000 usd in EUR, BTC5k in USD5k btc in USD
Compact time notation3b2h30m(2 + 30/60.0), 2h2
Mixed numbers8b2 1/22 + (1/2), 3 3/43 + (3/4)
Ordinal suffix stripping61st1, 2nd2, 3rd3, 4th4
SI notation expansion85k5000, 3M3000000, 2B2000000000
Possessive plurals93 tens(3 * 10), 2 dozens(2 * 12), 5 scores(5 * 20)
"X from Y" subtraction1610 from 100100 - 10

SI notation uses case‑sensitive matching: k/K = thousand, M = million, B = billion, T = trillion. Lowercase m is NOT treated as SI (it would conflict with meters in unit conversion). Unlike the old parenthesized form (5 * 1000), SI now expands to the computed numeric value (5000) so that subsequent currency conversion can match the complete number.

The "how many times" pattern runs before SI expansion so that 5k, 3M, etc. are captured as a single token and expanded after substitution. The number capture groups accept an optional SI suffix ([kKMBT]).

Phase 2 — Percentage Relationship Phrases

These patterns answer relational percentage questions without needing to set up the formula manually:

PhraseTransformationExample
X is what percent of Y (or %)(X / Y) * 10010 is what percent of 5020
X as a percentage of Y(X / Y) * 10010 as a percentage of 5020
X percent of what is Y (or %)(Y / X) * 10050 percent of what is 2550
what percent of Y is X (or %)(X / Y) * 100what percent of 50 is 1020
X out of Y as a percentage(X / Y) * 10010 out of 50 as a percentage20
X pct / X p.c. / X pcX percent10 pct of 20020
X plus Y% tip/taxX + X * Y / 10040 plus 15% tip46
X minus/after Y% discountX - X * Y / 100200 minus 10% discount180

Phase 3 — Advanced Math / Natural Function Phrases

These patterns provide natural‑language access to advanced mathematical operations:

PhraseTransformationExample
X! (postfix !)factorial(X)5!120
log base X of Yln(Y) / ln(X)log base 2 of 83
X choose YnCr(X, Y)5 choose 310
how many times does X go into YY / Xhow many times does 5 go into 204
the square root of Xsqrt(X)the square root of 14412
the cube root of Xcbrt(X)the cube root of 273
the absolute value of Xabs(X)the absolute value of -55
sine of X / cosine of X / tangent of Xsin(X) / cos(X) / tan(X)sine of 00
log of X / ln of X / natural log of Xln(X)log of 1004.605...
square X / cube X (verb)X ^ 2 / X ^ 3square 525
half as much as XX * 0.5half as much as 105
quarter as much as XX * 0.25quarter as much as 205
how many X in YY / Xhow many 5 in 204

The factorial ! operator is parsed at the lexer level as a tokBang token and applied in the parser's parseAtom — it binds tighter than all binary operators. The nCr function is registered in the built‑in function table. log base and choose are substituted before word operators so they don't conflict with other patterns. The "how many times" pattern runs before SI expansion (step 7) to handle SI suffixed numbers like 5k.

Date Math (before and after naturalize)

Date math runs before naturalize (to preserve date keywords like today, now, next) and again after naturalize (handling cleaned expressions like what is next weeknext week).

An additional embedded extraction pass searches for date patterns anywhere in the post-naturalize string, so expressions like asjeh fjfugh today + 3 months etc still resolve to the correct date.

Supported patterns:

  • today / now — returns current date/time
  • next week / last month / next year — relative dates
  • today + 14 days / today - 3 months / now + 1 year
  • March 1 + 30 days / Dec 25 2026 + 7 days
  • N days from now / N months ago
  • Embedded in text: I completing a book at today + 14 days some others story → date

Context References

  • of that, of it, of the result, of the answer, of the value — replaces with previous line's result
  • then X, result X, answer X — prepends previous result: then + 542 + 5
  • previous X, last X, prior X, prev X — same as then
  • my age / my current age — returns previous line's result (useful after a birth-year query)
  • Entire line that, it, my age, my current age, previous, last, prior, prev — returns previous result

Word Operators

EnglishSymbol
plus, and, combined with, together with, along with+
minus, subtracted from, less, reduced by, take away, subtract, without, fewer-
times, multiplied by, multiply, groups of, lots of, sets of*
divided by, split into/between/among, per, divide, shared between/among/
to the power of, raised to, raised to the power of, exponent, to the N^
mod, modulo%

Note: The per/ conversion is aggressive. For unit conversion (e.g., 10 km per hour), use the X in Y syntax instead.

Pattern Matching (pre-parser)

Before falling through to the general parser, the engine checks for three special patterns:

Unit Conversion: X fromUnit in toUnit

10 inches in cm     → 25.4 cm
100 USD in EUR      → 92.59 EUR
100 c to f          → 212 °F

Percentage of: X% of Y

10% of 200          → 20
15% on 80           → 12

Percentage add/subtract: X ± Y%

100 + 15%           → 115
200 - 10%           → 180

Arithmetic Parser

Recursive descent parser implementing PEMDAS:

parseAddSub  → parseMulDiv {(+|-) parseMulDiv}
parseMulDiv  → parsePow {(*|/|%) parsePow}
parsePow     → parseUnary {^ parseUnary}
parseUnary   → {-|+} parseAtom
parseAtom    → number | (expr) | ident[(args...)]

Lexer (tokenizer)

Tokens: +, -, *, /, ^, %, (, ), ,, !, numbers (tokNum), identifiers (tokIdent), EOF.

The postfix factorial operator ! (tokBang) binds tighter than all binary operators and is handled during atom parsing.

Built-in Functions

FunctionDescription
sin(x)Sine (radians)
cos(x)Cosine (radians)
tan(x)Tangent (radians)
asin(x)Arc sine
acos(x)Arc cosine
atan(x)Arc tangent
atan2(y, x)Arc tangent of y/x
sinh(x)Hyperbolic sine
cosh(x)Hyperbolic cosine
tanh(x)Hyperbolic tangent
sqrt(x)Square root
cbrt(x)Cube root
abs(x)Absolute value
round(x)Nearest integer
floor(x)Round down
ceil(x)Round up
trunc(x)Truncate decimals
fract(x)Fractional part
log(x) / ln(x)Natural logarithm
log10(x)Base-10 logarithm
log2(x)Base-2 logarithm
exp(x)e^x
pow(x, y)x^y
fact(x) / factorial(x)Factorial
nCr(n, r) / choose(n, r)Combinations (n choose r)
nPr(n, r)Permutations (n pick r)
gcd(a, b)Greatest common divisor
lcm(a, b)Least common multiple
rand() / random()Random [0, 1)
min(a, b, ...)Minimum
max(a, b, ...)Maximum
sum(a, b, ...)Sum
avg(a, b, ...)Average
median(a, b, ...)Median value
mode(a, b, ...)Mode (most frequent)
stdev(a, b, ...) / stddev(a, b, ...)Standard deviation
variance(a, b, ...) / var(a, b, ...)Population variance
range(a, b, ...)Range (max - min)
sign(x) / sgn(x)Sign (-1, 0, 1)
deg(x)Radians to degrees
rad(x)Degrees to radians
isprime(n) / is_prime(n)Primality test (returns 1 or 0)
hypot(a, b) / pythag(a, b)Hypotenuse: sqrt(a² + b²)

Constants

  • pi / π — π (3.14159...)
  • e — Euler's number (2.71828...)
  • speed_of_light / lightspeed / c_light — 299,792,458 m/s
  • gravity / g_force — 9.80665 m/s²
  • planck / planck_constant — 6.62607015×10⁻³⁴ J·s
  • boltzmann / boltzmann_constant — 1.380649×10⁻²³ J/K
  • gas_constant / gasconstant — 8.314462618 J/(mol·K)
  • avogadro / avogadro_constant — 6.02214076×10²³ mol⁻¹
  • stefan_boltzmann / stefanboltzmann — 5.670367×10⁻⁸ W/(m²·K⁴)
  • electron_mass / me — 9.10938356×10⁻³¹ kg
  • proton_mass / mp — 1.67262192369×10⁻²⁷ kg
  • neutron_mass / mn — 1.67492749804×10⁻²⁷ kg
  • electron_charge / e_charge — 1.602176634×10⁻¹⁹ C
  • bohr_radius / bohrradius — 5.29177210903×10⁻¹¹ m
  • rydberg / rydberg_constant — 10,973,731.568160 m⁻¹

Variables

  • Assignment: x = 42
  • Reference: x * 284
  • Variables persist across lines until cleared (⌘K)
  • Case-insensitive names

History

Each successful evaluation is recorded in the engine's history as a HistoryEntry{Input, Output}. Accessible via GetHistory() and ClearHistory() on the engine and service layer. The frontend provides Cmd+↑ / Cmd+↓ navigation through computed entries.

Unit Database

Built-in conversion for:

  • Length: meter, kilometer, centimeter, millimeter, inch, foot, yard, mile
  • Mass: gram, kilogram, pound, ounce
  • Volume: liter, milliliter, gallon, quart, cup
  • Temperature: Celsius, Fahrenheit
  • Time: second, minute, hour, day
  • Currency: USD, EUR, GBP, JPY, CNY, INR, CAD, AUD, CHF, KRW, RUB, BRL, MXN, ZAR, NZD, SEK, NOK, PLN, HKD, SGD, THB, ILS, VND, PHP, UAH, KZT, PYG, GHS, TRY, AZN, GEL, BDT, PKR, LKR, NPR, MYR, IDR, TWD, SAR, AED, KWD, EGP, NGN, COP, CLP, ARS, PEN, MAD, BTC, XAU, XAG

Step-by-Step Evaluation

The GetSteps(input) method evaluates an expression in read-only mode — it runs the full parser pipeline but does not modify engine state (no history recording, no variable mutations). Each parser level records Step entries showing the operation performed and the intermediate result:

Parser LevelExample Step
Naturalizenaturalize("what is 5 + 3 * 2")"5 + 3 * 2"
Add/Subtract5 + 611
Multiply/Divide3 * 26
Power2 ^ 38
Negate-5-5
Modulo17 % 52
Factorial5!120
Functionsqrt(144)12
Constantpi3.1416
Variablex42

Steps are returned as an EvalDetail struct containing the final result string and an ordered slice of Step objects.

Function Graphing

The EvaluateGraph(input) method detects graphing commands and evaluates an expression across a range of x values:

plot x^2                 →  200 points from -10 to 10
graph sin(x)             →  200 points from -10 to 10
y = 2x + 1               →  200 points from -10 to 10
graph x^2 from -5 to 5   →  200 points from -5 to 5

The implementation:

  1. Strips the leading plot/graph/y = prefix
  2. Extracts optional from N to N range specifier (defaults to -10 to 10)
  3. Saves the current x variable, evaluates the expression for 200 evenly spaced x values, and restores the original x
  4. Returns a GraphResult with the sampled Point slice, cleaned expression, and range bounds

Error Handling

All evaluation errors return an empty string — no error messages are shown to the user. Division and modulo by zero are silently dropped.

Plugin System

The calculator engine integrates a plugin system that extends the function table at runtime.

Plugin Manager (app/plugin/)

FileResponsibility
types.goManifest, Plugin, FunctionDef, ThemeDef, VariableDef types
loader.goManager struct — scans plugin directory, loads manifests, registers functions
builtins.go20+ pre-defined builtin functions available to plugin expressions
expr.goSafe expression evaluator for plugin function definitions
state.goPlugin enabled/disabled state persistence

How Plugins Extend the Engine

  1. On startup, main.go creates a plugin.Manager and calls Scan() to discover plugins in ~/.config/neostore/linesolv/plugins/
  2. Each plugin's plugin.json manifest declares functions, themes, and variables
  3. Expression functions are compiled by the expr.go evaluator into callable PluginFunction closures
  4. Builtin functions map to pre-defined operations in builtins.go
  5. The engine's callBuiltinOrPlugin() function checks plugin functions after builtins
  6. Plugin variables are merged into the engine's variable map
  7. Plugin themes are injected into the frontend theme picker

Expression Evaluator (expr.go)

Plugin expressions use a safe subset of math:

  • Operators: +, -, *, /, ^, %
  • Functions: min, max, abs, sin, cos, tan, asin, acos, atan, sqrt, cbrt, log, ln, log2, exp, pow, floor, ceil, round, sign, mod, atan2
  • Constants: pi(), e(), tau(), phi()
  • Variables: a, b, c... mapped to function arguments

No arbitrary code execution — expressions are parsed and evaluated in a sandboxed environment.

Plugin State Persistence

Plugin enabled/disabled state is persisted in a plugin_state.json file in the data directory. State survives app restarts.