Skip to content

Commit

Permalink
Allow the user to specify a maximal interval for a specific card in t…
Browse files Browse the repository at this point in the history
…he metadata.
  • Loading branch information
ticki committed Oct 20, 2019
1 parent 5810c17 commit 5bd3c1d
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 40 deletions.
82 changes: 44 additions & 38 deletions backend/src/cards.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Content and state of flashcards.

use std::{env, process, io};
use std::{env, process, io, cmp};

use chrono;
use serde::{Serialize, Deserialize};
Expand Down Expand Up @@ -137,6 +137,10 @@ pub struct Card {
pub tags: Vec<String>,
/// The card's priority.
pub priority: Priority,
/// The user-specified upper-bound for the interval of the card.
///
/// If the calculated interval exceeds bound, the given interval will be this duration.
pub max_interval: chrono::Duration,
}

impl Default for Card {
Expand All @@ -146,6 +150,8 @@ impl Default for Card {
tags: Vec::new(),
// 2 should be around the average priority, so seems like a good default value.
priority: 2,
// Default to no maximal interval.
max_interval: chrono::Duration::max_value(),
}
}
}
Expand Down Expand Up @@ -183,32 +189,32 @@ impl Metacard {
}

/// Calculate new interval assuming that `self.state` is `New`.
fn new_interval_new(&self, settings: &settings::TagSettings, score: Score) -> chrono::Duration {
settings.learning_intervals[settings.get_learning_interval(
fn new_interval_new(&self, settings: &settings::TagSettings, score: Score, max_interval: chrono::Duration) -> chrono::Duration {
cmp::min(max_interval, settings.learning_intervals[settings.get_learning_interval(
settings.learning_interval_progressions[score as usize] - 1
)]
)])
}

/// Calculate new interval assuming that `self.state` is `Learning(step)`.
fn new_interval_learning(&self, settings: &settings::TagSettings, score: Score, step: usize)
fn new_interval_learning(&self, settings: &settings::TagSettings, score: Score, step: usize, max_interval: chrono::Duration)
-> chrono::Duration
{
settings.learning_intervals[settings.get_learning_interval(
cmp::min(max_interval, settings.learning_intervals[settings.get_learning_interval(
step as isize + settings.learning_interval_progressions[score as usize]
)]
)])
}

/// Calculate new interval assuming that `self.state` is `Relearning(step)`.
fn new_interval_relearning(&self, settings: &settings::TagSettings, score: Score, step: usize)
fn new_interval_relearning(&self, settings: &settings::TagSettings, score: Score, step: usize, max_interval: chrono::Duration)
-> chrono::Duration
{
settings.relearning_intervals[settings.get_relearning_interval(
cmp::min(max_interval, settings.relearning_intervals[settings.get_relearning_interval(
step as isize + settings.relearning_interval_progressions[score as usize]
)]
)])
}

/// Calculate new interval after nonfailed review assuming that `self.state` is `Learnt`.
fn new_interval_learnt(&self, settings: &settings::TagSettings, score: Score, priority: Priority, modifier: Ease) -> chrono::Duration {
fn new_interval_learnt(&self, settings: &settings::TagSettings, score: Score, priority: Priority, modifier: Ease, max_interval: chrono::Duration) -> chrono::Duration {
debug_assert!(score != Score::Fail);

// Calculate unsaturated new interval. This formula is based on the SM2 algorithm.
Expand All @@ -220,13 +226,13 @@ impl Metacard {
* settings.priority_modifiers[priority as usize]) as i64);

// Saturate the new interval according to the chosen settings.
if new_int > settings.max_interval {
cmp::min(max_interval, if new_int > settings.max_interval {
settings.max_interval
} else if new_int - self.current_interval < settings.min_interval_increase {
self.current_interval + settings.min_interval_increase
} else {
new_int
}
})
}

/// Calculate the new ease after reviewing a card with score `score`.
Expand All @@ -245,42 +251,42 @@ impl Metacard {
}

/// The updated intervals, depending on score.
pub fn new_intervals(&self, settings: &settings::TagSettings, priority: Priority, modifier: Ease) -> [chrono::Duration; SCORES] {
pub fn new_intervals(&self, settings: &settings::TagSettings, priority: Priority, modifier: Ease, max_interval: chrono::Duration) -> [chrono::Duration; SCORES] {
match self.state {
CardState::New => [
self.new_interval_new(settings, Score::Fail),
self.new_interval_new(settings, Score::Hard),
self.new_interval_new(settings, Score::Okay),
self.new_interval_new(settings, Score::Good),
self.new_interval_new(settings, Score::Easy),
self.new_interval_new(settings, Score::Fail, max_interval),
self.new_interval_new(settings, Score::Hard, max_interval),
self.new_interval_new(settings, Score::Okay, max_interval),
self.new_interval_new(settings, Score::Good, max_interval),
self.new_interval_new(settings, Score::Easy, max_interval),
],
CardState::Learning(step) => [
self.new_interval_learning(settings, Score::Fail, step),
self.new_interval_learning(settings, Score::Hard, step),
self.new_interval_learning(settings, Score::Okay, step),
self.new_interval_learning(settings, Score::Good, step),
self.new_interval_learning(settings, Score::Easy, step),
self.new_interval_learning(settings, Score::Fail, step, max_interval),
self.new_interval_learning(settings, Score::Hard, step, max_interval),
self.new_interval_learning(settings, Score::Okay, step, max_interval),
self.new_interval_learning(settings, Score::Good, step, max_interval),
self.new_interval_learning(settings, Score::Easy, step, max_interval),
],
CardState::Relearning(step) => [
self.new_interval_relearning(settings, Score::Fail, step),
self.new_interval_relearning(settings, Score::Hard, step),
self.new_interval_relearning(settings, Score::Okay, step),
self.new_interval_relearning(settings, Score::Good, step),
self.new_interval_relearning(settings, Score::Easy, step),
self.new_interval_relearning(settings, Score::Fail, step, max_interval),
self.new_interval_relearning(settings, Score::Hard, step, max_interval),
self.new_interval_relearning(settings, Score::Okay, step, max_interval),
self.new_interval_relearning(settings, Score::Good, step, max_interval),
self.new_interval_relearning(settings, Score::Easy, step, max_interval),
],
CardState::Learnt => [
// If the card was failed, we enter relearning.
settings.relearning_intervals[0],
self.new_interval_learnt(settings, Score::Hard, priority, modifier),
self.new_interval_learnt(settings, Score::Okay, priority, modifier),
self.new_interval_learnt(settings, Score::Good, priority, modifier),
self.new_interval_learnt(settings, Score::Easy, priority, modifier),
self.new_interval_learnt(settings, Score::Hard, priority, modifier, max_interval),
self.new_interval_learnt(settings, Score::Okay, priority, modifier, max_interval),
self.new_interval_learnt(settings, Score::Good, priority, modifier, max_interval),
self.new_interval_learnt(settings, Score::Easy, priority, modifier, max_interval),
],
}
}

/// Update the card after review.
pub fn review(&mut self, settings: &settings::TagSettings, score: Score, priority: Priority, familiarity: Ease) {
pub fn review(&mut self, settings: &settings::TagSettings, score: Score, priority: Priority, familiarity: Ease, max_interval: chrono::Duration) {
let now = now();
// Add review to card history.
self.history.push(Review {
Expand All @@ -296,14 +302,14 @@ impl Metacard {
match self.state {
CardState::New => {
// Update interval.
self.current_interval = self.new_interval_new(settings, score);
self.current_interval = self.new_interval_new(settings, score, max_interval);
self.due = now + self.current_interval;
// Update state.
self.state = CardState::Learning(0);
},
CardState::Learning(step) => {
// Update interval.
self.current_interval = self.new_interval_learning(settings, score, step);
self.current_interval = self.new_interval_learning(settings, score, step, max_interval);
self.due = now + self.current_interval;

// TODO: Get rid of this spaghetti. This is already a done in
Expand All @@ -326,7 +332,7 @@ impl Metacard {
// `new_interval_relearning`. For example, make `new_interval_relearning` also
// return `new_step`.
// Update interval.
self.current_interval = self.new_interval_relearning(settings, score, step);
self.current_interval = self.new_interval_relearning(settings, score, step, max_interval);
self.due = now + self.current_interval;

// Calculate new step.
Expand All @@ -350,7 +356,7 @@ impl Metacard {
self.due = now + self.current_interval;
} else {
// Update interval.
self.current_interval = self.new_interval_learnt(settings, score, priority, familiarity);
self.current_interval = self.new_interval_learnt(settings, score, priority, familiarity, max_interval);
self.due = now + self.current_interval;
}
// Update ease.
Expand Down
2 changes: 2 additions & 0 deletions backend/src/deck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ impl Parser {
"pdf" => self.current_card.view.extend(value.split(',').map(|x| x.trim().into()).map(cards::View::Pdf)),
"sh" => self.current_card.view.push(cards::View::Command(cards::Command(value.to_string()))),
"tags" => self.current_card.tags.extend(value.split(',').map(|x| x.trim().to_string())),
"max interval" => self.current_card.max_interval = parse_duration(value)?,
"priority" => {
// Parse the priority
let priority = value.parse::<cards::Priority>()? - 1;
Expand Down Expand Up @@ -531,6 +532,7 @@ file: derived_couple.pdf
tags: Definition
priority: 5
file: triad.pdf
max interval: 500d
[card 127]
tags: Definition
Expand Down
18 changes: 16 additions & 2 deletions backend/src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,17 @@ impl Scheduler {
// Calculate the average of the familiarity.
let average_familiarity = familiarity_sum / familiarity_num as f32;
// Register the review on the metacard.
// TODO: Get rid of spaghetti.
let priority = self.deck.cards[&self.sched.metacards[self.current_card].id].priority;
self.sched.metacards[self.current_card].review(tag_settings, score, priority, average_familiarity);
let max_interval = self.deck.cards[&self.sched.metacards[self.current_card].id].max_interval;
self.sched.metacards[self.current_card]
.review(
tag_settings,
score,
priority,
average_familiarity,
max_interval,
);

// Add back the card to the schedule.
self.reschedule();
Expand Down Expand Up @@ -434,7 +443,12 @@ impl Scheduler {
}

// Calculate the new intervals.
self.current_metacard().new_intervals(self.deck.tag_settings(&card.tags), card.priority, familiarity_sum / familiarity_num as f32)
self.current_metacard()
.new_intervals(
self.deck.tag_settings(&card.tags), card.priority,
familiarity_sum / familiarity_num as f32,
card.max_interval,
)
}

/// Get the number of due cards.
Expand Down

0 comments on commit 5bd3c1d

Please sign in to comment.