require 'filestream'
require 'vector'
require 'io'
local time = require 'kaltime'
local Eat = @record{
kcal: number,
fat: number,
carb: number,
protein: number,
brand: string,
name: string,
}
local Day = @record{
date: time.Time,
goal: number,
burnt: number,
eats: vector(Eat),
}
function Day:populated(): boolean
return self.date ~= 0 and self.goal ~= 0
end
function Day:deficit(): number
local eaten = 0
for _, e in ipairs(self.eats) do
eaten = eaten + e.kcal
end
return eaten - self.burnt
end
local function load_days(file: filestream): (vector(Day), string)
local result: vector(Day)
local day: Day
local lineno = 0
for line <close> in file:lines() do
lineno = lineno + 1
if line == '' then
if day:populated() then result:push(day) end
day = Day{}
continue
elseif line == 'd now' then
day.date = time.Now()
continue
end
##[[local function try(pat)]] local ok, matches <close> = string.matchview(line, #[pat]#) ##[[end]]
##[[local function orfail(kind)]] if not ok then return {}, string.format('bad %s in line #%d: %s', #[kind]#, lineno, line) end ##[[end]]
switch line[1] do
case 'd'_u8 then
## try '^d %S+ (%d+ %S+ %d+)$' orfail 'date'
local ok, epoch = time.Parse('%0d %b %Y', matches[1]) ## orfail 'date'
day.date = epoch
case 't'_u8 then
## try '^t %d+ / (%d+) %([-%d]+%) = %d+%%$' orfail 'calorie goal'
day.goal = tonumber(matches[1])
case 'b'_u8 then
## try '^b (%d+) = %d+%% %([-%d]+%)$' orfail 'calories burned'
day.burnt = tonumber(matches[1])
case 'T'_u8 then
-- ignore
case 'k'_u8 then
## try '^k +%d+%% +(%d+) +(%d+) +(%d+) +(%d+) +(%S+) "(.-)"$'
if not ok then return {}, 'bad k line: ' .. line end
day.eats:push({
kcal = tonumber(matches[1]),
fat = tonumber(matches[2]),
carb = tonumber(matches[3]),
protein = tonumber(matches[4]),
brand = string.copy(matches[5]),
name = string.copy(matches[6]),
})
else
return {}, 'bad line: ' .. line
end
end
if day:populated() then result:push(day) end
return result
end
local function dump_days(days: vector(Day)): string
local buffer: [256]byte
local tbuffer = (@string){data=&buffer[0], size=#buffer}
local first = true
for i, d in ipairs(days) do
if first then first = false else print '' end
local eaten : number
local left = d.goal
local fat : number
local carb : number
local protein : number
for _, e in ipairs(d.eats) do
eaten = eaten + e.kcal
left = left - e.kcal
fat = fat + e.fat
carb = carb + e.carb
protein = protein + e.protein
end
local daypct = eaten / d.goal * 100
local burnpct = d.burnt / eaten * 100
local proteinpct = protein / (fat + carb + protein) * 100
if not time.Format(tbuffer, '%A, %d %b %Y', d.date) then return string.format('unable to format date: %lu', d.date) end
io.writef('d %s\n', tbuffer)
io.writef('t %.0f / %.0f (%.0f) = %.0f%%\n', eaten, d.goal, left, daypct)
io.writef('b %.0f = %.0f%% (%.0f)\n', d.burnt, burnpct, d:deficit())
io.writef('T %.0f:%.0f:%.0f = %.0f %.0f%%\n', fat, carb, protein, fat+carb+protein, proteinpct)
for _, e in ipairs(d.eats) do
local pct = e.kcal / d.goal * 100
io.writef('k %3.0f%% %4.0f %3.0f %3.0f %3.0f %s "%s"\n', pct, e.kcal, e.fat, e.carb, e.protein, cstring(e.brand), cstring(e.name))
end
end
io.flush()
return ''
end
##[[local function ordie()]] if err ~= '' then io.stderr:print(err) return 1 end ##[[end]]
local diet <close>, err <close>, errcode = io.open 'diet' ## ordie()
local days, err = load_days(diet) ## ordie()
defer
for _, d in ipairs(days) do
for _, e in ipairs(d.eats) do
e.brand:destroy()
e.name:destroy()
end
d.eats:destroy()
end
days:destroy()
end
local err = dump_days(days) ## ordie()