Skip to content

Commit

Permalink
Feat/flatten (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
L-M-Sherlock committed Aug 11, 2024
1 parent c7aea54 commit 75ce5c7
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 2 deletions.
5 changes: 5 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .schedule.reschedule import reschedule
from .schedule.postpone import postpone
from .schedule.advance import advance
from .schedule.flatten import flatten
from .schedule.reset import clear_custom_data
from .schedule.disperse_siblings import disperse_siblings
from .schedule.easy_days import (
Expand Down Expand Up @@ -143,6 +144,9 @@ def reschedule_recent(did):
menu_advance = build_action(advance, "Advance cards in all decks")
add_action_to_gear(advance, "Advance cards")

menu_flatten = build_action(flatten, "Flatten reviews in all decks (experimental)")
add_action_to_gear(flatten, "Flatten cards")

menu_reset = build_action(clear_custom_data, "Clear custom data in all cards")

menu_disperse_siblings = build_action(disperse_siblings, "Disperse all siblings")
Expand Down Expand Up @@ -192,6 +196,7 @@ def sponsor(did=None):
menu_for_helper.addAction(menu_reschedule_recent)
menu_for_helper.addAction(menu_postpone)
menu_for_helper.addAction(menu_advance)
menu_for_helper.addAction(menu_flatten)
menu_for_helper.addAction(menu_reset)
menu_for_helper.addAction(menu_disperse_siblings)
menu_for_helper.addSeparator()
Expand Down
182 changes: 182 additions & 0 deletions schedule/flatten.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
from collections import defaultdict
from ..configuration import Config
from ..utils import *
from anki.decks import DeckManager
from anki.utils import ids2str


def get_desired_flatten_limit_with_response(did):
inquire_text = "Enter the maximum number of reviews you want in the future.\n"
info_text = (
"This feature only affects the cards that have been scheduled by FSRS.\n"
)
warning_text = "This feature doesn't respect maximum interval settings, and may reduce your true retention significantly.\n"
(s, r) = getText(inquire_text + info_text + warning_text, default="100")
if r:
return (RepresentsInt(s), r)
return (None, r)


def flatten(did):
if not mw.col.get_config("fsrs"):
tooltip(FSRS_ENABLE_WARNING)
return

(desired_flatten_limit, resp) = get_desired_flatten_limit_with_response(did)
if desired_flatten_limit is None:
if resp:
showWarning("Please enter the number of cards you want to flatten.")
return
else:
if desired_flatten_limit <= 0:
showWarning("Please enter a positive integer.")
return

start_time = time.time()

def on_done(future):
finish_text = future.result()
mw.progress.finish()
tooltip(f"{finish_text} in {time.time() - start_time:.2f} seconds")
mw.reset()

fut = mw.taskman.run_in_background(
lambda: flatten_background(did, desired_flatten_limit), on_done
)
return fut


def flatten_background(did, desired_flatten_limit):
config = Config()
config.load()

easy_days = []
if config.load_balance:
easy_days = config.easy_days

DM = DeckManager(mw.col)
if did is not None:
did_list = ids2str(DM.deck_and_child_ids(did))

today = mw.col.sched.today
current_date = sched_current_date()
true_due = "CASE WHEN odid==0 THEN due ELSE odue END"

cards_exceed_future = mw.col.db.all(
f"""
SELECT rc.id, rc.true_due, rc.stability
FROM (
SELECT id,
true_due,
stability,
ROW_NUMBER() OVER (
PARTITION BY true_due
ORDER BY stability
) AS rank
FROM (
SELECT id,
{true_due} AS true_due,
json_extract(data, '$.s') AS stability
FROM cards
WHERE true_due >= {today}
AND data != ''
AND json_extract(data, '$.s') IS NOT NULL
AND queue = {QUEUE_TYPE_REV}
{"AND did IN %s" % did_list if did is not None else ""}
) AS subquery
) AS rc
JOIN (
SELECT {true_due} AS true_due
FROM cards
WHERE true_due >= {today}
AND queue = {QUEUE_TYPE_REV}
AND data != ''
AND json_extract(data, '$.s') IS NOT NULL
{"AND did IN %s" % did_list if did is not None else ""}
GROUP BY true_due
HAVING COUNT(*) > {desired_flatten_limit}
) AS overdue ON rc.true_due = overdue.true_due
WHERE rc.rank > {desired_flatten_limit}
ORDER BY rc.true_due
"""
)

cards_backlog = mw.col.db.all(
f"""
SELECT id,
{true_due} AS true_due,
json_extract(data, '$.s') AS stability
FROM cards
WHERE true_due < { today }
AND data != ''
AND json_extract(data, '$.s') IS NOT NULL
AND queue = {QUEUE_TYPE_REV}
{"AND did IN %s" % did_list if did is not None else ""}
ORDER BY stability
"""
)

cards_to_flatten = cards_backlog + cards_exceed_future
total_cnt = len(cards_to_flatten)

due_cnt_per_day = defaultdict(
int,
{
day: cnt
for day, cnt in mw.col.db.all(
f"""SELECT {true_due} AS true_due, count()
FROM cards
WHERE true_due >= { today }
AND queue = {QUEUE_TYPE_REV}
{"AND did IN %s" % did_list if did is not None else ""}
GROUP BY {true_due}"""
)
},
)

undo_entry = mw.col.add_custom_undo_entry("flatten")
mw.taskman.run_on_main(
lambda: mw.progress.start(label="Flattening", max=total_cnt, immediate=True)
)
cnt = 0
cancelled = False
flattened_cards = []

for new_due in range(today, today + 36500):
if cancelled:
break
rest_cnt = len(cards_to_flatten) - cnt
if rest_cnt <= 0:
break
due_date = current_date + timedelta(days=new_due - today)
if config.load_balance and due_date.weekday() in easy_days:
continue
due_cnt = due_cnt_per_day[new_due]
if due_cnt > desired_flatten_limit:
continue
quota = desired_flatten_limit - due_cnt
start_index = cnt
end_index = cnt + min(quota, rest_cnt)
for cid, due, ivl in cards_to_flatten[start_index:end_index]:
card = mw.col.get_card(cid)
last_review = get_last_review_date(card)
new_ivl = new_due - last_review
card = update_card_due_ivl(card, new_ivl)
write_custom_data(card, "v", "flatten")
flattened_cards.append(card)
cnt += 1
if cnt % 500 == 0:
mw.taskman.run_on_main(
lambda: mw.progress.update(
label=f"{cnt}/{total_cnt} cards flattened",
value=cnt,
max=total_cnt,
)
)
if mw.progress.want_cancel():
cancelled = True

mw.col.update_cards(flattened_cards)
mw.col.merge_undo_entries(undo_entry)
finish_text = f"{cnt} cards flattened"
return finish_text
6 changes: 4 additions & 2 deletions schedule/reschedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ def reschedule_background(
)
cnt = 0
cancelled = False
rescheduled_cards = []
for cid, _, _, desired_retention, maximum_interval in cards:
if cancelled:
break
Expand All @@ -294,8 +295,7 @@ def reschedule_background(
card = reschedule_card(cid, fsrs, filter_flag)
if card is None:
continue
mw.col.update_card(card)
mw.col.merge_undo_entries(undo_entry)
rescheduled_cards.append(card)
cnt += 1
if cnt % 500 == 0:
mw.taskman.run_on_main(
Expand All @@ -308,6 +308,8 @@ def reschedule_background(
if mw.progress.want_cancel():
cancelled = True

mw.col.update_cards(rescheduled_cards)
mw.col.merge_undo_entries(undo_entry)
finish_text = f"{cnt} cards rescheduled"

if config.auto_disperse_after_reschedule:
Expand Down

0 comments on commit 75ce5c7

Please sign in to comment.