Anki/rslib/src/scheduler/upgrade.rs
2021-02-23 17:35:20 +10:00

186 lines
6 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::HashMap;
use crate::{
card::{CardQueue, CardType},
config::SchedulerVersion,
prelude::*,
search::SortMode,
};
use super::cutoff::local_minutes_west_for_stamp;
struct V1FilteredDeckInfo {
/// True if the filtered deck had rescheduling enabled.
reschedule: bool,
/// If the filtered deck had custom steps enabled, `original_step_count`
/// contains the step count of the home deck, which will be used to ensure
/// the remaining steps of the card are not out of bounds.
original_step_count: Option<u32>,
}
impl Card {
/// Update relearning cards and cards in filtered decks.
/// `filtered_info` should be provided if card is in a filtered deck.
fn upgrade_to_v2(&mut self, filtered_info: Option<V1FilteredDeckInfo>) {
// relearning cards have their own type
if self.ctype == CardType::Review
&& matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn)
{
self.ctype = CardType::Relearn;
}
// filtered deck handling
if let Some(info) = filtered_info {
// cap remaining count to home deck
if let Some(step_count) = info.original_step_count {
self.remaining_steps = self.remaining_steps.min(step_count);
}
if !info.reschedule {
// preview cards start in the review queue in v2
if self.queue == CardQueue::New {
self.queue = CardQueue::Review;
}
// to ensure learning cards are reset to new on exit, we must
// make them new now
if self.ctype == CardType::Learn {
self.queue = CardQueue::PreviewRepeat;
self.ctype = CardType::New;
}
}
}
}
}
fn get_filter_info_for_card(
card: &Card,
decks: &HashMap<DeckID, Deck>,
configs: &HashMap<DeckConfID, DeckConf>,
) -> Option<V1FilteredDeckInfo> {
if card.original_deck_id.0 == 0 {
None
} else {
let (had_custom_steps, reschedule) = if let Some(deck) = decks.get(&card.deck_id) {
if let DeckKind::Filtered(filtered) = &deck.kind {
(!filtered.delays.is_empty(), filtered.reschedule)
} else {
// not a filtered deck, give up
return None;
}
} else {
// missing filtered deck, give up
return None;
};
let original_step_count = if had_custom_steps {
let home_conf_id = decks
.get(&card.original_deck_id)
.and_then(|deck| deck.config_id())
.unwrap_or(DeckConfID(1));
Some(
configs
.get(&home_conf_id)
.map(|config| {
if card.ctype == CardType::Review {
config.inner.relearn_steps.len()
} else {
config.inner.learn_steps.len()
}
})
.unwrap_or(0) as u32,
)
} else {
None
};
Some(V1FilteredDeckInfo {
reschedule,
original_step_count,
})
}
}
impl Collection {
/// Expects an existing transaction. No-op if already on v2.
pub(crate) fn upgrade_to_v2_scheduler(&mut self) -> Result<()> {
if self.scheduler_version() == SchedulerVersion::V2 {
// nothing to do
return Ok(());
}
self.storage.upgrade_revlog_to_v2()?;
self.upgrade_cards_to_v2()?;
self.set_scheduler_version_config_key(SchedulerVersion::V2)?;
// enable new timezone code by default
let created = self.storage.creation_stamp()?;
if self.get_creation_utc_offset().is_none() {
self.set_creation_utc_offset(Some(local_minutes_west_for_stamp(created.0)))?;
}
// force full sync
self.storage.set_schema_modified()
}
fn upgrade_cards_to_v2(&mut self) -> Result<()> {
let count = self.search_cards_into_table(
// can't add 'is:learn' here, as it matches on card type, not card queue
"deck:filtered OR is:review",
SortMode::NoOrder,
)?;
if count > 0 {
let decks = self.storage.get_decks_map()?;
let configs = self.storage.get_deck_config_map()?;
self.storage.for_each_card_in_search(|mut card| {
let filtered_info = get_filter_info_for_card(&card, &decks, &configs);
card.upgrade_to_v2(filtered_info);
self.storage.update_card(&card)
})?;
}
self.storage.clear_searched_cards_table()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn v2_card() {
let mut c = Card::default();
// relearning cards should be reclassified
c.ctype = CardType::Review;
c.queue = CardQueue::DayLearn;
c.upgrade_to_v2(None);
assert_eq!(c.ctype, CardType::Relearn);
// check step capping
c.remaining_steps = 5005;
c.upgrade_to_v2(Some(V1FilteredDeckInfo {
reschedule: true,
original_step_count: Some(2),
}));
assert_eq!(c.remaining_steps, 2);
// with rescheduling off, relearning cards don't need changing
c.upgrade_to_v2(Some(V1FilteredDeckInfo {
reschedule: false,
original_step_count: None,
}));
assert_eq!(c.ctype, CardType::Relearn);
assert_eq!(c.queue, CardQueue::DayLearn);
// but learning cards are reset to new
c.ctype = CardType::Learn;
c.upgrade_to_v2(Some(V1FilteredDeckInfo {
reschedule: false,
original_step_count: None,
}));
assert_eq!(c.ctype, CardType::New);
assert_eq!(c.queue, CardQueue::PreviewRepeat);
}
}