mirror of
https://github.com/ankitects/anki.git
synced 2025-09-22 07:52:24 -04:00
398 lines
12 KiB
Rust
398 lines
12 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 std::iter::Peekable;
|
|
|
|
use anki_proto::decks::deck::normal::DayLimit;
|
|
use id_tree::InsertBehavior;
|
|
use id_tree::Node;
|
|
use id_tree::NodeId;
|
|
use id_tree::Tree;
|
|
|
|
use super::Deck;
|
|
use super::NormalDeck;
|
|
use crate::deckconfig::DeckConfig;
|
|
use crate::deckconfig::DeckConfigId;
|
|
use crate::prelude::*;
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub(crate) enum LimitKind {
|
|
Review,
|
|
New,
|
|
}
|
|
|
|
/// The deck's review limit for today, or its regular one, if any is
|
|
/// configured.
|
|
pub fn current_review_limit(deck: &NormalDeck, today: u32) -> Option<u32> {
|
|
review_limit_today(deck, today).or(deck.review_limit)
|
|
}
|
|
|
|
/// The deck's new limit for today, or its regular one, if any is
|
|
/// configured.
|
|
pub fn current_new_limit(deck: &NormalDeck, today: u32) -> Option<u32> {
|
|
new_limit_today(deck, today).or(deck.new_limit)
|
|
}
|
|
|
|
/// The deck's review limit for today.
|
|
pub fn review_limit_today(deck: &NormalDeck, today: u32) -> Option<u32> {
|
|
deck.review_limit_today
|
|
.and_then(|day_limit| limit_if_today(day_limit, today))
|
|
}
|
|
|
|
/// The deck's new limit for today.
|
|
pub fn new_limit_today(deck: &NormalDeck, today: u32) -> Option<u32> {
|
|
deck.new_limit_today
|
|
.and_then(|day_limit| limit_if_today(day_limit, today))
|
|
}
|
|
|
|
pub fn limit_if_today(limit: DayLimit, today: u32) -> Option<u32> {
|
|
(limit.today == today).then_some(limit.limit)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) struct RemainingLimits {
|
|
pub(crate) review: u32,
|
|
pub(crate) new: u32,
|
|
pub(crate) cap_new_to_review: bool,
|
|
}
|
|
|
|
impl RemainingLimits {
|
|
pub(crate) fn new(
|
|
deck: &Deck,
|
|
config: Option<&DeckConfig>,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
) -> Self {
|
|
if let Ok(normal) = deck.normal() {
|
|
if let Some(config) = config {
|
|
return Self::new_for_normal_deck(
|
|
deck,
|
|
today,
|
|
new_cards_ignore_review_limit,
|
|
normal,
|
|
config,
|
|
);
|
|
}
|
|
}
|
|
Self::default()
|
|
}
|
|
|
|
fn new_for_normal_deck(
|
|
deck: &Deck,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
normal: &NormalDeck,
|
|
config: &DeckConfig,
|
|
) -> RemainingLimits {
|
|
Self::new_for_normal_deck_v3(deck, today, new_cards_ignore_review_limit, normal, config)
|
|
}
|
|
|
|
fn new_for_normal_deck_v3(
|
|
deck: &Deck,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
normal: &NormalDeck,
|
|
config: &DeckConfig,
|
|
) -> RemainingLimits {
|
|
let mut review_limit =
|
|
current_review_limit(normal, today).unwrap_or(config.inner.reviews_per_day) as i32;
|
|
let mut new_limit =
|
|
current_new_limit(normal, today).unwrap_or(config.inner.new_per_day) as i32;
|
|
let (new_today_count, review_today_count) = deck.new_rev_counts(today);
|
|
|
|
review_limit -= review_today_count;
|
|
new_limit -= new_today_count;
|
|
if !new_cards_ignore_review_limit {
|
|
review_limit -= new_today_count;
|
|
new_limit = new_limit.min(review_limit);
|
|
}
|
|
|
|
Self {
|
|
review: review_limit.max(0) as u32,
|
|
new: new_limit.max(0) as u32,
|
|
cap_new_to_review: !new_cards_ignore_review_limit,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn get(&self, kind: LimitKind) -> u32 {
|
|
match kind {
|
|
LimitKind::Review => self.review,
|
|
LimitKind::New => self.new,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn cap_to(&mut self, limits: RemainingLimits) {
|
|
self.review = self.review.min(limits.review);
|
|
self.new = self.new.min(limits.new);
|
|
}
|
|
|
|
/// True if some limit was decremented to 0.
|
|
fn decrement(&mut self, kind: LimitKind) -> DecrementResult {
|
|
let before = *self;
|
|
match kind {
|
|
LimitKind::Review => {
|
|
self.review = self.review.saturating_sub(1);
|
|
if self.cap_new_to_review {
|
|
self.new = self.new.min(self.review);
|
|
}
|
|
}
|
|
LimitKind::New => self.new = self.new.saturating_sub(1),
|
|
}
|
|
DecrementResult::new(&before, self)
|
|
}
|
|
}
|
|
|
|
struct DecrementResult {
|
|
count_reached_zero: bool,
|
|
}
|
|
|
|
impl DecrementResult {
|
|
fn new(before: &RemainingLimits, after: &RemainingLimits) -> Self {
|
|
Self {
|
|
count_reached_zero: before.review > 0 && after.review == 0
|
|
|| before.new > 0 && after.new == 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for RemainingLimits {
|
|
fn default() -> Self {
|
|
RemainingLimits {
|
|
review: 9999,
|
|
new: 9999,
|
|
cap_new_to_review: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn remaining_limits_map<'a>(
|
|
decks: impl Iterator<Item = &'a Deck>,
|
|
config: &'a HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
) -> HashMap<DeckId, RemainingLimits> {
|
|
decks
|
|
.map(|deck| {
|
|
(
|
|
deck.id,
|
|
RemainingLimits::new(
|
|
deck,
|
|
deck.config_id().and_then(|id| config.get(&id)),
|
|
today,
|
|
new_cards_ignore_review_limit,
|
|
),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Wrapper of [RemainingLimits] with some additional meta data.
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct NodeLimits {
|
|
/// absolute level in the deck hierarchy
|
|
level: usize,
|
|
limits: RemainingLimits,
|
|
}
|
|
|
|
impl NodeLimits {
|
|
fn new(
|
|
deck: &Deck,
|
|
config: &HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
) -> Self {
|
|
Self {
|
|
level: deck.name.components().count(),
|
|
limits: RemainingLimits::new(
|
|
deck,
|
|
deck.config_id().and_then(|id| config.get(&id)),
|
|
today,
|
|
new_cards_ignore_review_limit,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct LimitTreeMap {
|
|
/// A tree representing the remaining limits of the active deck hierarchy.
|
|
//
|
|
// As long as we never (1) allow a tree without a root, (2) remove nodes,
|
|
// and (3) have more than 1 tree, it's safe to unwrap on Tree::get() and
|
|
// Tree::root_node_id(), even if we clone Nodes.
|
|
tree: Tree<NodeLimits>,
|
|
/// A map to access the tree node of a deck.
|
|
map: HashMap<DeckId, NodeId>,
|
|
}
|
|
|
|
impl LimitTreeMap {
|
|
/// [Deck]s must be sorted by name.
|
|
pub(crate) fn build(
|
|
decks: &[Deck],
|
|
config: &HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
) -> Self {
|
|
let root_limits = NodeLimits::new(&decks[0], config, today, new_cards_ignore_review_limit);
|
|
let mut tree = Tree::new();
|
|
let root_id = tree
|
|
.insert(Node::new(root_limits), InsertBehavior::AsRoot)
|
|
.unwrap();
|
|
|
|
let mut map = HashMap::new();
|
|
map.insert(decks[0].id, root_id.clone());
|
|
|
|
let mut limits = Self { tree, map };
|
|
let mut remaining_decks = decks[1..].iter().peekable();
|
|
limits.add_child_nodes(
|
|
root_id,
|
|
&mut remaining_decks,
|
|
config,
|
|
today,
|
|
new_cards_ignore_review_limit,
|
|
);
|
|
|
|
limits
|
|
}
|
|
|
|
/// Recursively appends descendants to the provided parent [Node], and adds
|
|
/// them to the [HashMap].
|
|
/// Given [Deck]s are assumed to arrive in depth-first order.
|
|
/// The tree-from-deck-list logic is taken from
|
|
/// [crate::decks::tree::add_child_nodes].
|
|
fn add_child_nodes<'d>(
|
|
&mut self,
|
|
parent_node_id: NodeId,
|
|
remaining_decks: &mut Peekable<impl Iterator<Item = &'d Deck>>,
|
|
config: &HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
) {
|
|
let parent = *self.tree.get(&parent_node_id).unwrap().data();
|
|
while let Some(deck) = remaining_decks.peek() {
|
|
match deck.name.components().count() {
|
|
l if l <= parent.level => {
|
|
// next item is at a higher level
|
|
break;
|
|
}
|
|
l if l == parent.level + 1 => {
|
|
// next item is an immediate descendent of parent
|
|
self.insert_child_node(
|
|
deck,
|
|
parent_node_id.clone(),
|
|
config,
|
|
today,
|
|
new_cards_ignore_review_limit,
|
|
);
|
|
remaining_decks.next();
|
|
}
|
|
_ => {
|
|
// next item is at a lower level
|
|
if let Some(last_child_node_id) = self
|
|
.tree
|
|
.get(&parent_node_id)
|
|
.unwrap()
|
|
.children()
|
|
.last()
|
|
.cloned()
|
|
{
|
|
self.add_child_nodes(
|
|
last_child_node_id,
|
|
remaining_decks,
|
|
config,
|
|
today,
|
|
new_cards_ignore_review_limit,
|
|
)
|
|
} else {
|
|
// immediate parent is missing, skip the deck until a DB check is run
|
|
remaining_decks.next();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn insert_child_node(
|
|
&mut self,
|
|
child_deck: &Deck,
|
|
parent_node_id: NodeId,
|
|
config: &HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
new_cards_ignore_review_limit: bool,
|
|
) {
|
|
let mut child_limits =
|
|
NodeLimits::new(child_deck, config, today, new_cards_ignore_review_limit);
|
|
child_limits
|
|
.limits
|
|
.cap_to(self.get_node_limits(&parent_node_id));
|
|
let child_node_id = self
|
|
.tree
|
|
.insert(
|
|
Node::new(child_limits),
|
|
InsertBehavior::UnderNode(&parent_node_id),
|
|
)
|
|
.unwrap();
|
|
self.map.insert(child_deck.id, child_node_id);
|
|
}
|
|
|
|
fn get_node_id(&self, deck_id: DeckId) -> Result<&NodeId> {
|
|
self.map
|
|
.get(&deck_id)
|
|
.or_invalid("deck not found in limits map")
|
|
}
|
|
|
|
fn get_node_limits(&self, node_id: &NodeId) -> RemainingLimits {
|
|
self.tree.get(node_id).unwrap().data().limits
|
|
}
|
|
|
|
fn get_deck_limits(&self, deck_id: DeckId) -> Result<RemainingLimits> {
|
|
self.get_node_id(deck_id)
|
|
.map(|node_id| self.get_node_limits(node_id))
|
|
}
|
|
|
|
fn get_root_limits(&self) -> RemainingLimits {
|
|
self.get_node_limits(self.tree.root_node_id().unwrap())
|
|
}
|
|
|
|
pub(crate) fn root_limit_reached(&self, kind: LimitKind) -> bool {
|
|
self.get_root_limits().get(kind) == 0
|
|
}
|
|
|
|
pub(crate) fn limit_reached(&self, deck_id: DeckId, kind: LimitKind) -> Result<bool> {
|
|
Ok(self.get_deck_limits(deck_id)?.get(kind) == 0)
|
|
}
|
|
|
|
pub(crate) fn decrement_deck_and_parent_limits(
|
|
&mut self,
|
|
deck_id: DeckId,
|
|
kind: LimitKind,
|
|
) -> Result<()> {
|
|
let node_id = self.get_node_id(deck_id)?.clone();
|
|
self.decrement_node_and_parent_limits(&node_id, kind);
|
|
Ok(())
|
|
}
|
|
|
|
fn decrement_node_and_parent_limits(&mut self, node_id: &NodeId, kind: LimitKind) {
|
|
let node = self.tree.get_mut(node_id).unwrap();
|
|
let parent = node.parent().cloned();
|
|
|
|
let limits = &mut node.data_mut().limits;
|
|
if limits.decrement(kind).count_reached_zero {
|
|
let limits = *limits;
|
|
self.cap_node_and_descendants(node_id, limits);
|
|
};
|
|
|
|
if let Some(parent_id) = parent {
|
|
self.decrement_node_and_parent_limits(&parent_id, kind)
|
|
}
|
|
}
|
|
|
|
fn cap_node_and_descendants(&mut self, node_id: &NodeId, limits: RemainingLimits) {
|
|
let node = self.tree.get_mut(node_id).unwrap();
|
|
node.data_mut().limits.cap_to(limits);
|
|
for child_id in node.children().clone() {
|
|
self.cap_node_and_descendants(&child_id, limits);
|
|
}
|
|
}
|
|
}
|