# Inline calculator `Features/Calculator/Model/` is a **Foundation-only** engine (no AppKit / SwiftUI imports) fronted by `CalcMemo`, a one-deep memo mirroring `AppIndex`'s. It must stay Foundation-only because the `Tests/calc-test.swift` harness compiles the real engine sources — including `CalcDateTime`. It is also **pure**: the inputs it can't compute — the FX rate table and the Mac's own currency — are passed in (see Currency below). ## Invariants - **`Model/` (including `CalcDateTime`) stays Foundation-only *and pure*** — no AppKit or SwiftUI, no clock read, no network, **no `Locale`**. `calc-test` compiles the real engine sources. Every externally-sourced input is injected: the clock via `now`/`calendar`, the FX table via `rates`, the Mac's own currency via `region`, which `RegionCurrency` reads and `CalcMemo` passes down, and the number format via `format`, which `RegionNumberFormatMonitor` reads from Language & Region. - **The engine only ever reads and writes canonical English numbers.** `CalcNumberFormat.canonical` rewrites a query before anything else sees it, and `CalcNumberFormat.localized` rewrites an answer only at presentation — the card, the actions header, a history row, the pasteboard. No grammar, tokenizer rule or formatter learns about locales, and `CalculatorHistoryStore` stores canonical text, so history re-renders in whatever format is chosen later. - **`CalcEngine.evaluate` never fetches** — it takes a finished `CurrencyRates?`, nil meaning no snapshot has landed yet. `CurrencyRateStore` owns the fetch and the cacheless `.ephemeral` session, and `CurrencyFeed` — pure, so the harness covers it — turns the payloads into that snapshot. - **The time-zone table is Foundation's, never generated and never hand-listed.** `TimeZone.knownTimeZoneIdentifiers` already carries the whole IANA database, so `CalcTimeZone` builds its city index from that on first use rather than shipping a copy that would rot every time IANA moves a zone. `TimeZone.abbreviationDictionary` stays deliberately unused — it holds 51 entries and its `BDT` is the Bangladeshi taka. The home zone is read off the **injected calendar**, never `TimeZone.current`, which is what keeps the path pure and the harness deterministic. `localizedName` needs a `Locale`, so a badge is the identifier's own city component instead. Countries are the one exception: Foundation carries no country for a zone, so `CountryZoneData.generated.swift` comes from `node Scripts/gen-countries.js` and is never hand-edited. - **A workday is 8 hours, and nothing consults a calendar.** Weekends and public holidays would make the same query answer differently on two Macs, and the only supported source for them is EventKit, whose Full Calendar Access grant a calculator must never provoke mid-keystroke. `workdays` is therefore an ordinary time unit, and `calendarEnabled` stays the Calendar feature's own consent. - **`CurrencyData.generated.swift` is emitted by `node Scripts/gen-currencies.js`** and never hand-edited. Four currency tables are hand-maintained, all in `CalcCurrency`: `contested`, the nouns several currencies share (`dollars`, `pounds`); `isoNames`, the standard's own names where CLDR substitutes a different one (ISO 4217 calls CNY "Yuan Renminbi"); `signCodes`, the codes daily use spells from CLDR's sign instead (`NT$` makes TWD `ntd`); and `crypto`, which no standards body names. Do not add slang or synonyms to any of them — no source of truth, so they rot. ## Evaluation pipeline `CalcEngine.evaluate` runs: Single ASCII words return immediately: a bare app name, constant or date keyword never earns a card. 1. Natural-language date/time (`CalcDateTime`, e.g. `hrs till 9am`, `days till 9april`, `today + 3 weeks`) 2. **Time zones** (`CalcTimeZone`, e.g. `time in Tokyo`, `5pm ldn in sf`) — before tokenizing, because a zone phrase is words rather than calculator input 3. Tokenize, then preserve the complete prefix of a trailing binary operator 4. Base conversion 5. **Typed quantity arithmetic** (`10kg + 500g`, `$10 + €5`, `5m * 4m`, `100km / 2h to km/h`, `(1hr + 30min) to timespan`) 6. Explicit unit conversion (`10km to mi`, `m to ft`, `day s`) 7. Currency conversion (`1 euro to dollars`, `€20 to GBP`, `1 btc to eur`) 8. Bare-unit auto-conversion (`1m` → feet + inches, `1hr` → 60 min) 9. Natural-language percent, ratio and list forms (`CalcPercent`) `CalcExpressionParser` is the sole precedence-climbing evaluator, returning `CalcValue` for numbers, measurements, currencies and booleans. `CalcQuantity` turns those values into cards and applies display policy. Scalar operands in conversions and percent phrases use the same parser's `scalar` projection; there is no second arithmetic parser or fallback evaluation of a completed scalar query. `CalcTokenizer` scans Unicode scalars, retaining canonical-equivalent accented currency names. `CalcOperator` owns operator identity, binding power and spelling; `CalcMath` owns the function and constant catalog. Spoken roots use the typed parser too: `square root of 25m2` is `5 m`, and `cube root of -8m3` is `-2 m`. Dimensionless results can feed base conversion too: `2m / 2m to hex` is `0x1`. `CalcNumberBase` owns radix names and prefixes for both tokens and results. A conversion target is checked before evaluating its source, so an ordinary unit conversion never attempts radix arithmetic. Overflowing literals and intermediate arithmetic are rejected before comparisons can hide the overflow. When a trailing operator keeps a conversion visible, its input is reconstructed at full Double precision; display rounding never feeds back into evaluation. `UnitDef` is an immutable, Sendable reference shared by its aliases and parsed values. The catalog stores 150 base definitions as compact text records rather than repeated construction code, then adds SI and transfer-rate prefixes once on first use, for 679 aliases. `CalcUnitCatalog` owns this data; `CalcUnits` owns conversion policy. Typed arithmetic precedes simple conversion so `1 / 20ms to hz` divides by a duration, not a scalar subsequently labeled milliseconds. Simple conversions still own their source badges. Date/time takes an injected `now` / `calendar`. `CalcMemo` supplies the live clock and calendar at the UI boundary; the model and harness perform no ambient clock reads. Date arithmetic requires a moment signal, so ordinary numeric expressions skip calendar parsing. Numeric date components share one parser, while the separator still chooses ISO, month-first or day-first interpretation. `CalcDateTime` recognizes these grammars: - **A** — duration until a moment: `hrs till 9am`, `days till 9april` - **B** — duration since a past moment: `days since 9jul`, `hrs since noon` - **C** — a moment ± durations: `today + 3 weeks`, `now + 90 min`, `17.2.26 + 100 weekdays - 4 + 2` - **D** — difference between two moments: `jul 4 - today` - **E** — a leading duration: `5 weekdays from now`, `3 days from today`, `2 weeks ago` - **F** — a weekday inside a future week: `monday in 3 weeks`, `friday in 2 weeks` - **G** — a named moment, once qualified: `tomorrow at 9am`, `next monday`, `last friday` **An answered moment badges its weekday.** Grammars C and E resolve to a date, and the day of the week is the thing a date does not say out loud — so `5 weekdays from now` reads `4 September` under a `Friday` pill rather than repeating the weekday inside the date and badging it `Result`. `answerString` is `momentString` without the leading `EEEE` for exactly that reason; the source badge keeps its own weekday, since nothing else on the card carries it. A bare, recurring date or time resolves by _bias_: `till` takes the upcoming occurrence, `since` the most recent past one; an absolute date ignores the bias. Grammar D needs an unambiguous date/time signal: a letter, a clock, an ISO or dotted date, or an explicit time-unit target. Fraction-only operands (`5/2 - 1/2`) remain arithmetic. Two-digit years expand the way date pickers do — 00–68 to the 2000s, 69–99 to the 1900s. A **dotted** date is day-first (`19.2.27` is 19 February 2027), matching the convention that writes it, where the slashed form stays month-first. It needs three parts and a two- or four-digit year, which is what separates a date from a decimal and from a version number: `1.5 + 3` is 4.5 and `1.2.3 + 1` earns no card. The version-number overlap is only **partly** closed, and irreducibly so: `1.2.24` is both a plausible semver and a valid 1 February 2024, with nothing in the text to tell them apart. A one-digit or three-digit tail is rejected (`1.2.3`, `10.15.7`), which covers the common shapes, but a two-digit patch reads as a date. Requiring a four-digit year would close it and cost `19.2.27`, which is the more common thing to type. The same convention writes an **ordinal dot** after the day, so `28. aug + 3` reads as 28 August. Only a trailing dot is dropped, which is why `28.5 aug` stays silent rather than becoming a date. Grammar G needs the qualifier. A lone `tomorrow` is an app search, so `at