J.A.R.V.I.S-rust/resources/commands/expenses/log.lua
Bossiara13 87bb186e94
Some checks are pending
Rust CI / cargo test (jarvis-core) (push) Waiting to run
Rust CI / cargo clippy (push) Waiting to run
Rust CI / cargo check (workspace) (push) Waiting to run
feat: Wave 10 — expense tracker pack (voice-driven)
resources/commands/expenses/ (4 commands):
- expenses.log     "потратил 500 на еду"     → append entry
- expenses.today   "сколько потратил сегодня" → today's total
- expenses.week    "сколько на неделе"       → 7-day rolling total
- expenses.breakdown "куда ушли деньги"      → top-5 categories

Storage: `jarvis.state` (auto-namespaced per-pack JSON). Wire format is
compact "ts1,amount1,cat1|ts2,amount2,cat2" so we don't need a JSON parser
inside Lua.

Phrase parsing:
- Trigger stripped via jarvis.text.strip_trigger.
- First number captured; "12,50" comma decimals accepted.
- Category extracted from "на <слово>" (RU) or "for/on <word>" (EN);
  defaults to "разное" / "misc".

Time math:
- Day boundary: local-calendar midnight via os.time({year,month,day,...}).
- Week boundary: rolling 7×86400 seconds.

Pack count: 98 → 99 functional.
2026-05-16 14:57:58 +03:00

74 lines
2.8 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Voice "потратил 500 на еду" → append {amount, category, timestamp} to the
-- pack's state JSON.
--
-- Parsing the phrase:
-- 1. Strip the trigger ("потратил" / "spent" / ...)
-- 2. First number = amount
-- 3. After the number: optional "на <слово>" → category (RU) or "for <word>"
-- / "on <word>" (EN). Otherwise category defaults to "разное" / "misc".
local lang = jarvis.context.language
local raw = (jarvis.context.phrase or ""):lower()
local triggers = {
"потратил", "записать трату", "запиши трату", "купил за", "заплатил за",
"spent", "log expense", "paid for", "bought",
}
local rest = jarvis.text.strip_trigger(raw, triggers) or raw
rest = rest:gsub("^%s+", ""):gsub("%s+$", "")
-- Pull the first integer-like token. Allows comma decimals ("12,50" → 12.5).
local amount_str, after = rest:match("(%-?[%d]+[%.,]?[%d]*)%s*(.*)")
if not amount_str then
return jarvis.cmd.not_found(lang == "ru"
and "Не понял сумму, сэр."
or "I didn't catch the amount, sir.")
end
amount_str = amount_str:gsub(",", ".")
local amount = tonumber(amount_str)
if not amount or amount <= 0 then
return jarvis.cmd.not_found(lang == "ru"
and "Сумма не похожа на число."
or "That doesn't look like an amount.")
end
local category = "разное"
local cat_match = (after or ""):match("на%s+(%S+)") or (after or ""):match("for%s+(%S+)")
or (after or ""):match("on%s+(%S+)")
if cat_match and cat_match ~= "" then
category = cat_match
end
-- Load existing log, append, save.
local raw_state = jarvis.state.get("entries") or ""
local entries = {}
if raw_state ~= "" then
-- Encoded as "ts1,amount1,cat1|ts2,amount2,cat2|..."
for chunk in raw_state:gmatch("([^|]+)") do
local ts, am, ca = chunk:match("^(%d+),([%-%d%.]+),(.+)$")
if ts and am and ca then
table.insert(entries, { ts = tonumber(ts), am = tonumber(am), ca = ca })
end
end
end
local now = jarvis.context.time
-- chrono::Local timestamp is exposed via {year, month, day, hour, minute, second}
-- but not as a unix epoch. Approximate via os.time({...}) — sandbox standard
-- gives us os.time/date.
local ts = os.time({
year = now.year, month = now.month, day = now.day,
hour = now.hour, min = now.minute, sec = now.second or 0,
})
table.insert(entries, { ts = ts, am = amount, ca = category })
local out = {}
for _, e in ipairs(entries) do
table.insert(out, string.format("%d,%s,%s", e.ts, tostring(e.am), e.ca))
end
jarvis.state.set("entries", table.concat(out, "|"))
return jarvis.cmd.ok(string.format(lang == "ru"
and "Записал: %s на %s."
or "Logged: %s on %s.",
amount_str, category))