mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
retire the v1 scheduler
This commit is contained in:
parent
68092082f2
commit
7f40d6d2a5
5 changed files with 31 additions and 1738 deletions
|
@ -98,6 +98,9 @@ scheduling-update-done = Scheduler updated successfully.
|
|||
scheduling-update-button = Update
|
||||
scheduling-update-later-button = Later
|
||||
scheduling-update-more-info-button = Learn More
|
||||
scheduling-update-required =
|
||||
Your collection needs to be upgraded to the V2 scheduler.
|
||||
Please select { scheduling-update-more-info-button } before proceeding.
|
||||
|
||||
## Other scheduling strings
|
||||
|
||||
|
|
|
@ -5,24 +5,14 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
from heapq import *
|
||||
|
||||
import anki
|
||||
from anki import hooks
|
||||
from anki.cards import Card
|
||||
from anki.consts import *
|
||||
from anki.decks import DeckId
|
||||
from anki.utils import ids2str, int_time
|
||||
|
||||
from .v2 import QueueConfig
|
||||
from .v2 import Scheduler as V2
|
||||
|
||||
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
|
||||
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
|
||||
# positive revlog intervals are in days (rev), negative in seconds (lrn)
|
||||
|
||||
|
||||
class Scheduler(V2):
|
||||
version = 1
|
||||
|
@ -35,689 +25,60 @@ class Scheduler(V2):
|
|||
self, col: anki.collection.Collection
|
||||
) -> None:
|
||||
super().__init__(col)
|
||||
self.queueLimit = 50
|
||||
self.reportLimit = 1000
|
||||
self.dynReportLimit = 99999
|
||||
self.queueLimit = 0
|
||||
self.reportLimit = 0
|
||||
self.dynReportLimit = 0
|
||||
self.reps = 0
|
||||
self.lrnCount = 0
|
||||
self.revCount = 0
|
||||
self.newCount = 0
|
||||
self._haveQueues = False
|
||||
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
def getCard(self) -> Card | None:
|
||||
raise Exception("v1 scheduler no longer supported")
|
||||
|
||||
def answerCard(self, card: Card, ease: int) -> None:
|
||||
assert 1 <= ease <= 4
|
||||
self.col.save_card_review_undo_info(card)
|
||||
if self._burySiblingsOnAnswer:
|
||||
self._burySiblings(card)
|
||||
card.reps += 1
|
||||
self.reps += 1
|
||||
# former is for logging new cards, latter also covers filt. decks
|
||||
card.wasNew = card.type == CARD_TYPE_NEW # type: ignore
|
||||
wasNewQ = card.queue == QUEUE_TYPE_NEW
|
||||
raise Exception("v1 scheduler no longer supported")
|
||||
|
||||
new_delta = 0
|
||||
review_delta = 0
|
||||
def _is_finished(self) -> bool:
|
||||
return False
|
||||
|
||||
if wasNewQ:
|
||||
# came from the new queue, move to learning
|
||||
card.queue = QUEUE_TYPE_LRN
|
||||
# if it was a new card, it's now a learning card
|
||||
if card.type == CARD_TYPE_NEW:
|
||||
card.type = CARD_TYPE_LRN
|
||||
# init reps to graduation
|
||||
card.left = self._startingLeft(card)
|
||||
# dynamic?
|
||||
if card.odid and card.type == CARD_TYPE_REV:
|
||||
if self._resched(card):
|
||||
# reviews get their ivl boosted on first sight
|
||||
card.ivl = self._dynIvlBoost(card)
|
||||
card.odue = self.today + card.ivl
|
||||
new_delta = +1
|
||||
if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
|
||||
self._answerLrnCard(card, ease)
|
||||
elif card.queue == QUEUE_TYPE_REV:
|
||||
self._answerRevCard(card, ease)
|
||||
review_delta = +1
|
||||
else:
|
||||
raise Exception(f"Invalid queue '{card}'")
|
||||
|
||||
self.update_stats(
|
||||
card.did,
|
||||
new_delta=new_delta,
|
||||
review_delta=review_delta,
|
||||
milliseconds_delta=+card.time_taken(),
|
||||
)
|
||||
|
||||
card.mod = int_time()
|
||||
card.usn = self.col.usn()
|
||||
card.flush()
|
||||
|
||||
def counts(self, card: Card | None = None) -> tuple[int, int, int]:
|
||||
counts = [self.newCount, self.lrnCount, self.revCount]
|
||||
if card:
|
||||
idx = self.countIdx(card)
|
||||
if idx == QUEUE_TYPE_LRN:
|
||||
counts[int(QUEUE_TYPE_LRN)] += card.left // 1000
|
||||
else:
|
||||
counts[idx] += 1
|
||||
|
||||
new, lrn, rev = counts
|
||||
return (new, lrn, rev)
|
||||
|
||||
def countIdx(self, card: Card) -> int:
|
||||
if card.queue == QUEUE_TYPE_DAY_LEARN_RELEARN:
|
||||
return QUEUE_TYPE_LRN
|
||||
return card.queue
|
||||
|
||||
def answerButtons(self, card: Card) -> int:
|
||||
if card.odue:
|
||||
# normal review in dyn deck?
|
||||
if card.odid and card.queue == QUEUE_TYPE_REV:
|
||||
return 4
|
||||
conf = self._lrnConf(card)
|
||||
if card.type in (CARD_TYPE_NEW, CARD_TYPE_LRN) or len(conf["delays"]) > 1:
|
||||
return 3
|
||||
return 2
|
||||
elif card.queue == QUEUE_TYPE_REV:
|
||||
return 4
|
||||
else:
|
||||
return 3
|
||||
|
||||
# Getting the next card
|
||||
##########################################################################
|
||||
|
||||
def _getCard(self) -> Card | None:
|
||||
"Return the next due card id, or None."
|
||||
# learning card due?
|
||||
c = self._getLrnCard()
|
||||
if c:
|
||||
return c
|
||||
# new first, or time for one?
|
||||
if self._timeForNewCard():
|
||||
c = self._getNewCard()
|
||||
if c:
|
||||
return c
|
||||
# card due for review?
|
||||
c = self._getRevCard()
|
||||
if c:
|
||||
return c
|
||||
# day learning card due?
|
||||
c = self._getLrnDayCard()
|
||||
if c:
|
||||
return c
|
||||
# new cards left?
|
||||
c = self._getNewCard()
|
||||
if c:
|
||||
return c
|
||||
# collapse or finish
|
||||
return self._getLrnCard(collapse=True)
|
||||
|
||||
# Learning queues
|
||||
##########################################################################
|
||||
|
||||
def _resetLrnCount(self) -> None:
|
||||
# sub-day
|
||||
self.lrnCount = (
|
||||
self.col.db.scalar(
|
||||
f"""
|
||||
select sum(left/1000) from (select left from cards where
|
||||
did in %s and queue = {QUEUE_TYPE_LRN} and due < ? limit %d)"""
|
||||
% (self._deck_limit(), self.reportLimit),
|
||||
self.day_cutoff,
|
||||
)
|
||||
or 0
|
||||
)
|
||||
# day
|
||||
self.lrnCount += self.col.db.scalar(
|
||||
f"""
|
||||
select count() from cards where did in %s and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN}
|
||||
and due <= ? limit %d"""
|
||||
% (self._deck_limit(), self.reportLimit),
|
||||
self.today,
|
||||
)
|
||||
|
||||
def _resetLrn(self) -> None:
|
||||
self._resetLrnCount()
|
||||
self._lrnQueue: list[Any] = []
|
||||
self._lrnDayQueue: list[Any] = []
|
||||
self._lrnDids = self.col.decks.active()[:]
|
||||
|
||||
# sub-day learning
|
||||
def _fillLrn(self) -> bool | list[Any]:
|
||||
if not self.lrnCount:
|
||||
return False
|
||||
if self._lrnQueue:
|
||||
return True
|
||||
self._lrnQueue = self.col.db.all(
|
||||
f"""
|
||||
select due, id from cards where
|
||||
did in %s and queue = {QUEUE_TYPE_LRN} and due < ?
|
||||
limit %d"""
|
||||
% (self._deck_limit(), self.reportLimit),
|
||||
self.day_cutoff,
|
||||
)
|
||||
self._lrnQueue = [tuple(e) for e in self._lrnQueue]
|
||||
# as it arrives sorted by did first, we need to sort it
|
||||
self._lrnQueue.sort()
|
||||
return self._lrnQueue
|
||||
|
||||
def _getLrnCard(self, collapse: bool = False) -> Card | None:
|
||||
if self._fillLrn():
|
||||
cutoff = time.time()
|
||||
if collapse:
|
||||
cutoff += self.col.conf["collapseTime"]
|
||||
if self._lrnQueue[0][0] < cutoff:
|
||||
id = heappop(self._lrnQueue)[1]
|
||||
card = self.col.getCard(id)
|
||||
self.lrnCount -= card.left // 1000
|
||||
return card
|
||||
return None
|
||||
|
||||
def _answerLrnCard(self, card: Card, ease: int) -> None:
|
||||
# ease 1=no, 2=yes, 3=remove
|
||||
conf = self._lrnConf(card)
|
||||
if card.odid and not card.wasNew: # type: ignore
|
||||
type = REVLOG_CRAM
|
||||
elif card.type == CARD_TYPE_REV:
|
||||
type = REVLOG_RELRN
|
||||
else:
|
||||
type = REVLOG_LRN
|
||||
leaving = False
|
||||
# lrnCount was decremented once when card was fetched
|
||||
lastLeft = card.left
|
||||
# immediate graduate?
|
||||
if ease == BUTTON_THREE:
|
||||
self._rescheduleAsRev(card, conf, True)
|
||||
leaving = True
|
||||
# graduation time?
|
||||
elif ease == BUTTON_TWO and (card.left % 1000) - 1 <= 0:
|
||||
self._rescheduleAsRev(card, conf, False)
|
||||
leaving = True
|
||||
else:
|
||||
# one step towards graduation
|
||||
if ease == BUTTON_TWO:
|
||||
# decrement real left count and recalculate left today
|
||||
left = (card.left % 1000) - 1
|
||||
card.left = self._leftToday(conf["delays"], left) * 1000 + left
|
||||
# failed
|
||||
else:
|
||||
card.left = self._startingLeft(card)
|
||||
resched = self._resched(card)
|
||||
if "mult" in conf and resched:
|
||||
# review that's lapsed
|
||||
card.ivl = max(1, conf["minInt"], int(card.ivl * conf["mult"]))
|
||||
else:
|
||||
# new card; no ivl adjustment
|
||||
pass
|
||||
if resched and card.odid:
|
||||
card.odue = self.today + 1
|
||||
delay = self._delayForGrade(conf, card.left)
|
||||
if card.due < time.time():
|
||||
# not collapsed; add some randomness
|
||||
delay *= int(random.uniform(1, 1.25))
|
||||
card.due = int(time.time() + delay)
|
||||
# due today?
|
||||
if card.due < self.day_cutoff:
|
||||
self.lrnCount += card.left // 1000
|
||||
# if the queue is not empty and there's nothing else to do, make
|
||||
# sure we don't put it at the head of the queue and end up showing
|
||||
# it twice in a row
|
||||
card.queue = QUEUE_TYPE_LRN
|
||||
if self._lrnQueue and not self.revCount and not self.newCount:
|
||||
smallestDue = self._lrnQueue[0][0]
|
||||
card.due = max(card.due, smallestDue + 1)
|
||||
heappush(self._lrnQueue, (card.due, card.id))
|
||||
else:
|
||||
# the card is due in one or more days, so we need to use the
|
||||
# day learn queue
|
||||
ahead = ((card.due - self.day_cutoff) // 86400) + 1
|
||||
card.due = self.today + ahead
|
||||
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
|
||||
self._logLrn(card, ease, conf, leaving, type, lastLeft)
|
||||
|
||||
def _lrnConf(self, card: Card) -> QueueConfig:
|
||||
if card.type == CARD_TYPE_REV:
|
||||
return self._lapseConf(card)
|
||||
else:
|
||||
return self._newConf(card)
|
||||
|
||||
def _rescheduleAsRev(self, card: Card, conf: QueueConfig, early: bool) -> None:
|
||||
lapse = card.type == CARD_TYPE_REV
|
||||
if lapse:
|
||||
if self._resched(card):
|
||||
card.due = max(self.today + 1, card.odue)
|
||||
else:
|
||||
card.due = card.odue
|
||||
card.odue = 0
|
||||
else:
|
||||
self._rescheduleNew(card, conf, early)
|
||||
card.queue = QUEUE_TYPE_REV
|
||||
card.type = CARD_TYPE_REV
|
||||
# if we were dynamic, graduating means moving back to the old deck
|
||||
resched = self._resched(card)
|
||||
if card.odid:
|
||||
card.did = card.odid
|
||||
card.odue = 0
|
||||
card.odid = DeckId(0)
|
||||
# if rescheduling is off, it needs to be set back to a new card
|
||||
if not resched and not lapse:
|
||||
card.queue = QUEUE_TYPE_NEW
|
||||
card.type = CARD_TYPE_NEW
|
||||
card.due = self.col.nextID("pos")
|
||||
|
||||
def _startingLeft(self, card: Card) -> int:
|
||||
if card.type == CARD_TYPE_REV:
|
||||
conf = self._lapseConf(card)
|
||||
else:
|
||||
conf = self._lrnConf(card)
|
||||
tot = len(conf["delays"])
|
||||
tod = self._leftToday(conf["delays"], tot)
|
||||
return tot + tod * 1000
|
||||
# stubs of v1-specific routines that add-ons may be overriding
|
||||
|
||||
def _graduatingIvl(
|
||||
self, card: Card, conf: QueueConfig, early: bool, adj: bool = True
|
||||
) -> int:
|
||||
if card.type == CARD_TYPE_REV:
|
||||
# lapsed card being relearnt
|
||||
if card.odid:
|
||||
if conf["resched"]:
|
||||
return self._dynIvlBoost(card)
|
||||
return card.ivl
|
||||
if not early:
|
||||
# graduate
|
||||
ideal = conf["ints"][0]
|
||||
else:
|
||||
# early remove
|
||||
ideal = conf["ints"][1]
|
||||
if adj:
|
||||
return self._adjRevIvl(card, ideal)
|
||||
else:
|
||||
return ideal
|
||||
|
||||
def _rescheduleNew(self, card: Card, conf: QueueConfig, early: bool) -> None:
|
||||
"Reschedule a new card that's graduated for the first time."
|
||||
card.ivl = self._graduatingIvl(card, conf, early)
|
||||
card.due = self.today + card.ivl
|
||||
card.factor = conf["initialFactor"]
|
||||
|
||||
def _logLrn(
|
||||
self,
|
||||
card: Card,
|
||||
ease: int,
|
||||
conf: QueueConfig,
|
||||
leaving: bool,
|
||||
type: int,
|
||||
lastLeft: int,
|
||||
) -> None:
|
||||
lastIvl = -(self._delayForGrade(conf, lastLeft))
|
||||
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
|
||||
|
||||
def log() -> None:
|
||||
self.col.db.execute(
|
||||
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
||||
int(time.time() * 1000),
|
||||
card.id,
|
||||
self.col.usn(),
|
||||
ease,
|
||||
ivl,
|
||||
lastIvl,
|
||||
card.factor,
|
||||
card.time_taken(),
|
||||
type,
|
||||
)
|
||||
|
||||
try:
|
||||
log()
|
||||
except:
|
||||
# duplicate pk; retry in 10ms
|
||||
time.sleep(0.01)
|
||||
log()
|
||||
return 0
|
||||
|
||||
def removeLrn(self, ids: list[int] | None = None) -> None:
|
||||
"Remove cards from the learning queues."
|
||||
if ids:
|
||||
extra = f" and id in {ids2str(ids)}"
|
||||
else:
|
||||
# benchmarks indicate it's about 10x faster to search all decks
|
||||
# with the index than scan the table
|
||||
extra = f" and did in {ids2str(d.id for d in self.col.decks.all_names_and_ids())}"
|
||||
# review cards in relearning
|
||||
self.col.db.execute(
|
||||
f"""
|
||||
update cards set
|
||||
due = odue, queue = {QUEUE_TYPE_REV}, mod = %d, usn = %d, odue = 0
|
||||
where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CARD_TYPE_REV}
|
||||
%s
|
||||
"""
|
||||
% (int_time(), self.col.usn(), extra)
|
||||
)
|
||||
# new cards in learning
|
||||
self.forgetCards(
|
||||
self.col.db.list(
|
||||
f"select id from cards where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) %s"
|
||||
% extra
|
||||
)
|
||||
)
|
||||
pass
|
||||
|
||||
def _lrnForDeck(self, did: DeckId) -> int:
|
||||
cnt = (
|
||||
self.col.db.scalar(
|
||||
f"""
|
||||
select sum(left/1000) from
|
||||
(select left from cards where did = ? and queue = {QUEUE_TYPE_LRN} and due < ? limit ?)""",
|
||||
did,
|
||||
int_time() + self.col.conf["collapseTime"],
|
||||
self.reportLimit,
|
||||
)
|
||||
or 0
|
||||
)
|
||||
return cnt + self.col.db.scalar(
|
||||
f"""
|
||||
select count() from
|
||||
(select 1 from cards where did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN}
|
||||
and due <= ? limit ?)""",
|
||||
did,
|
||||
self.today,
|
||||
self.reportLimit,
|
||||
)
|
||||
|
||||
# Reviews
|
||||
##########################################################################
|
||||
return 0
|
||||
|
||||
def _deckRevLimit(self, did: DeckId) -> int:
|
||||
return self._deckNewLimit(did, self._deckRevLimitSingle)
|
||||
|
||||
def _resetRev(self) -> None:
|
||||
self._revQueue: list[Any] = []
|
||||
self._revDids = self.col.decks.active()[:]
|
||||
|
||||
def _fillRev(self, recursing: bool = False) -> bool:
|
||||
"True if a review card can be fetched."
|
||||
if self._revQueue:
|
||||
return True
|
||||
if not self.revCount:
|
||||
return False
|
||||
while self._revDids:
|
||||
did = self._revDids[0]
|
||||
lim = min(self.queueLimit, self._deckRevLimit(did))
|
||||
if lim:
|
||||
# fill the queue with the current did
|
||||
self._revQueue = self.col.db.list(
|
||||
f"""
|
||||
select id from cards where
|
||||
did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
|
||||
did,
|
||||
self.today,
|
||||
lim,
|
||||
)
|
||||
if self._revQueue:
|
||||
# ordering
|
||||
if self.col.decks.get(did)["dyn"]:
|
||||
# dynamic decks need due order preserved
|
||||
self._revQueue.reverse()
|
||||
else:
|
||||
# random order for regular reviews
|
||||
r = random.Random()
|
||||
r.seed(self.today)
|
||||
r.shuffle(self._revQueue)
|
||||
# is the current did empty?
|
||||
if len(self._revQueue) < lim:
|
||||
self._revDids.pop(0)
|
||||
return True
|
||||
# nothing left in the deck; move to next
|
||||
self._revDids.pop(0)
|
||||
|
||||
# if we didn't get a card but the count is non-zero,
|
||||
# we need to check again for any cards that were
|
||||
# removed from the queue but not buried
|
||||
if recursing:
|
||||
print("bug: fillRev()")
|
||||
return False
|
||||
self._reset_counts()
|
||||
self._resetRev()
|
||||
return self._fillRev(recursing=True)
|
||||
|
||||
# Answering a review card
|
||||
##########################################################################
|
||||
|
||||
def _answerRevCard(self, card: Card, ease: int) -> None:
|
||||
delay: int = 0
|
||||
if ease == BUTTON_ONE:
|
||||
delay = self._rescheduleLapse(card)
|
||||
else:
|
||||
self._rescheduleRev(card, ease)
|
||||
self._logRev(card, ease, delay, REVLOG_REV)
|
||||
|
||||
def _rescheduleLapse(self, card: Card) -> int:
|
||||
conf = self._lapseConf(card)
|
||||
card.lastIvl = card.ivl
|
||||
if self._resched(card):
|
||||
card.lapses += 1
|
||||
card.ivl = self._nextLapseIvl(card, conf)
|
||||
card.factor = max(1300, card.factor - 200)
|
||||
card.due = self.today + card.ivl
|
||||
# if it's a filtered deck, update odue as well
|
||||
if card.odid:
|
||||
card.odue = card.due
|
||||
# if suspended as a leech, nothing to do
|
||||
delay: int = 0
|
||||
if self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED:
|
||||
return delay
|
||||
# if no relearning steps, nothing to do
|
||||
if not conf["delays"]:
|
||||
return delay
|
||||
# record rev due date for later
|
||||
if not card.odue:
|
||||
card.odue = card.due
|
||||
delay = self._delayForGrade(conf, 0)
|
||||
card.due = int(delay + time.time())
|
||||
card.left = self._startingLeft(card)
|
||||
# queue 1
|
||||
if card.due < self.day_cutoff:
|
||||
self.lrnCount += card.left // 1000
|
||||
card.queue = QUEUE_TYPE_LRN
|
||||
heappush(self._lrnQueue, (card.due, card.id))
|
||||
else:
|
||||
# day learn queue
|
||||
ahead = ((card.due - self.day_cutoff) // 86400) + 1
|
||||
card.due = self.today + ahead
|
||||
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
|
||||
return delay
|
||||
return 0
|
||||
|
||||
def _nextLapseIvl(self, card: Card, conf: QueueConfig) -> int:
|
||||
return max(conf["minInt"], int(card.ivl * conf["mult"]))
|
||||
return 0
|
||||
|
||||
def _rescheduleRev(self, card: Card, ease: int) -> None: # type: ignore[override]
|
||||
# update interval
|
||||
card.lastIvl = card.ivl
|
||||
if self._resched(card):
|
||||
self._updateRevIvl(card, ease)
|
||||
# then the rest
|
||||
card.factor = max(1300, card.factor + [-150, 0, 150][ease - 2])
|
||||
card.due = self.today + card.ivl
|
||||
else:
|
||||
card.due = card.odue
|
||||
if card.odid:
|
||||
card.did = card.odid
|
||||
card.odid = DeckId(0)
|
||||
card.odue = 0
|
||||
|
||||
# Interval management
|
||||
##########################################################################
|
||||
pass
|
||||
|
||||
def _nextRevIvl(self, card: Card, ease: int) -> int: # type: ignore[override]
|
||||
"Ideal next interval for CARD, given EASE."
|
||||
delay = self._daysLate(card)
|
||||
conf = self._revConf(card)
|
||||
fct = card.factor / 1000
|
||||
ivl2 = self._constrainedIvl((card.ivl + delay // 4) * 1.2, conf, card.ivl)
|
||||
ivl3 = self._constrainedIvl((card.ivl + delay // 2) * fct, conf, ivl2)
|
||||
ivl4 = self._constrainedIvl(
|
||||
(card.ivl + delay) * fct * conf["ease4"], conf, ivl3
|
||||
)
|
||||
if ease == BUTTON_TWO:
|
||||
interval = ivl2
|
||||
elif ease == BUTTON_THREE:
|
||||
interval = ivl3
|
||||
elif ease == BUTTON_FOUR:
|
||||
interval = ivl4
|
||||
# interval capped?
|
||||
return min(interval, conf["maxIvl"])
|
||||
return 0
|
||||
|
||||
def _constrainedIvl(self, ivl: float, conf: QueueConfig, prev: int) -> int: # type: ignore[override]
|
||||
"Integer interval after interval factor and prev+1 constraints applied."
|
||||
new = ivl * conf.get("ivlFct", 1)
|
||||
return int(max(new, prev + 1))
|
||||
|
||||
def _updateRevIvl(self, card: Card, ease: int) -> None:
|
||||
idealIvl = self._nextRevIvl(card, ease)
|
||||
card.ivl = min(
|
||||
max(self._adjRevIvl(card, idealIvl), card.ivl + 1),
|
||||
self._revConf(card)["maxIvl"],
|
||||
)
|
||||
return 0
|
||||
|
||||
def _adjRevIvl(self, card: Card, idealIvl: int) -> int:
|
||||
if self._spreadRev:
|
||||
idealIvl = self._fuzzedIvl(idealIvl)
|
||||
return idealIvl
|
||||
|
||||
# Filtered deck handling
|
||||
##########################################################################
|
||||
return 0
|
||||
|
||||
def _dynIvlBoost(self, card: Card) -> int:
|
||||
assert card.odid and card.type == CARD_TYPE_REV
|
||||
assert card.factor
|
||||
elapsed = card.ivl - (card.odue - self.today)
|
||||
factor = ((card.factor / 1000) + 1.2) / 2
|
||||
ivl = int(max(card.ivl, elapsed * factor, 1))
|
||||
conf = self._revConf(card)
|
||||
return min(conf["maxIvl"], ivl)
|
||||
|
||||
# Leeches
|
||||
##########################################################################
|
||||
|
||||
def _checkLeech(self, card: Card, conf: QueueConfig) -> bool:
|
||||
"Leech handler. True if card was a leech."
|
||||
lf = conf["leechFails"]
|
||||
if not lf:
|
||||
return False
|
||||
# if over threshold or every half threshold reps after that
|
||||
if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0:
|
||||
# add a leech tag
|
||||
f = card.note()
|
||||
f.add_tag("leech")
|
||||
f.flush()
|
||||
# handle
|
||||
a = conf["leechAction"]
|
||||
if a == LEECH_SUSPEND:
|
||||
# if it has an old due, remove it from cram/relearning
|
||||
if card.odue:
|
||||
card.due = card.odue
|
||||
if card.odid:
|
||||
card.did = card.odid
|
||||
card.odue = 0
|
||||
card.odid = DeckId(0)
|
||||
card.queue = QUEUE_TYPE_SUSPENDED
|
||||
# notify UI
|
||||
hooks.card_did_leech(card)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Tools
|
||||
##########################################################################
|
||||
|
||||
def _newConf(self, card: Card) -> QueueConfig:
|
||||
conf = self._cardConf(card)
|
||||
# normal deck
|
||||
if not card.odid:
|
||||
return conf["new"]
|
||||
# dynamic deck; override some attributes, use original deck for others
|
||||
oconf = self.col.decks.config_dict_for_deck_id(card.odid)
|
||||
delays = conf["delays"] or oconf["new"]["delays"]
|
||||
return dict(
|
||||
# original deck
|
||||
ints=oconf["new"]["ints"],
|
||||
initialFactor=oconf["new"]["initialFactor"],
|
||||
bury=oconf["new"].get("bury", True),
|
||||
# overrides
|
||||
delays=delays,
|
||||
order=NEW_CARDS_DUE,
|
||||
perDay=self.reportLimit,
|
||||
)
|
||||
|
||||
def _lapseConf(self, card: Card) -> QueueConfig:
|
||||
conf = self._cardConf(card)
|
||||
# normal deck
|
||||
if not card.odid:
|
||||
return conf["lapse"]
|
||||
# dynamic deck; override some attributes, use original deck for others
|
||||
oconf = self.col.decks.config_dict_for_deck_id(card.odid)
|
||||
delays = conf["delays"] or oconf["lapse"]["delays"]
|
||||
return dict(
|
||||
# original deck
|
||||
minInt=oconf["lapse"]["minInt"],
|
||||
leechFails=oconf["lapse"]["leechFails"],
|
||||
leechAction=oconf["lapse"]["leechAction"],
|
||||
mult=oconf["lapse"]["mult"],
|
||||
# overrides
|
||||
delays=delays,
|
||||
resched=conf["resched"],
|
||||
)
|
||||
return 0
|
||||
|
||||
def _resched(self, card: Card) -> bool:
|
||||
conf = self._cardConf(card)
|
||||
if not conf["dyn"]:
|
||||
return True
|
||||
return conf["resched"]
|
||||
|
||||
# Deck finished state
|
||||
##########################################################################
|
||||
|
||||
def have_buried(self) -> bool:
|
||||
sdids = self._deck_limit()
|
||||
cnt = self.col.db.scalar(
|
||||
f"select 1 from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s limit 1"
|
||||
% sdids
|
||||
)
|
||||
return bool(cnt)
|
||||
|
||||
# Next time reports
|
||||
##########################################################################
|
||||
|
||||
def nextIvl(self, card: Card, ease: int) -> float:
|
||||
"Return the next interval for CARD, in seconds."
|
||||
if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
|
||||
return self._nextLrnIvl(card, ease)
|
||||
elif ease == BUTTON_ONE:
|
||||
# lapsed
|
||||
conf = self._lapseConf(card)
|
||||
if conf["delays"]:
|
||||
return conf["delays"][0] * 60
|
||||
return self._nextLapseIvl(card, conf) * 86400
|
||||
else:
|
||||
# review
|
||||
return self._nextRevIvl(card, ease) * 86400
|
||||
|
||||
# this isn't easily extracted from the learn code
|
||||
def _nextLrnIvl(self, card: Card, ease: int) -> float:
|
||||
if card.queue == 0:
|
||||
card.left = self._startingLeft(card)
|
||||
conf = self._lrnConf(card)
|
||||
if ease == BUTTON_ONE:
|
||||
# fail
|
||||
return self._delayForGrade(conf, len(conf["delays"]))
|
||||
elif ease == BUTTON_THREE:
|
||||
# early removal
|
||||
if not self._resched(card):
|
||||
return 0
|
||||
return self._graduatingIvl(card, conf, True, adj=False) * 86400
|
||||
else:
|
||||
left = card.left % 1000 - 1
|
||||
if left <= 0:
|
||||
# graduate
|
||||
if not self._resched(card):
|
||||
return 0
|
||||
return self._graduatingIvl(card, conf, False, adj=False) * 86400
|
||||
else:
|
||||
return self._delayForGrade(conf, left)
|
||||
return False
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -356,7 +356,7 @@ class DeckBrowser:
|
|||
<center>
|
||||
<div class=callout>
|
||||
<div>
|
||||
{tr.scheduling_update_soon()}
|
||||
{tr.scheduling_update_required()}
|
||||
</div>
|
||||
<div>
|
||||
<button onclick='pycmd("v2upgrade")'>
|
||||
|
|
|
@ -131,6 +131,9 @@ class Reviewer:
|
|||
hooks.card_did_leech.append(self.onLeech)
|
||||
|
||||
def show(self) -> None:
|
||||
if self.mw.col.sched_ver() == 1:
|
||||
self.mw.moveToState("deckBrowser")
|
||||
return
|
||||
self.mw.setStateShortcuts(self._shortcutKeys()) # type: ignore
|
||||
self.web.set_bridge_command(self._linkHandler, self)
|
||||
self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
|
||||
|
|
Loading…
Reference in a new issue