DKL9 GitList
Repositories
DKL9 home
memoire
Code
Commits
Branches
Tags
Search
Tree:
a3c966a
Branches
Tags
master
memoire
actions.lua
Add delay-last review ratio histogram
John Smith
commited
a3c966a
at 2023-63 12:32:57
actions.lua
Blame
History
Raw
--[[ gets the score (again/hard/good/easy/correction) from a string by first character scores are numbers, higher is easier, -1 is correction ]] local function succfromstr(s) local f = s:sub(1, 1):lower() if f == "0" or f == "a" then return 0 elseif f == "1" or f == "h" then return 0.5 elseif f == "2" or f == "g" then return 1 elseif f == "3" or f == "e" then return 1.5 elseif f == "-" or f == "c" then return -1 end return 1 end --[[ clears the terminal implementation may be platform-specific ]] local function clear() -- ANSI io.write("\x1b[H\x1b[J") end --[[ show progress thru a review session, conditioned on the user configuring that to happen ]] local function showprogress(k, n, conf) if conf:get("ShowProgress") then io.write(string.format("(%d/%d) ", k, n)) end end --[[ display text with images, inserted with `IMG[path]` implementation may be platform-specific `basepath` points to the base directory of the images ]] local function imgify(text, basepath, ind, climn, conf) local fh = io.popen("tput lines") local tl = tonumber(fh:read("a")) io.write(("\n"):rep(math.floor(0.4 * tl))) showprogress(ind, climn, conf) if conf:get("BoldContext") then local csi, cei = text:find("^[^:]+:") if csi then local cpt = text:sub(csi, cei) io.write("\x1b[1m") io.write(cpt) io.write("\x1b[0m") text = text:sub(cei + 1) end end -- adapted from https://stackoverflow.com/a/1579673 local IMG_PATTERN = "IMG%[([^%]]+)%]" local PATTERN = "(.-)" .. IMG_PATTERN -- Linux/dwm local DISPIMG = function(path, st) if path and #path > 0 then os.execute(string.format( "/usr/local/bin/iv -t '%s' %s%s &", st:gsub("'", "\""):gsub(IMG_PATTERN, ""), basepath, path )) end end local lastend = 1 local mstart, mend, prevtext, imgpath = text:find(PATTERN) io.write(prevtext or "") DISPIMG(imgpath, text) while mstart do lastend = mend + 1 mstart, mend, prevtext, imgpath = text:find(PATTERN, lastend) io.write(prevtext or "") DISPIMG(imgpath, text) end if lastend <= #text then io.write(text:sub(lastend)) end end --[[ rounds `x` to `n` places (default 0) after the radix point in base `b` (default 10) ]] local function round(x, n, b) local b = b or 10 local n = n or 0 if n == 0 then return math.tointeger(math.floor(x * b ^ n) / b ^ n) else return math.floor(x * b ^ n) / b ^ n end end --[[ return human-friendly string to present a duration (input in seconds), e.g. "44:37", "4.8h", "11d", "8.3m", "17.4y" ]] local function disptime(ts) local YDAY = 365.24 local MDAY = YDAY / 12 local DSEC = 86400 local HSEC = 3600 if ts >= 3 * YDAY * DSEC then return round(ts / (YDAY * DSEC), 1) .. "y" elseif ts >= 4 * MDAY * DSEC then return round(ts / (MDAY * DSEC), 1) .. "m" elseif ts >= 10 * DSEC then return round(ts / DSEC) .. "d" elseif ts >= 1.5 * DSEC then return round(ts / DSEC, 1) .. "d" elseif ts >= 8 * HSEC then return round(ts / HSEC) .. "h" elseif ts >= HSEC then return round(ts / HSEC, 1) .. "h" else return string.format("%d:%02d", round(ts / 60), round(ts % 60)) end end --[[ check if standard input is a terminal ]] local function isatty() local succ = os.execute("tty >/dev/null") return succ end --[[ return number indicating how much of a leech the prompt is ]] local function badness(pr) return pr.fail / (pr.fail + pr.succ) * math.log(pr.fail) end --[[ generate and return text-based vertical histogram, with bins of given width across interval from max to min, transforming values to numbers via func, scaling bar-length by scale ]] local function histogram(list, min, max, width, func, scale) local bins = {} local oorc = 0 local prc = 0 for _, v in ipairs(list) do local n = func(v) if n == n and n >= min and n < max + width then local k = math.floor((n - min) / width) + 1 bins[k] = (bins[k] or 0) + 1 else oorc = oorc + 1 end prc = prc + 1 end local s = "" for n = min, max, width do local k = math.floor((n - min) / width) + 1 s = string.format("%s%.2f\t%s %d\n", s, n, ("\u{2588}"):rep(math.ceil(scale * (bins[k] or 0))), bins[k] or 0) end return s .. string.format("(%d out of range)\n", oorc) end local actions = { a = { desc = "(a)dd card (from stdin)", nd = true, fn = function(config, deck) if isatty() then io.write("Reading prompts; press Ctrl-D to stop\n") end local rt = io.read("a") local cardctr = 0 local prctr = 0 for pt in rt:gmatch("(..-)\n\n") do local nc = {} -- these Prompts will just be converted to Rec, so we don't need to exclude answers from prompt-text for _re in pt:gmatch("%{[^%}]+%}") do table.insert(nc, Prompt:new(pt, "", os.time(), 30, 0, 0, os.time(), config:get("StartingEase") or 2.5)) prctr = prctr + 1 end table.insert(deck, nc) cardctr = cardctr + 1 end io.write(string.format( "Added %d cards (%d prompts)\n", cardctr, prctr )) if cardctr > 0 then return deck end end, }, c = { desc = "(c)orrect card (with card:prompt index)", nd = true, ni = true, fn = function(config, deck, _, ip) local sp = deck[ip[1]][ip[2]] local rt = sp:spt() local fname = "memoire_cc.tmp" local fh = io.open(fname, "w") fh:write(rt) fh:close() os.execute(string.format( "%s %q", os.getenv("EDITOR") or "vi", fname )) fh = io.open(fname, "r") local nt = fh:read("a"):gsub("%s+$", ""):gsub("^%s+", "") fh:close() os.remove(fname) if nt ~= sp:spt() and nt ~= "" then local cardrec = Prompt:torec(deck[ip[1]]) cardrec.PromptText = { nt } deck[ip[1]] = Prompt:fromrecs({ cardrec }, config)[1] return deck end end, }, h = { desc = "(h)elp", nd = false, fn = function() io.write(string.format("Usage: %s ACTION\n\nACTION may be one of\n", arg[0])) local sal = {} for _, v in pairs(ACTIONS) do table.insert(sal, v.desc) end table.sort(sal) for _, v in ipairs(sal) do io.write(string.format("\t%s\n", v)) end end }, l = { desc = "(l)eech search", nd = true, fn = function(config, deck) local leechqueue = {} for i, cg in ipairs(deck) do for j, pr in ipairs(cg) do if pr.ease < 2 and pr.delay >= 0 then table.insert(leechqueue, { i, j }) end end end table.sort(leechqueue, function(a, b) return badness(deck[a[1]][a[2]]) > badness(deck[b[1]][b[2]]) end) io.write(string.format("Detected %d leeches:\n", #leechqueue)) for _, ij in ipairs(leechqueue) do io.write(string.format("%d:%d %s\n", ij[1], ij[2], deck[ij[1]][ij[2]])) end end, }, r = { desc = "(r)eview sesion", nd = true, fn = function(config, deck, deckfname) if not isatty() then io.write("Review should be done in the terminal\n") return nil end -- collect list of index-pairs for prompts to be reviewed this session local ctrt = review.list(deck) io.write(string.format( "%d prompts are ready for review.\nHow many to review in this session? ", #ctrt )) io.flush() local climt = io.read("l") local climn = tonumber(climt) if not climn then io.write("Input not a number; aborting\n") return nil end climn = math.min(climn, #ctrt) local lrc = 0 local editqueue = {} local start = os.time() -- review prompts in this session's set for ind, ip in ipairs(ctrt) do if ind > climn then break end local pr = deck[ip[1]][ip[2]] -- remove braces for display local pst = pr.text:gsub("%{([^{}]+)%}", "%1") local st = os.time() imgify(pst:gsub("%%A", "___"), deckfname:match(".*/") or "", ind, climn, config) io.write("\n") io.read("l") clear() imgify( pst:gsub("%%A", "\x1b[1m" .. (pr.ans:gsub("%%", "%%%%")) .. "\x1b[0m"), deckfname:match(".*/") or "", ind, climn, config ) io.write(string.format( "\nAgain / Hard (%s) / Good (%s) / Easy (%s) / Correction ", disptime(review.schedule(pr, st, 0.5, config)), disptime(review.schedule(pr, st, 1, config)), disptime(review.schedule(pr, st, 1.5, config)) )) io.flush() local resp = io.read("l") local sc = succfromstr(resp) if sc == -1 then table.insert(editqueue, ip) else if sc == 0 then lrc = lrc + 1 end review.update(deck[ip[1]][ip[2]], st, sc, config) end clear() if resp:sub(#resp):lower() == "q" then climn = ind break end end if #ctrt > 0 then local endt = os.time() io.write(string.format( "Reviewed %d prompts in %d seconds\n", math.min(#ctrt, climn), endt - start )) local s if lrc == 1 then s = "" else s = "s" end io.write(string.format( "(%d lapse%s, %f seconds per card)\n", lrc, s, (endt - start) / math.min(#ctrt, climn) )) end if #editqueue > 0 then io.write(string.format("The following cards are marked for correction:\n")) for _, ip in ipairs(editqueue) do io.write(string.format("%d:%d %s\n", ip[1], ip[2], deck[ip[1]][ip[2]])) end end if climn > 0 then return deck end end, }, s = { desc = "(s)tatistics (with optional query)", nd = true, nq = true, fn = function(config, deck, deckfname, qs) local tcc = 0 local tsc = 0 local tfc = 0 local spc = 0 local expdeck = {} for _, card in ipairs(deck) do local rtc for _, pr in ipairs(card) do if not qs or (pr.text:find(qs) or pr.ans:find(qs)) then rtc = true table.insert(expdeck, pr) tsc = tsc + pr.succ tfc = tfc + pr.fail if pr.delay < 0 then spc = spc + 1 end end end if rtc then tcc = tcc + 1 end end local qfn if qs then qfn = string.format(" (filtered with query %q)", qs) else qfn = "" end io.write(string.format( "%s%s has %d cards and %d total prompts\n", deckfname, qfn, tcc, #expdeck )) io.write(string.format( "(average %.2f prompts per card, %d/%.1f%% prompts suspended).\n", #expdeck / tcc, spc, 100 * spc / #expdeck )) io.write(string.format( "The deck's prompts have been reviewed a total of %d times\n", tsc + tfc )) io.write(string.format( "(average %.2f reviews per prompt), overall retention %.2f%%.\n", (tsc + tfc) / #expdeck, 100 * tsc / (tsc + tfc) )) io.write("\nEase\n" .. histogram(expdeck, 1.0, 3.3, 0.1, function(pr) return pr.ease end, 0.03)) io.write("\nLog2-delay (seconds)\n" .. histogram(expdeck, 15, 28, 0.5, function(pr) return math.log(pr.delay, 2) end, 0.1)) io.write("\nDelay-last review ratio\n" .. histogram(expdeck, 0, 2, 0.1, function(pr) return (os.time() - pr.time) / pr.delay end, 0.1)) io.write("\nRetention (%)\n" .. histogram(expdeck, 30, 100, 10, function(pr) return 100 * pr:retention() end, 0.02)) io.write("\nReview counts\n" .. histogram(expdeck, 0, 20, 2, function(pr) return pr.succ + pr.fail end, 0.03)) io.write("\nLog2-age (seconds)\n" .. histogram(expdeck, 18, 28, 0.5, function(pr) return math.log(os.time() - (pr.created or 2 * os.time()), 2) end, 0.05)) io.write("\nPrompt-count\n" .. histogram(deck, 1, 10, 1, function(card) return #card end, 0.03)) local cpcs = {} for _, card in ipairs(deck) do local cp = card[1].text:match("([^:]+):") or "" cpcs[cp] = (cpcs[cp] or 0) + 1 end local scl = {} for l, c in pairs(cpcs) do table.insert(scl, { l = l, c = c }) end table.sort(scl, function(a, b) return a.c > b.c end) for k, lc in ipairs(scl) do scl[k] = string.format("%s (%d)", lc.l ~= "" and lc.l or "(none)", lc.c) end io.write("\nMost common contexts: " .. table.concat(scl, ", ", 1, math.min(20, #scl)) .. "\n") table.sort(expdeck, function(a, b) return a.time + a.delay < b.time + b.delay end) io.write("\nUpcoming due prompts wrt days from now:\n") local ppi = 1 local mpi = 1 for dc = 1,100 do for uc = ppi,#expdeck do if expdeck[uc].time + expdeck[uc].delay - os.time() > dc * 86400 then mpi = uc break end end if dc <= 10 or dc % 10 == 0 then io.write(string.format("%d\t%s %d\n", dc, ("\u{2588}"):rep(math.ceil((mpi - ppi) / 10)), mpi - ppi)) end ppi = mpi end end }, t = { desc = "(t)oggle suspension of prompt (with card:prompt index)", nd = true, ni = true, fn = function(config, deck, _, ip) local sp = deck[ip[1]][ip[2]] local dir if sp.delay < 0 then dir = "Restoring" else dir = "Suspending" end io.write(string.format("%s %s\n", dir, sp)) sp.delay = -sp.delay return deck end, }, } return actions