diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0133823..0dbf0ce 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -29,10 +29,12 @@ src/ updater.ts checks tauri-plugin-updater, installs + relaunches src-tauri/ src/main.rs tray, hide-to-tray, storage commands, rates fetching - (open.er-api.com + CoinGecko) with cache, backups, - data-folder migration, file drops, plugin registration - (autostart, global-shortcut, single-instance, opener, - dialog, updater, process) + (open.er-api.com + CoinGecko + Yahoo Finance) with + cache, historical rates (frankfurter.app, permanent + per-date cache), backups, data-folder migration, + file drops, plugin registration (autostart, + global-shortcut, single-instance, opener, dialog, + updater, process) ``` ## How a line becomes a result @@ -89,9 +91,25 @@ snapshot price) plus the mirror list in `src-tauri/src/main.rs`. because an "attosecond" symbol would shadow the word `as`. - `and`/`or` are dual-purpose: `and` adds (`5 and 3` = 8), `or` is bitwise. `of/off/on` only act on a percent left-hand side; otherwise they're prose. + The same `on` token doubles as the historical-rate date separator (`in EUR on + 2024-01-01`) — `parseSeq` checks for `pctop(on)` + `datelit` right after a + currency conv target and consumes both when present. - Currency rates are "units per USD"; a currency Unit's ratio is computed at - tokenization time from the current rate map, so re-setting rates re-prices - everything on the next evaluation. + evaluation time from the current rate map, so re-setting rates re-prices + everything on the next evaluation. Historical rates live in a separate + `historicalRates: Map>` on `SumEngine` and are + passed through `EvalCtx` to `evalConv`. +- Live market data (stocks, commodities via Yahoo Finance) is merged into the + same rate map as exchange rates via `applyAllRates()` in `main.ts` to avoid + one `setRates()` call overwriting the other. +- Goal seek (`? * 1.2 = 1000`) tokenizes `?` as `{ t: "unknown" }`, parsed + into `{ k: "unknown" }` nodes. `parseLine` detects the presence of `unknown` + + `assign` and builds a `{ k: "goalseek", lhs, rhs }` node. The solver + tries a linear probe (f(0), f(1) → exact root for affine expressions) and + falls back to bisection for nonlinear ones. +- `ChartValue` (`kind: "chart"`) is returned by `evalAgg("chart", …)` and + bypasses `formatValue` — the results overlay renders an SVG sparkline + instead of a text string. - The registry is rebuilt never; extensions mutate it (add phrases/functions) at startup. @@ -131,9 +149,24 @@ snapshot price) plus the mirror list in `src-tauri/src/main.rs`. (write-only — there is no durable copy in the repo). Losing that key means already-installed copies can never trust a future signed release again. +## Storage (cache files) + +All files below live in `%APPDATA%/app.summarum.calc` (or the user-chosen folder): + +| File | Contents | TTL | +|------|----------|-----| +| `settings.json` | app settings | persisted | +| `documents.json` | all sheets | persisted (400 ms debounce) | +| `rates.json` | live exchange rates | 1 hour | +| `market.json` | stock/commodity prices | 15 minutes | +| `rates-YYYY-MM-DD.json` | historical ECB rates for one date | permanent | +| `backups/documents-YYYY-MM-DD.json` | daily snapshots | 14 days | +| `backups/deleted/*.numi` | soft-deleted sheets | configurable | + ## Tests -`npm test` runs ~96 vitest cases over the engine (`src/engine/*.test.ts`): +`npm test` runs 113 vitest cases over the engine (`src/engine/*.test.ts`): every expression class, both languages, deterministic injected rates, -plus a regression suite covering all known-fixed bugs. -UI is exercised manually; the engine is where the complexity lives. +goal seek, historical rates (injected), plus a regression suite covering +all known-fixed bugs. UI is exercised manually; the engine is where the +complexity lives. diff --git a/ARCHITECTURE.ru.md b/ARCHITECTURE.ru.md index 37f57e3..ca3c993 100644 --- a/ARCHITECTURE.ru.md +++ b/ARCHITECTURE.ru.md @@ -30,10 +30,12 @@ src/ updater.ts проверка tauri-plugin-updater, установка + перезапуск src-tauri/ src/main.rs трей, скрытие в трей, команды хранилища, загрузка курсов - (open.er-api.com + CoinGecko) с кэшем, резервные копии, - миграция папки данных, открытие файлов, регистрация - плагинов (autostart, global-shortcut, single-instance, - opener, dialog, updater, process) + (open.er-api.com + CoinGecko + Yahoo Finance) с кэшем, + исторические курсы (frankfurter.app, постоянный кэш + на каждую дату), резервные копии, миграция папки данных, + открытие файлов, регистрация плагинов (autostart, + global-shortcut, single-instance, opener, dialog, + updater, process) ``` ## Как строка превращается в результат @@ -89,9 +91,23 @@ src-tauri/ только ms/µs/ns/ps, потому что символ «аттосекунды» затенил бы слово `as`. - `и`/`или` двойного назначения: `и` складывает (`5 и 3` = 8), `или` — побитовое. `от/без/на` работают только с процентом в левой части; иначе это просто текст. + Тот же токен `на` используется как разделитель даты для исторических курсов + (`в EUR на 2024-01-01`) — `parseSeq` проверяет `pctop(on)` + `datelit` + сразу после цели конвертации валюты и поглощает оба при совпадении. - Курсы валют — «единиц за 1 USD»; соотношение единицы валюты вычисляется при - токенизации из текущей карты курсов, поэтому обновление курсов пересчитывает - всё при следующей оценке. + вычислении из текущей карты курсов, поэтому обновление курсов пересчитывает + всё при следующей оценке. Исторические курсы живут отдельно в + `historicalRates: Map>` на `SumEngine` и передаются + через `EvalCtx` в `evalConv`. +- Живые рыночные данные (акции, сырьё через Yahoo Finance) объединяются в ту же + карту курсов через `applyAllRates()` в `main.ts`, чтобы один вызов `setRates()` + не перезаписывал другой. +- Поиск неизвестного (`? * 1.2 = 1000`): `?` токенизируется как `{ t: "unknown" }`, + разбирается в узел `{ k: "unknown" }`. `parseLine` обнаруживает `unknown` + `assign` + и строит `{ k: "goalseek", lhs, rhs }`. Решатель пробует линейный метод (f(0), + f(1) → точный корень для аффинных выражений) и переходит к бисекции для нелинейных. +- `ChartValue` (`kind: "chart"`) возвращается из `evalAgg("chart", …)` и + минует `formatValue` — оверлей результатов рисует SVG-спарклайн вместо текста. - Реестр не пересобирается никогда; расширения мутируют его (добавляют фразы/функции) при запуске. @@ -132,9 +148,24 @@ src-tauri/ означает, что уже установленные копии никогда больше не смогут доверять будущему подписанному релизу. +## Хранилище (кэш-файлы) + +Все файлы ниже живут в `%APPDATA%/app.summarum.calc` (или в папке данных пользователя): + +| Файл | Содержимое | TTL | +|------|-----------|-----| +| `settings.json` | настройки | постоянно | +| `documents.json` | все листы | постоянно (дебаунс 400 мс) | +| `rates.json` | живые курсы валют | 1 час | +| `market.json` | котировки акций/сырья | 15 минут | +| `rates-YYYY-MM-DD.json` | исторические курсы ЕЦБ за одну дату | навсегда | +| `backups/documents-YYYY-MM-DD.json` | ежедневные снимки | 14 дней | +| `backups/deleted/*.numi` | мягко удалённые листы | настраивается | + ## Тесты -`npm test` запускает ~96 vitest-кейсов по движку (`src/engine/*.test.ts`): +`npm test` запускает 113 vitest-кейсов по движку (`src/engine/*.test.ts`): все классы выражений, оба языка, детерминированные инжектированные курсы, -плюс набор регрессий по всем известным исправленным багам. -UI проверяется вручную; движок — там где живёт вся сложность. +поиск неизвестного, исторические курсы (инжектированные), плюс набор регрессий +по всем известным исправленным багам. UI проверяется вручную; движок — там +где живёт вся сложность. diff --git a/README.md b/README.md index 36d0027..76dbdfe 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,11 @@ total → $1,066.97 $2000 in rubles → 144,060 ₽ 20% off 1500 → 1,200 today + 6 weeks → 22 July 2026 -time in Tokyo → 14:25 +1000 USD in EUR on 2024-01-01 → € 917.30 +? * 1.2 = 1000 → 833.33 +AAPL in USD → $213.55 +days until 2027-01-01 → 183 day +chart → ▂▄▇█▆ ``` Works in **English and Russian** at the same time: `5 метров в см`, `20$ в евро`, @@ -35,13 +39,14 @@ Requirements: - **[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)** — already part of Windows 11 and most updated Windows 10 systems; if it's missing, the installer downloads it automatically. -- Internet connection is only needed for live currency rates — everything +- Internet connection is only needed for live rates and market data — everything else works offline (rates fall back to a cached/bundled snapshot). ## What it understands **Plain math** — `2 + 2`, `8 / (45 - 20%)`, parentheses, `^`, `sqrt`, `sin`, -factorials. Decimal-exact: `0.1 + 0.2` is `0.3`, not `0.30000000000000004`. +factorials, `random()`, `random(1; 6)`. Decimal-exact: `0.1 + 0.2` is `0.3`, +not `0.30000000000000004`. **Words as operators** — `12 plus 4`, `20 divided by 4`, `5 и 3`. Text around the math is ignored, so `spent 20 on pizza and 5 on coffee` just gives 25. @@ -58,30 +63,48 @@ the math is ignored, so `spent 20 on pizza and 5 on coffee` just gives 25. (refreshed hourly, cached for offline). `$100 + 20%`, `5 eth in USD`, `9800 рублей в долларах`. +**Live market data** — stock and commodity prices refreshed every 15 minutes: +`100 AAPL in USD`, `2 GC=F in USD` (gold futures), `MSFT + GOOGL`. + +**Historical exchange rates** — `1000 USD in EUR on 2024-01-01` looks up the +ECB rate for that exact date (via frankfurter.app) and caches it permanently. + **Percents the human way** — `20% of 80`, `20% off 1500`, `5 as a % of 25`, `20% of what is 30`. **Dates and time** — `today + 2 weeks`, `tomorrow - today`, `time in New York`, -`now in unix`, `1750000000 as date`. +`now in unix`, `1750000000 as date`. ISO date literals: `2024-01-01 + 30 days`, +`days until 2027-01-01`, `days since 2020-03-15`. + +**Goal seek** — put `?` where the unknown is and an `=` with the target: +`? * 1.2 = 1000` → `833.33`, `100 - ? = 30` → `70`. Works for any expression +solvable by linear probe or bisection. **Programmer stuff** — `0xFF & 0x0F`, `1 << 4`, `255 in hex`, `in binary`, plus `in fraction` (0.75 → 3/4) and `in roman` (2026 → MMXXVI). **Sheets that calculate** — variables (`rent = $500`), running totals -(`sum`, `avg`, `prev`), `#` headers, `//` comments. The status bar always shows -the sheet total; select a few lines to see just their sum. +(`sum`, `avg`, `prev`, `count`, `min`, `max`, `product`), `#` headers, +`//` comments. The status bar always shows the sheet total; select a few lines +to see just their sum. Add `chart` at the end of a block to draw a sparkline. ## Handy things - **Ctrl+Alt+N** (configurable — the settings field records keys you press) shows/hides the window from anywhere. Closing hides to tray. -- **Ctrl+F** (or the 🔍 button) searches every sheet at once, not just the - one you're in — click a hit to jump straight to that line. +- **Ctrl+W** closes the current sheet (asks first if it has content). +- **Ctrl+F** (or the 🔍 button) searches every sheet at once — by text or by + result value: `>1000`, `<50`, `=100`, `~100` (±1%). Click a hit to jump there. - **Click a result** to copy it. Copying a line copies it *with* the answer (`rent * 3 = $1,500`). **Ctrl+Shift+C** copies the whole sheet with answers. +- **Export as image** — the ⤓ menu includes "Copy as image": renders the sheet + to a PNG and puts it on the clipboard (falls back to file save if the browser + blocks clipboard write). - **Drag the divider** to resize the results column. - **Drop a `.numi`, `.sum`, `.txt` or `.md` file** into the window to open it as a sheet. +- **Pin and reorder sheets** — click the pin icon on any sheet in the sidebar + to keep it at the top; use ↑↓ to rearrange. - **Backups are automatic**: a daily snapshot of all sheets (last 14 kept), and deleted sheets sit in a bin for 14 days (configurable) — restore one by dragging the file back in. Settings → Backups opens the folder. @@ -120,7 +143,7 @@ see [ARCHITECTURE.md](ARCHITECTURE.md). ```bash npm install -npm test # engine tests +npm test # engine tests (113 cases) npm run tauri dev # run the app npm run tauri build # build the installer ``` diff --git a/README.ru.md b/README.ru.md index bb94840..ded68da 100644 --- a/README.ru.md +++ b/README.ru.md @@ -16,7 +16,11 @@ $2000 в рублях → 144,060 ₽ скидка 20% от 1500 → 1,200 сегодня + 6 недель → 22 Июля 2026 -время в Токио → 14:25 +1000 USD в EUR на 2024-01-01 → € 917.30 +? * 1.2 = 1000 → 833.33 +AAPL в USD → $213.55 +дней до 2027-01-01 → 183 day +график → ▂▄▇█▆ ``` Работает на **английском и русском одновременно**: `5 метров в см`, `20$ в евро`, `сумма` — переключать язык не нужно. @@ -31,13 +35,14 @@ $2000 в рублях → 144,060 ₽ - **[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)** — уже встроен в Windows 11 и большинство обновлённых Windows 10; если отсутствует, установщик скачает его автоматически. -- Интернет нужен только для живых курсов валют — всё остальное работает офлайн +- Интернет нужен только для живых курсов и рыночных данных — всё остальное работает офлайн (курсы берутся из кэша или встроенного снимка). ## Что понимает **Обычная математика** — `2 + 2`, `8 / (45 - 20%)`, скобки, `^`, `sqrt`, `sin`, -факториалы. Точная десятичная арифметика: `0.1 + 0.2` это `0.3`, а не `0.30000000000000004`. +факториалы, `случайное()`, `случайное(1; 6)`. Точная десятичная арифметика: +`0.1 + 0.2` это `0.3`, а не `0.30000000000000004`. **Слова как операторы** — `12 плюс 4`, `20 делить на 4`, `5 и 3`. Текст вокруг математики игнорируется: `потратил 20 на пиццу и 5 на кофе` даст 25. @@ -54,29 +59,47 @@ $2000 в рублях → 144,060 ₽ (обновляются раз в час, кэшируются для офлайна). `$100 + 20%`, `5 eth в USD`, `9800 рублей в долларах`. +**Живые рыночные данные** — котировки акций и сырья обновляются каждые 15 минут: +`100 AAPL в USD`, `2 GC=F в USD` (фьючерс на золото), `MSFT + GOOGL`. + +**Исторические курсы** — `1000 USD в EUR на 2024-01-01` запрашивает курс ЕЦБ +за этот день (через frankfurter.app) и кэширует его навсегда. + **Проценты по-человечески** — `20% от 80`, `скидка 20% от 1500`, `5 это сколько % от 25`, `20% от какого числа равно 30`. **Даты и время** — `сегодня + 2 недели`, `завтра - сегодня`, `время в Нью-Йорке`, -`сейчас в unix`, `1750000000 как дата`. +`сейчас в unix`, `1750000000 как дата`. ISO-даты: `2024-01-01 + 30 дней`, +`дней до 2027-01-01`, `дней с 2020-03-15`. + +**Поиск неизвестного** — поставь `?` на место неизвестного и `=` с нужным результатом: +`? * 1.2 = 1000` → `833.33`, `100 - ? = 30` → `70`. Работает для любых выражений, +решаемых линейным методом или бисекцией. **Для программистов** — `0xFF & 0x0F`, `1 << 4`, `255 в hex`, `в binary`, плюс `в дроби` (0.75 → 3/4) и `в roman` (2026 → MMXXVI). **Листы с переменными** — переменные (`аренда = $500`), нарастающие итоги -(`сумма`, `avg`, `prev`), заголовки `#`, комментарии `//`. Строка статуса всегда -показывает итог листа; выдели несколько строк — увидишь их сумму. +(`сумма`, `avg`, `prev`, `количество`, `минимум`, `максимум`, `произведение`), +заголовки `#`, комментарии `//`. Строка статуса всегда показывает итог листа; +выдели несколько строк — увидишь их сумму. Добавь `график` в конце блока чтобы +нарисовать спарклайн. ## Удобные мелочи - **Ctrl+Alt+N** (настраивается — поле в настройках записывает нажатые клавиши) показывает/скрывает окно из любого места. Закрытие прячет в трей. -- **Ctrl+F** (или кнопка 🔍) ищет сразу по всем листам, а не только по текущему — - клик по результату переносит прямо на нужную строку. +- **Ctrl+W** закрывает текущий лист (спрашивает подтверждение если есть содержимое). +- **Ctrl+F** (или кнопка 🔍) ищет сразу по всем листам — по тексту или по значению + результата: `>1000`, `<50`, `=100`, `~100` (±1%). Клик по результату — переходишь туда. - **Клик по результату** копирует его. Копирование строки копирует её *вместе с ответом* (`аренда * 3 = $1,500`). **Ctrl+Shift+C** копирует весь лист с ответами. +- **Экспорт картинкой** — меню ⤓ содержит «Скопировать картинкой»: рендерит лист + в PNG и кладёт в буфер обмена (при ошибке предлагает сохранить файл). - **Потяни разделитель** чтобы изменить ширину колонки результатов. - **Брось файл `.numi`, `.sum`, `.txt` или `.md`** в окно — откроется как лист. +- **Закрепление и порядок листов** — нажми значок булавки на любом листе в сайдбаре + чтобы закрепить его вверху; кнопки ↑↓ меняют порядок. - **Резервные копии автоматические**: ежедневный снимок всех листов (хранятся 14 дней), удалённые листы лежат в корзине 14 дней (настраивается) — верни перетащив обратно. Настройки → Backups откроет папку. @@ -115,7 +138,7 @@ numi.addFunction({ id: "гипот", phrases: "гипот" }, ```bash npm install -npm test # тесты движка +npm test # тесты движка (113 кейсов) npm run tauri dev # запустить приложение npm run tauri build # собрать установщик ```