mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
322 lines
10 KiB
Rust
322 lines
10 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, iter::Peekable};
|
|
|
|
use id_tree::{InsertBehavior, Node, NodeId, Tree};
|
|
|
|
use super::{Deck, NormalDeck};
|
|
use crate::{
|
|
deckconfig::{DeckConfig, DeckConfigId},
|
|
pb::decks::deck::normal::DayLimit,
|
|
prelude::*,
|
|
};
|
|
|
|
impl NormalDeck {
|
|
/// The deck's review limit for today, or its regular one, if any is configured.
|
|
pub fn current_review_limit(&self, today: u32) -> Option<u32> {
|
|
self.review_limit_today(today).or(self.review_limit)
|
|
}
|
|
|
|
/// The deck's new limit for today, or its regular one, if any is configured.
|
|
pub fn current_new_limit(&self, today: u32) -> Option<u32> {
|
|
self.new_limit_today(today).or(self.new_limit)
|
|
}
|
|
|
|
/// The deck's review limit for today.
|
|
pub fn review_limit_today(&self, today: u32) -> Option<u32> {
|
|
self.review_limit_today
|
|
.and_then(|day_limit| day_limit.limit(today))
|
|
}
|
|
|
|
/// The deck's new limit for today.
|
|
pub fn new_limit_today(&self, today: u32) -> Option<u32> {
|
|
self.new_limit_today
|
|
.and_then(|day_limit| day_limit.limit(today))
|
|
}
|
|
}
|
|
|
|
impl DayLimit {
|
|
pub fn limit(&self, today: u32) -> Option<u32> {
|
|
(self.today == today).then_some(self.limit)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) struct RemainingLimits {
|
|
pub review: u32,
|
|
pub new: u32,
|
|
}
|
|
|
|
impl RemainingLimits {
|
|
pub(crate) fn new(deck: &Deck, config: Option<&DeckConfig>, today: u32, v3: bool) -> Self {
|
|
if let Ok(normal) = deck.normal() {
|
|
if let Some(config) = config {
|
|
return Self::new_for_normal_deck(deck, today, v3, normal, config);
|
|
}
|
|
}
|
|
Self::default()
|
|
}
|
|
|
|
fn new_for_normal_deck(
|
|
deck: &Deck,
|
|
today: u32,
|
|
v3: bool,
|
|
normal: &NormalDeck,
|
|
config: &DeckConfig,
|
|
) -> RemainingLimits {
|
|
let review_limit = normal
|
|
.current_review_limit(today)
|
|
.unwrap_or(config.inner.reviews_per_day);
|
|
let new_limit = normal
|
|
.current_new_limit(today)
|
|
.unwrap_or(config.inner.new_per_day);
|
|
let (new_today, mut rev_today) = deck.new_rev_counts(today);
|
|
if v3 {
|
|
// any reviewed new cards contribute to the review limit
|
|
rev_today += new_today;
|
|
}
|
|
|
|
Self {
|
|
review: (review_limit as i32 - rev_today).max(0) as u32,
|
|
new: (new_limit as i32 - new_today).max(0) as u32,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn cap_to(&mut self, limits: RemainingLimits) {
|
|
self.review = self.review.min(limits.review);
|
|
self.new = self.new.min(limits.new);
|
|
}
|
|
}
|
|
|
|
impl Default for RemainingLimits {
|
|
fn default() -> Self {
|
|
RemainingLimits {
|
|
review: 9999,
|
|
new: 9999,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn remaining_limits_map<'a>(
|
|
decks: impl Iterator<Item = &'a Deck>,
|
|
config: &'a HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
v3: bool,
|
|
) -> HashMap<DeckId, RemainingLimits> {
|
|
decks
|
|
.map(|deck| {
|
|
(
|
|
deck.id,
|
|
RemainingLimits::new(
|
|
deck,
|
|
deck.config_id().and_then(|id| config.get(&id)),
|
|
today,
|
|
v3,
|
|
),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Wrapper of [RemainingLimits] with some additional meta data.
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct NodeLimits {
|
|
deck_id: DeckId,
|
|
/// absolute level in the deck hierarchy
|
|
level: usize,
|
|
limits: RemainingLimits,
|
|
}
|
|
|
|
impl NodeLimits {
|
|
fn new(deck: &Deck, config: &HashMap<DeckConfigId, DeckConfig>, today: u32) -> Self {
|
|
Self {
|
|
deck_id: deck.id,
|
|
level: deck.name.components().count(),
|
|
limits: RemainingLimits::new(
|
|
deck,
|
|
deck.config_id().and_then(|id| config.get(&id)),
|
|
today,
|
|
true,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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. Only decks with a remaining
|
|
/// limit above zero are included.
|
|
map: HashMap<DeckId, NodeId>,
|
|
}
|
|
|
|
impl LimitTreeMap {
|
|
/// Child [Deck]s must be sorted by name.
|
|
pub(crate) fn build(
|
|
root_deck: &Deck,
|
|
child_decks: Vec<Deck>,
|
|
config: &HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
) -> Self {
|
|
let root_limits = NodeLimits::new(root_deck, config, today);
|
|
let mut tree = Tree::new();
|
|
let root_id = tree
|
|
.insert(Node::new(root_limits), InsertBehavior::AsRoot)
|
|
.unwrap();
|
|
|
|
let mut map = HashMap::new();
|
|
if root_limits.limits.review > 0 {
|
|
map.insert(root_deck.id, root_id.clone());
|
|
}
|
|
|
|
let mut limits = Self { tree, map };
|
|
let mut remaining_decks = child_decks.into_iter().peekable();
|
|
limits.add_child_nodes(root_id, &mut remaining_decks, config, today);
|
|
|
|
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(
|
|
&mut self,
|
|
parent_node_id: NodeId,
|
|
remaining_decks: &mut Peekable<impl Iterator<Item = Deck>>,
|
|
config: &HashMap<DeckConfigId, DeckConfig>,
|
|
today: u32,
|
|
) {
|
|
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);
|
|
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)
|
|
} 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,
|
|
) {
|
|
let mut child_limits = NodeLimits::new(child_deck, config, today);
|
|
child_limits
|
|
.limits
|
|
.cap_to(self.tree.get(&parent_node_id).unwrap().data().limits);
|
|
|
|
let child_node_id = self
|
|
.tree
|
|
.insert(
|
|
Node::new(child_limits),
|
|
InsertBehavior::UnderNode(&parent_node_id),
|
|
)
|
|
.unwrap();
|
|
if child_limits.limits.review > 0 {
|
|
self.map.insert(child_deck.id, child_node_id);
|
|
}
|
|
}
|
|
pub(crate) fn root_limit_reached(&self) -> bool {
|
|
self.map.is_empty()
|
|
}
|
|
|
|
pub(crate) fn limit_reached(&self, deck_id: DeckId) -> bool {
|
|
self.map.get(&deck_id).is_none()
|
|
}
|
|
|
|
pub(crate) fn active_decks(&self) -> Vec<DeckId> {
|
|
self.tree
|
|
.traverse_pre_order(self.tree.root_node_id().unwrap())
|
|
.unwrap()
|
|
.map(|node| node.data().deck_id)
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn remaining_node_id(&self, deck_id: DeckId) -> Option<NodeId> {
|
|
self.map.get(&deck_id).map(Clone::clone)
|
|
}
|
|
|
|
pub(crate) fn decrement_node_and_parent_limits(&mut self, node_id: &NodeId, new: bool) {
|
|
let node = self.tree.get_mut(node_id).unwrap();
|
|
let parent = node.parent().cloned();
|
|
|
|
let limit = &mut node.data_mut().limits;
|
|
if if new {
|
|
limit.new = limit.new.saturating_sub(1);
|
|
limit.new
|
|
} else {
|
|
limit.review = limit.review.saturating_sub(1);
|
|
limit.review
|
|
} == 0
|
|
{
|
|
self.remove_node_and_descendants_from_map(node_id);
|
|
};
|
|
|
|
if let Some(parent_id) = parent {
|
|
self.decrement_node_and_parent_limits(&parent_id, new)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn remove_node_and_descendants_from_map(&mut self, node_id: &NodeId) {
|
|
let node = self.tree.get(node_id).unwrap();
|
|
self.map.remove(&node.data().deck_id);
|
|
|
|
for child_id in node.children().clone() {
|
|
self.remove_node_and_descendants_from_map(&child_id);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn cap_new_to_review(&mut self) {
|
|
self.cap_new_to_review_rec(&self.tree.root_node_id().unwrap().clone(), 9999);
|
|
}
|
|
|
|
fn cap_new_to_review_rec(&mut self, node_id: &NodeId, parent_limit: u32) {
|
|
let node = self.tree.get_mut(node_id).unwrap();
|
|
let mut limits = &mut node.data_mut().limits;
|
|
limits.new = limits.new.min(limits.review).min(parent_limit);
|
|
|
|
// clone because of borrowing rules
|
|
let node_limit = limits.new;
|
|
let children = node.children().clone();
|
|
|
|
if node_limit == 0 {
|
|
self.remove_node_and_descendants_from_map(node_id);
|
|
}
|
|
|
|
for child_id in children {
|
|
self.cap_new_to_review_rec(&child_id, node_limit);
|
|
}
|
|
}
|
|
}
|