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()