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}