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.
74 lines
2.8 KiB
Lua
74 lines
2.8 KiB
Lua
-- 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))
|