janki/
item.rs

1use crate::{game::AnkiDB, storage::Storage};
2use serde::{Deserialize, Serialize};
3use std::{
4    ops::Deref,
5    time::{Duration, SystemTime},
6};
7
8///A Fact - a term and a definition
9#[derive(Debug, Clone, Eq, PartialEq)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11pub struct Fact {
12    ///The term of the fact - this is given to the test taker
13    pub term: String,
14    ///The definition of the fact - the test taker gives this.
15    pub definition: String,
16}
17
18impl Fact {
19    ///Fact constructor using [`Into`]
20    pub fn new(term: impl Into<String>, definition: impl Into<String>) -> Self {
21        Self {
22            term: term.into(),
23            definition: definition.into(),
24        }
25    }
26}
27
28///An Item - contains a fact, as well as stats about the user's history with that fact.
29///
30///Often accessed in the client via an [`ItemGuard`]
31#[derive(Debug, Serialize, Deserialize, Clone)]
32pub struct Item {
33    ///The fact that is the focus of the item
34    pub fact: Fact,
35    ///The last time the user saw this fact.
36    ///
37    ///Can be [`Option::None`] if the user has never been tested on this before.
38    ///
39    ///Clients should never directly access this, as this is set via an [`ItemGuard`] or otherwise
40    pub(crate) last_tested: Option<SystemTime>,
41    ///The history of the user - each bool signifies whether or not the user answered correctly.
42    ///
43    ///`history[0]` is the first time that the user was tested on the fact, and as the user is tested again, `history.push` is used.
44    ///
45    ///Clients should never directly access this, as this is set via an [`ItemGuard`] or otherwise
46    pub(crate) history: Vec<bool>,
47}
48
49impl From<Fact> for Item {
50    fn from(f: Fact) -> Self {
51        Self::new(f)
52    }
53}
54
55impl Item {
56    ///Constructor for a new [`Item`] - sets the `last_tested` to [`Option::None`] and the `history` to an empty `Vec<bool>`
57    #[must_use]
58    pub(crate) const fn new(fact: Fact) -> Self {
59        Self {
60            fact,
61            last_tested: None,
62            history: vec![],
63        }
64    }
65
66    ///Constructor for a new [`Item`] where all fields are given as arguments
67    #[must_use]
68    #[allow(dead_code)]
69    pub(crate) const fn all_parts(fact: Fact, last_tested: SystemTime, history: Vec<bool>) -> Self {
70        Self {
71            fact,
72            last_tested: Some(last_tested),
73            history,
74        }
75    }
76
77    ///Gets the user's streak for that fact - AKA the number of times in a row that they have answered correctly, with a correction factor to not make the user start from beginning on every mistake.
78    #[must_use]
79    pub fn get_streak(&self) -> u32 {
80        let min = if self.history.contains(&true) && self.true_streak() > 0 {
81            1
82        } else {
83            0
84        };
85
86        let mut count = 0;
87        for b in &self.history {
88            if *b {
89                count += 1;
90            } else {
91                count /= 2;
92            }
93        }
94
95        count.min(min)
96    }
97
98    ///Gets the user's streak - the number of times they have correctly answered in a row
99    pub(crate) fn true_streak(&self) -> u32 {
100        let mut count = 0;
101        for b in &self.history {
102            if *b {
103                count += 1;
104            } else {
105                count = 0;
106            }
107        }
108
109        count
110    }
111
112    ///Gets the time since the user was last tested on this fact.
113    ///
114    ///Can return a [`None`] if the user was never tested, or was tested in the future due to a [`SystemTime`] error
115    #[must_use]
116    pub fn time_since_last_test(&self) -> Option<Duration> {
117        if let Some(st) = self.last_tested {
118            if let Ok(d) = st.elapsed() {
119                return Some(d);
120            }
121        }
122
123        None
124    }
125}
126
127///Guard for [`Item`] for Client use.
128///
129///On [`Drop::drop`], the [`crate::game::AnkiGame`] is updated, and as of such only one [`ItemGuard`] can exist per [`crate::game::AnkiGame`]
130#[derive(Debug)] //TODO: refactor for concurrency
131pub struct ItemGuard<'a, S: Storage> {
132    ///A mutable reference to the [`AnkiDB`] from the [`crate::game::AnkiGame`]
133    v: &'a mut AnkiDB,
134    ///The index in the database for the item.
135    index: usize,
136    ///A mutable reference to the [`Storage`] for the [`crate::game::AnkiGame`]
137    s: &'a mut S,
138
139    ///Whether or not the user was correct.
140    ///
141    ///This should start as an [`Option::None`], and then be changed to `Some(true)` or `Some(false)` when the user answers.
142    pub was_succesful: Option<bool>,
143}
144
145impl<'a, S: Storage> Drop for ItemGuard<'a, S> {
146    ///On drop, assuming the question was answered (AKA `self.was_successful.is_some()`), the following happens:
147    ///
148    /// - the `history` and `last_tested` of the underlying item are updated.
149    /// - the database is written using [`Storage::write_db`]
150    fn drop(&mut self) {
151        if let Some(ws) = self.was_succesful {
152            self.v[self.index].history.push(ws);
153            self.v[self.index].last_tested = Some(SystemTime::now());
154            self.s.write_db(self.v).unwrap();
155        }
156    }
157}
158
159impl<'a, S: Storage> Deref for ItemGuard<'a, S> {
160    type Target = Fact;
161
162    fn deref(&self) -> &Self::Target {
163        &self.v[self.index].fact
164    }
165}
166
167impl<'a, S: Storage> ItemGuard<'a, S> {
168    ///Constructor for a new [`ItemGuard`] - should only be called by an [`crate::game::AnkiGame`]
169    pub(crate) fn new(v: &'a mut AnkiDB, index: usize, s: &'a mut S) -> Self {
170        Self {
171            v,
172            index,
173            was_succesful: None,
174            s,
175        }
176    }
177}