mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Support rescheduling on weight/retention change
This commit is contained in:
parent
0ef28853fd
commit
072cd37b42
14 changed files with 165 additions and 42 deletions
|
@ -339,6 +339,7 @@ deck-config-fsrs-on-all-clients =
|
||||||
not work correctly if one of your clients is older.
|
not work correctly if one of your clients is older.
|
||||||
deck-config-set-optimal-retention = Set desired retention to { $num }
|
deck-config-set-optimal-retention = Set desired retention to { $num }
|
||||||
deck-config-complete = { $num }% complete.
|
deck-config-complete = { $num }% complete.
|
||||||
|
deck-config-reschedule-cards-on-change = Reschedule cards on change
|
||||||
|
|
||||||
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
|
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,9 @@ message DeckConfig {
|
||||||
bool bury_reviews = 28;
|
bool bury_reviews = 28;
|
||||||
bool bury_interday_learning = 29;
|
bool bury_interday_learning = 29;
|
||||||
|
|
||||||
float desired_retention = 37; // for fsrs
|
// for fsrs
|
||||||
|
float desired_retention = 37;
|
||||||
|
bool reschedule_fsrs_cards = 39;
|
||||||
|
|
||||||
bytes other = 255;
|
bytes other = 255;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
|
||||||
fsrs_weights: vec![],
|
fsrs_weights: vec![],
|
||||||
desired_retention: 0.9,
|
desired_retention: 0.9,
|
||||||
other: Vec::new(),
|
other: Vec::new(),
|
||||||
|
reschedule_fsrs_cards: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Default for DeckConfig {
|
impl Default for DeckConfig {
|
||||||
|
|
|
@ -71,6 +71,8 @@ pub struct DeckConfSchema11 {
|
||||||
desired_retention: f32,
|
desired_retention: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
stop_timer_on_answer: bool,
|
stop_timer_on_answer: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
reschedule_fsrs_cards: bool,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
other: HashMap<String, Value>,
|
other: HashMap<String, Value>,
|
||||||
|
@ -260,6 +262,7 @@ impl Default for DeckConfSchema11 {
|
||||||
bury_interday_learning: false,
|
bury_interday_learning: false,
|
||||||
fsrs_weights: vec![],
|
fsrs_weights: vec![],
|
||||||
desired_retention: 0.9,
|
desired_retention: 0.9,
|
||||||
|
reschedule_fsrs_cards: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,6 +334,7 @@ impl From<DeckConfSchema11> for DeckConfig {
|
||||||
bury_interday_learning: c.bury_interday_learning,
|
bury_interday_learning: c.bury_interday_learning,
|
||||||
fsrs_weights: c.fsrs_weights,
|
fsrs_weights: c.fsrs_weights,
|
||||||
desired_retention: c.desired_retention,
|
desired_retention: c.desired_retention,
|
||||||
|
reschedule_fsrs_cards: c.reschedule_fsrs_cards,
|
||||||
other: other_bytes,
|
other: other_bytes,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -425,6 +429,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
||||||
bury_interday_learning: i.bury_interday_learning,
|
bury_interday_learning: i.bury_interday_learning,
|
||||||
fsrs_weights: i.fsrs_weights,
|
fsrs_weights: i.fsrs_weights,
|
||||||
desired_retention: i.desired_retention,
|
desired_retention: i.desired_retention,
|
||||||
|
reschedule_fsrs_cards: i.reschedule_fsrs_cards,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -449,6 +454,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
|
||||||
"fsrsWeights",
|
"fsrsWeights",
|
||||||
"desiredRetention",
|
"desiredRetention",
|
||||||
"stopTimerOnAnswer",
|
"stopTimerOnAnswer",
|
||||||
|
"rescheduleFsrsCards"
|
||||||
};
|
};
|
||||||
|
|
||||||
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {
|
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {
|
||||||
|
|
|
@ -16,7 +16,7 @@ use fsrs::DEFAULT_WEIGHTS;
|
||||||
use crate::config::StringKey;
|
use crate::config::StringKey;
|
||||||
use crate::decks::NormalDeck;
|
use crate::decks::NormalDeck;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::scheduler::fsrs::memory_state::WeightsAndDesiredRetention;
|
use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
|
||||||
use crate::search::JoinSearches;
|
use crate::search::JoinSearches;
|
||||||
use crate::search::SearchNode;
|
use crate::search::SearchNode;
|
||||||
|
|
||||||
|
@ -123,19 +123,19 @@ impl Collection {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_deck_configs_inner(&mut self, mut input: UpdateDeckConfigsRequest) -> Result<()> {
|
fn update_deck_configs_inner(&mut self, mut req: UpdateDeckConfigsRequest) -> Result<()> {
|
||||||
require!(!input.configs.is_empty(), "config not provided");
|
require!(!req.configs.is_empty(), "config not provided");
|
||||||
let configs_before_update = self.storage.get_deck_config_map()?;
|
let configs_before_update = self.storage.get_deck_config_map()?;
|
||||||
let mut configs_after_update = configs_before_update.clone();
|
let mut configs_after_update = configs_before_update.clone();
|
||||||
|
|
||||||
// handle removals first
|
// handle removals first
|
||||||
for dcid in &input.removed_config_ids {
|
for dcid in &req.removed_config_ids {
|
||||||
self.remove_deck_config_inner(*dcid)?;
|
self.remove_deck_config_inner(*dcid)?;
|
||||||
configs_after_update.remove(dcid);
|
configs_after_update.remove(dcid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add/update provided configs
|
// add/update provided configs
|
||||||
for conf in &mut input.configs {
|
for conf in &mut req.configs {
|
||||||
let weight_len = conf.inner.fsrs_weights.len();
|
let weight_len = conf.inner.fsrs_weights.len();
|
||||||
if weight_len != 0 && weight_len != 17 {
|
if weight_len != 0 && weight_len != 17 {
|
||||||
return Err(AnkiError::FsrsWeightsInvalid);
|
return Err(AnkiError::FsrsWeightsInvalid);
|
||||||
|
@ -145,11 +145,11 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get selected deck and possibly children
|
// get selected deck and possibly children
|
||||||
let selected_deck_ids: HashSet<_> = if input.apply_to_children {
|
let selected_deck_ids: HashSet<_> = if req.apply_to_children {
|
||||||
let deck = self
|
let deck = self
|
||||||
.storage
|
.storage
|
||||||
.get_deck(input.target_deck_id)?
|
.get_deck(req.target_deck_id)?
|
||||||
.or_not_found(input.target_deck_id)?;
|
.or_not_found(req.target_deck_id)?;
|
||||||
self.storage
|
self.storage
|
||||||
.child_decks(&deck)?
|
.child_decks(&deck)?
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -157,18 +157,18 @@ impl Collection {
|
||||||
.map(|d| d.id)
|
.map(|d| d.id)
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
[input.target_deck_id].iter().cloned().collect()
|
[req.target_deck_id].iter().cloned().collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// loop through all normal decks
|
// loop through all normal decks
|
||||||
let usn = self.usn()?;
|
let usn = self.usn()?;
|
||||||
let today = self.timing_today()?.days_elapsed;
|
let today = self.timing_today()?.days_elapsed;
|
||||||
let selected_config = input.configs.last().unwrap();
|
let selected_config = req.configs.last().unwrap();
|
||||||
let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<SearchNode>> =
|
let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<SearchNode>> =
|
||||||
Default::default();
|
Default::default();
|
||||||
let fsrs_toggled = self.get_config_bool(BoolKey::Fsrs) != input.fsrs;
|
let fsrs_toggled = self.get_config_bool(BoolKey::Fsrs) != req.fsrs;
|
||||||
if fsrs_toggled {
|
if fsrs_toggled {
|
||||||
self.set_config_bool_inner(BoolKey::Fsrs, input.fsrs)?;
|
self.set_config_bool_inner(BoolKey::Fsrs, req.fsrs)?;
|
||||||
}
|
}
|
||||||
for deck in self.storage.get_all_decks()? {
|
for deck in self.storage.get_all_decks()? {
|
||||||
if let Ok(normal) = deck.normal() {
|
if let Ok(normal) = deck.normal() {
|
||||||
|
@ -181,6 +181,7 @@ impl Collection {
|
||||||
.map(|c| c.inner.new_card_insert_order())
|
.map(|c| c.inner.new_card_insert_order())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights);
|
let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights);
|
||||||
|
let previous_retention = previous_config.map(|c| c.inner.desired_retention);
|
||||||
|
|
||||||
// if a selected (sub)deck, or its old config was removed, update deck to point
|
// if a selected (sub)deck, or its old config was removed, update deck to point
|
||||||
// to new config
|
// to new config
|
||||||
|
@ -189,7 +190,7 @@ impl Collection {
|
||||||
{
|
{
|
||||||
let mut updated = deck.clone();
|
let mut updated = deck.clone();
|
||||||
updated.normal_mut()?.config_id = selected_config.id.0;
|
updated.normal_mut()?.config_id = selected_config.id.0;
|
||||||
update_deck_limits(updated.normal_mut()?, &input.limits, today);
|
update_deck_limits(updated.normal_mut()?, &req.limits, today);
|
||||||
self.update_deck_inner(&mut updated, deck, usn)?;
|
self.update_deck_inner(&mut updated, deck, usn)?;
|
||||||
selected_config.id
|
selected_config.id
|
||||||
} else {
|
} else {
|
||||||
|
@ -207,7 +208,11 @@ impl Collection {
|
||||||
|
|
||||||
// if weights differ, memory state needs to be recomputed
|
// if weights differ, memory state needs to be recomputed
|
||||||
let current_weights = current_config.map(|c| &c.inner.fsrs_weights);
|
let current_weights = current_config.map(|c| &c.inner.fsrs_weights);
|
||||||
if fsrs_toggled || previous_weights != current_weights {
|
let current_retention = current_config.map(|c| c.inner.desired_retention);
|
||||||
|
if fsrs_toggled
|
||||||
|
|| previous_weights != current_weights
|
||||||
|
|| previous_retention != current_retention
|
||||||
|
{
|
||||||
decks_needing_memory_recompute
|
decks_needing_memory_recompute
|
||||||
.entry(current_config_id)
|
.entry(current_config_id)
|
||||||
.or_default()
|
.or_default()
|
||||||
|
@ -219,13 +224,18 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decks_needing_memory_recompute.is_empty() {
|
if !decks_needing_memory_recompute.is_empty() {
|
||||||
let input: Vec<(Option<WeightsAndDesiredRetention>, Vec<SearchNode>)> =
|
let input: Vec<(Option<UpdateMemoryStateRequest>, Vec<SearchNode>)> =
|
||||||
decks_needing_memory_recompute
|
decks_needing_memory_recompute
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(conf_id, search)| {
|
.map(|(conf_id, search)| {
|
||||||
let weights = configs_after_update.get(&conf_id).and_then(|c| {
|
let weights = configs_after_update.get(&conf_id).and_then(|c| {
|
||||||
if input.fsrs {
|
if req.fsrs {
|
||||||
Some((c.inner.fsrs_weights.clone(), c.inner.desired_retention))
|
Some(UpdateMemoryStateRequest {
|
||||||
|
weights: c.inner.fsrs_weights.clone(),
|
||||||
|
desired_retention: c.inner.desired_retention,
|
||||||
|
max_interval: c.inner.maximum_review_interval,
|
||||||
|
reschedule: c.inner.reschedule_fsrs_cards,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -236,10 +246,10 @@ impl Collection {
|
||||||
self.update_memory_state(input)?;
|
self.update_memory_state(input)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_config_string_inner(StringKey::CardStateCustomizer, &input.card_state_customizer)?;
|
self.set_config_string_inner(StringKey::CardStateCustomizer, &req.card_state_customizer)?;
|
||||||
self.set_config_bool_inner(
|
self.set_config_bool_inner(
|
||||||
BoolKey::NewCardsIgnoreReviewLimit,
|
BoolKey::NewCardsIgnoreReviewLimit,
|
||||||
input.new_cards_ignore_review_limit,
|
req.new_cards_ignore_review_limit,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -89,17 +89,23 @@ impl Collection {
|
||||||
pub(crate) fn log_manually_scheduled_review(
|
pub(crate) fn log_manually_scheduled_review(
|
||||||
&mut self,
|
&mut self,
|
||||||
card: &Card,
|
card: &Card,
|
||||||
original: &Card,
|
original_interval: u32,
|
||||||
usn: Usn,
|
usn: Usn,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let ease_factor = u32::try_from(
|
||||||
|
card.memory_state
|
||||||
|
.map(|s| ((s.difficulty_shifted() * 1000.) as u16))
|
||||||
|
.unwrap_or(card.ease_factor),
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
let entry = RevlogEntry {
|
let entry = RevlogEntry {
|
||||||
id: RevlogId::new(),
|
id: RevlogId::new(),
|
||||||
cid: card.id,
|
cid: card.id,
|
||||||
usn,
|
usn,
|
||||||
button_chosen: 0,
|
button_chosen: 0,
|
||||||
interval: i32::try_from(card.interval).unwrap_or(i32::MAX),
|
interval: i32::try_from(card.interval).unwrap_or(i32::MAX),
|
||||||
last_interval: i32::try_from(original.interval).unwrap_or(i32::MAX),
|
last_interval: i32::try_from(original_interval).unwrap_or(i32::MAX),
|
||||||
ease_factor: u32::from(card.ease_factor),
|
ease_factor,
|
||||||
taken_millis: 0,
|
taken_millis: 0,
|
||||||
review_kind: RevlogReviewKind::Manual,
|
review_kind: RevlogReviewKind::Manual,
|
||||||
};
|
};
|
||||||
|
|
|
@ -367,7 +367,7 @@ impl Collection {
|
||||||
let days_elapsed = self
|
let days_elapsed = self
|
||||||
.storage
|
.storage
|
||||||
.time_of_last_review(card.id)?
|
.time_of_last_review(card.id)?
|
||||||
.map(|ts| ts.elapsed_days_since(timing.next_day_at))
|
.map(|ts| timing.next_day_at.elapsed_days_since(ts))
|
||||||
.unwrap_or_default() as u32;
|
.unwrap_or_default() as u32;
|
||||||
Some(fsrs.next_states(
|
Some(fsrs.next_states(
|
||||||
card.memory_state.map(Into::into),
|
card.memory_state.map(Into::into),
|
||||||
|
@ -464,6 +464,12 @@ pub mod test_helpers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Card {
|
||||||
|
pub(crate) fn get_fuzz_factor(&self) -> Option<f32> {
|
||||||
|
get_fuzz_factor(get_fuzz_seed(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return a consistent seed for a given card at a given number of reps.
|
/// Return a consistent seed for a given card at a given number of reps.
|
||||||
/// If in test environment, disable fuzzing.
|
/// If in test environment, disable fuzzing.
|
||||||
fn get_fuzz_seed(card: &Card) -> Option<u64> {
|
fn get_fuzz_seed(card: &Card) -> Option<u64> {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anki_proto::scheduler::ComputeMemoryStateResponse;
|
use anki_proto::scheduler::ComputeMemoryStateResponse;
|
||||||
use fsrs::FSRSItem;
|
use fsrs::FSRSItem;
|
||||||
use fsrs::FSRS;
|
use fsrs::FSRS;
|
||||||
|
@ -11,6 +13,7 @@ use crate::prelude::*;
|
||||||
use crate::revlog::RevlogEntry;
|
use crate::revlog::RevlogEntry;
|
||||||
use crate::scheduler::fsrs::weights::single_card_revlog_to_items;
|
use crate::scheduler::fsrs::weights::single_card_revlog_to_items;
|
||||||
use crate::scheduler::fsrs::weights::Weights;
|
use crate::scheduler::fsrs::weights::Weights;
|
||||||
|
use crate::scheduler::states::fuzz::with_review_fuzz;
|
||||||
use crate::search::JoinSearches;
|
use crate::search::JoinSearches;
|
||||||
use crate::search::Negated;
|
use crate::search::Negated;
|
||||||
use crate::search::SearchNode;
|
use crate::search::SearchNode;
|
||||||
|
@ -22,7 +25,13 @@ pub struct ComputeMemoryProgress {
|
||||||
pub total_cards: u32,
|
pub total_cards: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type WeightsAndDesiredRetention = (Weights, f32);
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct UpdateMemoryStateRequest {
|
||||||
|
pub weights: Weights,
|
||||||
|
pub desired_retention: f32,
|
||||||
|
pub max_interval: u32,
|
||||||
|
pub reschedule: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
/// For each provided set of weights, locate cards with the provided search,
|
/// For each provided set of weights, locate cards with the provided search,
|
||||||
|
@ -32,26 +41,71 @@ impl Collection {
|
||||||
/// memory state should be removed.
|
/// memory state should be removed.
|
||||||
pub(crate) fn update_memory_state(
|
pub(crate) fn update_memory_state(
|
||||||
&mut self,
|
&mut self,
|
||||||
entries: Vec<(Option<WeightsAndDesiredRetention>, Vec<SearchNode>)>,
|
entries: Vec<(Option<UpdateMemoryStateRequest>, Vec<SearchNode>)>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let usn = self.usn()?;
|
let usn = self.usn()?;
|
||||||
for (weights_and_desired_retention, search) in entries {
|
for (req, search) in entries {
|
||||||
let search = SearchBuilder::any(search.into_iter())
|
let search = SearchBuilder::any(search.into_iter())
|
||||||
.and(SearchNode::State(StateKind::New).negated());
|
.and(SearchNode::State(StateKind::New).negated());
|
||||||
let revlog = self.revlog_for_srs(search)?;
|
let revlog = self.revlog_for_srs(search)?;
|
||||||
|
let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or_default();
|
||||||
|
let last_reviews = if reschedule {
|
||||||
|
Some(get_last_reviews(&revlog))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let items = fsrs_items_for_memory_state(revlog, timing.next_day_at);
|
let items = fsrs_items_for_memory_state(revlog, timing.next_day_at);
|
||||||
let desired_retention = weights_and_desired_retention.as_ref().map(|w| w.1);
|
let desired_retention = req.as_ref().map(|w| w.desired_retention);
|
||||||
let fsrs = FSRS::new(weights_and_desired_retention.as_ref().map(|w| &w.0[..]))?;
|
let fsrs = FSRS::new(req.as_ref().map(|w| &w.weights[..]))?;
|
||||||
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
|
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
|
||||||
progress.update(false, |s| s.total_cards = items.len() as u32)?;
|
progress.update(false, |s| s.total_cards = items.len() as u32)?;
|
||||||
for (idx, (card_id, item)) in items.into_iter().enumerate() {
|
for (idx, (card_id, item)) in items.into_iter().enumerate() {
|
||||||
progress.update(true, |state| state.current_cards = idx as u32 + 1)?;
|
progress.update(true, |state| state.current_cards = idx as u32 + 1)?;
|
||||||
let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?;
|
let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?;
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
if weights_and_desired_retention.is_some() {
|
if let Some(req) = &req {
|
||||||
card.set_memory_state(&fsrs, item);
|
card.set_memory_state(&fsrs, item.clone());
|
||||||
card.desired_retention = desired_retention;
|
card.desired_retention = desired_retention;
|
||||||
|
// if rescheduling
|
||||||
|
if let Some(reviews) = &last_reviews {
|
||||||
|
// and we have a last review time for the card
|
||||||
|
if let Some(last_review) = reviews.get(&card.id) {
|
||||||
|
let days_elapsed =
|
||||||
|
timing.next_day_at.elapsed_days_since(*last_review) as i32;
|
||||||
|
// and the card's not new
|
||||||
|
if let Some(state) = &card.memory_state {
|
||||||
|
// or in (re)learning
|
||||||
|
if card.ctype == CardType::Review {
|
||||||
|
// reschedule it
|
||||||
|
let original_interval = card.interval;
|
||||||
|
let interval = fsrs.next_interval(
|
||||||
|
Some(state.stability),
|
||||||
|
card.desired_retention.unwrap(),
|
||||||
|
0,
|
||||||
|
) as f32;
|
||||||
|
card.interval = with_review_fuzz(
|
||||||
|
card.get_fuzz_factor(),
|
||||||
|
interval,
|
||||||
|
1,
|
||||||
|
req.max_interval,
|
||||||
|
);
|
||||||
|
let due = if card.original_due != 0 {
|
||||||
|
&mut card.original_due
|
||||||
|
} else {
|
||||||
|
&mut card.due
|
||||||
|
};
|
||||||
|
*due = (timing.days_elapsed as i32) - days_elapsed
|
||||||
|
+ card.interval as i32;
|
||||||
|
self.log_manually_scheduled_review(
|
||||||
|
&card,
|
||||||
|
original_interval,
|
||||||
|
usn,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
card.memory_state = None;
|
card.memory_state = None;
|
||||||
card.desired_retention = None;
|
card.desired_retention = None;
|
||||||
|
@ -117,6 +171,25 @@ pub(crate) fn fsrs_items_for_memory_state(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a map of cards to the last time they were reviewed.
|
||||||
|
fn get_last_reviews(revlogs: &[RevlogEntry]) -> HashMap<CardId, TimestampSecs> {
|
||||||
|
let mut out = HashMap::new();
|
||||||
|
revlogs
|
||||||
|
.iter()
|
||||||
|
.group_by(|r| r.cid)
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|(card_id, group)| {
|
||||||
|
let mut last_ts = TimestampSecs::zero();
|
||||||
|
for entry in group.into_iter().filter(|r| r.button_chosen >= 1) {
|
||||||
|
last_ts = entry.id.as_secs();
|
||||||
|
}
|
||||||
|
if last_ts != TimestampSecs::zero() {
|
||||||
|
out.insert(card_id, last_ts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// When calculating memory state, only the last FSRSItem is required.
|
/// When calculating memory state, only the last FSRSItem is required.
|
||||||
pub(crate) fn single_card_revlog_to_item(
|
pub(crate) fn single_card_revlog_to_item(
|
||||||
entries: Vec<RevlogEntry>,
|
entries: Vec<RevlogEntry>,
|
||||||
|
|
|
@ -168,7 +168,7 @@ impl Collection {
|
||||||
position += 1;
|
position += 1;
|
||||||
}
|
}
|
||||||
if log {
|
if log {
|
||||||
col.log_manually_scheduled_review(&card, &original, usn)?;
|
col.log_manually_scheduled_review(&card, original.interval, usn)?;
|
||||||
}
|
}
|
||||||
col.update_card_inner(&mut card, original, usn)?;
|
col.update_card_inner(&mut card, original, usn)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,7 @@ impl Collection {
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
let days_from_today = distribution.sample(&mut rng);
|
let days_from_today = distribution.sample(&mut rng);
|
||||||
card.set_due_date(today, days_from_today, ease_factor, spec.force_reset);
|
card.set_due_date(today, days_from_today, ease_factor, spec.force_reset);
|
||||||
col.log_manually_scheduled_review(&card, &original, usn)?;
|
col.log_manually_scheduled_review(&card, original.interval, usn)?;
|
||||||
col.update_card_inner(&mut card, original, usn)?;
|
col.update_card_inner(&mut card, original, usn)?;
|
||||||
}
|
}
|
||||||
if let Some(key) = context {
|
if let Some(key) = context {
|
||||||
|
|
|
@ -33,12 +33,21 @@ impl<'a> StateContext<'a> {
|
||||||
/// Apply fuzz, respecting the passed bounds.
|
/// Apply fuzz, respecting the passed bounds.
|
||||||
/// Caller must ensure reasonable bounds.
|
/// Caller must ensure reasonable bounds.
|
||||||
pub(crate) fn with_review_fuzz(&self, interval: f32, minimum: u32, maximum: u32) -> u32 {
|
pub(crate) fn with_review_fuzz(&self, interval: f32, minimum: u32, maximum: u32) -> u32 {
|
||||||
if let Some(fuzz_factor) = self.fuzz_factor {
|
with_review_fuzz(self.fuzz_factor, interval, minimum, maximum)
|
||||||
let (lower, upper) = constrained_fuzz_bounds(interval, minimum, maximum);
|
}
|
||||||
(lower as f32 + fuzz_factor * ((1 + upper - lower) as f32)).floor() as u32
|
}
|
||||||
} else {
|
|
||||||
(interval.round() as u32).clamp(minimum, maximum)
|
pub(crate) fn with_review_fuzz(
|
||||||
}
|
fuzz_factor: Option<f32>,
|
||||||
|
interval: f32,
|
||||||
|
minimum: u32,
|
||||||
|
maximum: u32,
|
||||||
|
) -> u32 {
|
||||||
|
if let Some(fuzz_factor) = fuzz_factor {
|
||||||
|
let (lower, upper) = constrained_fuzz_bounds(interval, minimum, maximum);
|
||||||
|
(lower as f32 + fuzz_factor * ((1 + upper - lower) as f32)).floor() as u32
|
||||||
|
} else {
|
||||||
|
(interval.round() as u32).clamp(minimum, maximum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +70,7 @@ fn constrained_fuzz_bounds(interval: f32, minimum: u32, maximum: u32) -> (u32, u
|
||||||
(lower, upper)
|
(lower, upper)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fuzz_bounds(interval: f32) -> (u32, u32) {
|
pub(crate) fn fuzz_bounds(interval: f32) -> (u32, u32) {
|
||||||
let delta = fuzz_delta(interval);
|
let delta = fuzz_delta(interval);
|
||||||
(
|
(
|
||||||
(interval - delta).round() as u32,
|
(interval - delta).round() as u32,
|
||||||
|
|
|
@ -30,7 +30,7 @@ impl Collection {
|
||||||
let days_elapsed = self
|
let days_elapsed = self
|
||||||
.storage
|
.storage
|
||||||
.time_of_last_review(card.id)?
|
.time_of_last_review(card.id)?
|
||||||
.map(|ts| ts.elapsed_days_since(timing.next_day_at))
|
.map(|ts| timing.next_day_at.elapsed_days_since(ts))
|
||||||
.unwrap_or_default() as u32;
|
.unwrap_or_default() as u32;
|
||||||
let fsrs_retrievability = card
|
let fsrs_retrievability = card
|
||||||
.memory_state
|
.memory_state
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl TimestampSecs {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elapsed_days_since(self, other: TimestampSecs) -> u64 {
|
pub fn elapsed_days_since(self, other: TimestampSecs) -> u64 {
|
||||||
(other.0 - self.0).max(0) as u64 / 86_400
|
(self.0 - other.0).max(0) as u64 / 86_400
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_millis(self) -> TimestampMillis {
|
pub fn as_millis(self) -> TimestampMillis {
|
||||||
|
|
|
@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
} from "@tslib/backend";
|
} from "@tslib/backend";
|
||||||
import * as tr from "@tslib/ftl";
|
import * as tr from "@tslib/ftl";
|
||||||
import { runWithBackendProgress } from "@tslib/progress";
|
import { runWithBackendProgress } from "@tslib/progress";
|
||||||
|
import SwitchRow from "components/SwitchRow.svelte";
|
||||||
|
|
||||||
import SettingTitle from "../components/SettingTitle.svelte";
|
import SettingTitle from "../components/SettingTitle.svelte";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
|
@ -223,6 +224,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</WeightsInputRow>
|
</WeightsInputRow>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="m-2">
|
||||||
|
<SwitchRow bind:value={$config.rescheduleFsrsCards} defaultValue={false}>
|
||||||
|
<SettingTitle>
|
||||||
|
{tr.deckConfigRescheduleCardsOnChange()}
|
||||||
|
</SettingTitle>
|
||||||
|
</SwitchRow>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<details>
|
<details>
|
||||||
<summary>{tr.deckConfigComputeOptimalWeights()}</summary>
|
<summary>{tr.deckConfigComputeOptimalWeights()}</summary>
|
||||||
|
|
Loading…
Reference in a new issue