From 942bf43b52898721a6e1ddc13988988c25ab3bef Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 24 Mar 2011 10:44:13 +0900 Subject: [PATCH] fix stats they're running now, but need to be sanity checked to make sure they're doing the right thing --- anki/deck.py | 11 ++++++ anki/stats.py | 89 +++++++++++++++++++++++---------------------- anki/storage.py | 4 +- tests/test_stats.py | 23 ++++++++++++ 4 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 tests/test_stats.py diff --git a/anki/deck.py b/anki/deck.py index d1d562016..224f581a5 100644 --- a/anki/deck.py +++ b/anki/deck.py @@ -617,6 +617,17 @@ select conf from gconf where id = (select gcid from groups where id = ?)""", import anki.find return anki.find.findDuplicates(self, fmids) + # Stats + ########################################################################## + + def cardStats(self, card): + from anki.stats import CardStats + return CardStats(self, card).report() + + def deckStats(self): + from anki.stats import DeckStats + return DeckStats(self).report() + # Timeboxing ########################################################################## diff --git a/anki/stats.py b/anki/stats.py index 26dd18c0c..8e5e73b60 100644 --- a/anki/stats.py +++ b/anki/stats.py @@ -4,6 +4,7 @@ import time, sys, os, datetime import anki, anki.utils +from anki.consts import * from anki.lang import _, ngettext from anki.hooks import runFilter @@ -21,12 +22,12 @@ class CardStats(object): fmt = anki.utils.fmtTimeSpan fmtFloat = anki.utils.fmtFloat self.txt = "" - self.addLine(_("Added"), self.strTime(c.created)) + self.addLine(_("Added"), self.strTime(c.crt)) first = self.deck.db.scalar( - "select time/1000 from revlog where rep = 1 and cardId = :id", id=c.id) + "select time/1000 from revlog where rep = 1 and cid = :id", id=c.id) if first: self.addLine(_("First Review"), self.strTime(first)) - self.addLine(_("Changed"), self.strTime(c.modified)) + self.addLine(_("Changed"), self.strTime(c.mod)) if c.reps: next = time.time() - c.due if next > 0: @@ -34,18 +35,18 @@ class CardStats(object): else: next = _("in %s") % fmt(abs(next)) self.addLine(_("Due"), next) - self.addLine(_("Interval"), fmt(c.interval * 86400)) + self.addLine(_("Interval"), fmt(c.ivl * 86400)) self.addLine(_("Ease"), fmtFloat(c.factor, point=2)) if c.reps: self.addLine(_("Reviews"), "%d/%d (s=%d)" % ( - c.reps-c.lapses, c.reps, c.successive)) + c.reps-c.lapses, c.reps, c.streak)) (cnt, total) = self.deck.db.first( - "select count(), sum(userTime)/1000 from revlog where cardId = :id", id=c.id) + "select count(), sum(taken)/1000 from revlog where cid = :id", id=c.id) if cnt: self.addLine(_("Average Time"), fmt(total / float(cnt), point=2)) self.addLine(_("Total Time"), fmt(total, point=2)) - self.addLine(_("Model Tags"), c.fact.model.tags) - self.addLine(_("Card Template") + " "*5, c.cardModel.name) + self.addLine(_("Model"), c.model().name) + self.addLine(_("Template") + " "*5, c.template()['name']) self.txt += "
" return self.txt @@ -66,32 +67,32 @@ class DeckStats(object): def matureCardCount(self): return self.deck.db.scalar( - "select count(id) from cards where interval >= :t ", + "select count(id) from cards where ivl >= :t ", t=MATURE_THRESHOLD) def youngCardCount(self): return self.deck.db.scalar( - "select count(id) from cards where interval < :t " + "select count(id) from cards where ivl < :t " "and reps != 0", t=MATURE_THRESHOLD) def newCountAll(self): "All new cards, including spaced." return self.deck.db.scalar( - "select count(id) from cards where type = 2") + "select count(id) from cards where type = 0") def report(self): "Return an HTML string with a report." fmtPerc = anki.utils.fmtPercentage fmtFloat = anki.utils.fmtFloat - if self.deck.isEmpty(): + if not self.deck.cardCount(): return _("Please add some cards first.") + "

" d = self.deck html="

" + _("Deck Statistics") + "

" - html += _("Deck created: %s ago
") % self.createdTimeStr() + html += _("Deck created: %s ago
") % self.crtTimeStr() total = d.cardCount() - new = d.newCountAll() - young = d.youngCardCount() - old = d.matureCardCount() + new = self.newCountAll() + young = self.youngCardCount() + old = self.matureCardCount() newP = new / float(total) * 100 youngP = young / float(total) * 100 oldP = old / float(total) * 100 @@ -109,7 +110,7 @@ class DeckStats(object): 'young': stats['young'], 'youngP' : fmtPerc(stats['youngP'])} html += _("Unseen cards:") + " %(new)d (%(newP)s)
" % { 'new': stats['new'], 'newP' : fmtPerc(stats['newP'])} - avgInt = self.getAverageInterval() + avgInt = self.getAverageIvl() if avgInt: html += _("Average interval: ") + ("%s ") % fmtFloat(avgInt) + _("days") html += "
" @@ -131,7 +132,7 @@ class DeckStats(object): 'partOf' : nYes, 'totalSum' : nAll } + "

") # average pending time - existing = d.cardCount() - d.newCount + existing = d.cardCount() - self.newCountAll() def tr(a, b): return "%s%s" % (a, b) def repsPerDay(reps,days): @@ -170,7 +171,7 @@ class DeckStats(object): else: html += "" html += tr(_("Deck life"), ("%s ") % ( - fmtFloat(self.getSumInverseRoundInterval())) + _("cards/day")) + fmtFloat(self.getSumInverseRoundIvl())) + _("cards/day")) html += tr(_("In next week"), ("%s ") % ( fmtFloat(self.getWorkloadPeriod(7))) + _("cards/day")) html += tr(_("In next month"), ("%s ") % ( @@ -234,37 +235,39 @@ class DeckStats(object): html += "
" html += "

" + _("Card Ease") + "
" - html += _("Lowest factor: %.2f") % d.s.scalar( - "select min(factor) from cards") + "
" - html += _("Average factor: %.2f") % d.s.scalar( - "select avg(factor) from cards") + "
" - html += _("Highest factor: %.2f") % d.s.scalar( - "select max(factor) from cards") + "
" + html += _("Lowest factor: %.2f") % d.db.scalar( + "select min(factor)/1000.0 from cards") + "
" + html += _("Average factor: %.2f") % d.db.scalar( + "select avg(factor)/1000.0 from cards") + "
" + html += _("Highest factor: %.2f") % d.db.scalar( + "select max(factor)/1000.0 from cards") + "
" html = runFilter("deckStats", html) return html def getMatureCorrect(self, test=None): if not test: - test = "lastInterval > 21" + test = "lastIvl > 21" head = "select count() from revlog where %s" all = self.deck.db.scalar(head % test) yes = self.deck.db.scalar((head % test) + " and ease > 1") + if not all: + return (0, 0, 0) return (all, yes, yes/float(all)*100) def getYoungCorrect(self): - return self.getMatureCorrect("lastInterval <= 21 and rep != 1") + return self.getMatureCorrect("lastIvl <= 21 and rep != 1") def getNewCorrect(self): return self.getMatureCorrect("rep = 1") def getDaysReviewed(self, start, finish): - today = self.deck.failedCutoff + today = self.deck.sched.dayCutoff x = today + 86400*start y = today + 86400*finish return self.deck.db.scalar(""" select count(distinct(cast((time/1000-:off)/86400 as integer))) from revlog -where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset) +where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.crt) def getRepsDone(self, start, finish): now = datetime.datetime.today() @@ -274,13 +277,13 @@ where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset) "select count() from revlog where time >= :x*1000 and time <= :y*1000", x=x, y=y) - def getAverageInterval(self): + def getAverageIvl(self): return self.deck.db.scalar( - "select sum(interval) / count(interval) from cards " + "select sum(ivl) / count(ivl) from cards " "where cards.reps > 0") or 0 - def intervalReport(self, intervals, labels, total): - boxes = self.splitIntoIntervals(intervals) + def ivlReport(self, ivls, labels, total): + boxes = self.splitIntoIvls(ivls) keys = boxes.keys() keys.sort() html = "" @@ -292,13 +295,13 @@ where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset) fmtPerc(boxes[key] / float(total) * 100)) return html - def splitIntoIntervals(self, intervals): + def splitIntoIvls(self, ivls): boxes = {} n = 0 - for i in range(len(intervals) - 1): - (min, max) = (intervals[i], intervals[i+1]) + for i in range(len(ivls) - 1): + (min, max) = (ivls[i], ivls[i+1]) for c in self.deck: - if c.interval > min and c.interval <= max: + if c.ivl > min and c.ivl <= max: boxes[n] = boxes.get(n, 0) + 1 n += 1 return boxes @@ -307,15 +310,15 @@ where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset) "Average number of new cards added each day." return self.deck.cardCount() / max(1, self.ageInDays()) - def createdTimeStr(self): - return anki.utils.fmtTimeSpan(time.time() - self.deck.created) + def crtTimeStr(self): + return anki.utils.fmtTimeSpan(time.time() - self.deck.crt) def ageInDays(self): - return (time.time() - self.deck.created) / 86400.0 + return (time.time() - self.deck.crt) / 86400.0 - def getSumInverseRoundInterval(self): + def getSumInverseRoundIvl(self): return self.deck.db.scalar( - "select sum(1/round(max(interval, 1)+0.5)) from cards " + "select sum(1/round(max(ivl, 1)+0.5)) from cards " "where cards.reps > 0 " "and queue != -1") or 0 @@ -336,7 +339,7 @@ where time > :cutoff*1000""", cutoff=cutoff) or 0) / float(period) cutoff = time.time() - 86400 * period return (self.deck.db.scalar(""" select count(id) from cards -where created > :cutoff""", cutoff=cutoff) or 0) +where crt > :cutoff""", cutoff=cutoff) or 0) def getFirstPeriod(self, period): cutoff = time.time() - 86400 * period diff --git a/anki/storage.py b/anki/storage.py index a20e05259..0f4882fa5 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -138,8 +138,8 @@ create table if not exists revlog ( cid integer not null, ease integer not null, rep integer not null, - int integer not null, - lastInt integer not null, + ivl integer not null, + lastIvl integer not null, factor integer not null, taken integer not null, type integer not null diff --git a/tests/test_stats.py b/tests/test_stats.py new file mode 100644 index 000000000..7d5eb831a --- /dev/null +++ b/tests/test_stats.py @@ -0,0 +1,23 @@ +# coding: utf-8 + +import time, copy +from tests.shared import assertException, getEmptyDeck +from anki.stdmodels import BasicModel +from anki.utils import stripHTML, intTime +from anki.hooks import addHook + +def test_stats(): + d = getEmptyDeck() + f = d.newFact() + f['Front'] = "foo" + d.addFact(f) + c = f.cards()[0] + # card stats + assert d.cardStats(c) + d.reset() + c = d.sched.getCard() + d.sched.answerCard(c, 3) + d.sched.answerCard(c, 2) + assert d.cardStats(c) + # deck stats + assert d.deckStats()