1use serde::{de, Deserialize, Serialize, Serializer};
2use toml::Value;
3
4use std::cmp::Ordering;
5use std::collections::{BTreeSet, HashSet};
6use std::ffi::OsString;
7use std::fs::read_to_string;
8use std::io::BufRead;
9use std::path::{Path, PathBuf};
10
11use std::time::Duration;
12use uuid::Uuid;
13
14use crate::cache::CardCache;
15use crate::categories::Category;
16use crate::media::AudioSource;
17use crate::{common::current_time, Id};
18
19pub type RecallRate = f32;
20
21#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Debug)]
22pub struct CardLocation {
23 file_name: OsString,
24 category: Category,
25}
26
27impl CardLocation {
28 pub fn new(path: &Path) -> Self {
29 let file_name = path.file_name().unwrap().to_owned();
30 let category = Category::from_card_path(path);
31 Self {
32 file_name,
33 category,
34 }
35 }
36
37 fn as_path(&self) -> PathBuf {
38 let mut path = self.category.as_path().join(self.file_name.clone());
39 path.set_extension("toml");
40 path
41 }
42}
43
44impl std::fmt::Display for SavedCard {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "{}", self.front_text())
47 }
48}
49
50impl From<SavedCard> for Card {
51 fn from(value: SavedCard) -> Self {
52 value.card
53 }
54}
55
56pub enum ReviewType {
57 Normal,
58 Pending,
59 Unfinished,
60}
61
62#[derive(Debug, Default)]
63pub struct CardInfo {
64 pub recall_rate: f32,
65 pub strength: f32,
66 pub stability: f32,
67 pub resolved: bool,
68 pub suspended: bool,
69 pub finished: bool,
70}
71
72impl CardInfo {
73 fn new(card: &SavedCard, cache: &mut CardCache) -> Option<Self> {
74 Self {
75 recall_rate: card.recall_rate()?,
76 strength: card.strength()?.as_secs_f32() / 86400.,
77 stability: card.stability()?.as_secs_f32() / 86400.,
78 resolved: card.is_resolved(cache),
79 suspended: card.is_suspended(),
80 finished: card.is_finished(),
81 }
82 .into()
83 }
84}
85
86#[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Hash, Debug)]
91pub struct SavedCard {
92 card: Card,
93 location: CardLocation,
94 last_modified: Duration,
95}
96
97impl SavedCard {
98 pub fn print(&self) -> String {
99 self.card.front.text.clone()
100 }
101
102 pub fn set_priority(&mut self, priority: Priority) {
103 self.card.meta.priority = priority;
104 self.persist();
105 }
106
107 #[allow(dead_code)]
108 pub fn priority(&self) -> &Priority {
109 &self.card.meta.priority
110 }
111
112 pub fn expected_gain(&self) -> Option<f32> {
113 self.card.history.expected_gain()
114 }
115
116 pub fn get_info(&self, cache: &mut CardCache) -> Option<CardInfo> {
117 CardInfo::new(self, cache)
118 }
119
120 pub fn reviews(&self) -> &Vec<Review> {
121 &self.card.history.0
122 }
123
124 pub fn raw_reviews(&self) -> &Reviews {
125 &self.card.history
126 }
127
128 pub fn new(card: Card, location: CardLocation, last_modified: Duration) -> Self {
129 Self {
130 card,
131 location,
132 last_modified,
133 }
134 }
135
136 pub fn get_unfinished_dependent_qty(&self, cache: &mut CardCache) -> usize {
137 let dependents = cache.recursive_dependents(self.id());
138 let mut unfinished = 0;
139 for dependent in dependents {
140 unfinished += cache.get_ref(dependent).is_finished() as usize;
141 }
142 unfinished
143 }
144
145 pub fn last_modified(&self) -> Duration {
146 self.last_modified
147 }
148
149 pub fn category(&self) -> &Category {
150 &self.location.category
151 }
152
153 pub fn front_text(&self) -> &str {
154 &self.card.front.text
155 }
156
157 #[allow(dead_code)]
158 pub fn is_pending(&self) -> bool {
159 self.card.history.is_empty()
160 }
161
162 pub fn is_suspended(&self) -> bool {
163 self.card.meta.suspended.is_suspended()
164 }
165
166 pub fn is_finished(&self) -> bool {
167 self.card.meta.finished
168 }
169
170 pub fn set_front_text(&mut self, text: &str) {
171 self.card.front.text = text.to_string();
172 self.persist();
173 }
174
175 pub fn set_back_text(&mut self, text: &str) {
176 self.card.back.text = text.to_string();
177 self.persist();
178 }
179
180 pub fn merge_with(&mut self, cache: &mut CardCache, other_card: Id) {
183 let other_card = cache.get_owned(other_card);
184
185 for dependency in other_card.dependency_ids() {
187 self.set_dependency(*dependency, cache);
188 }
189 for dependent in other_card.dependent_ids() {
190 self.set_dependent(*dependent, cache);
191 }
192
193 self.card
194 .history
195 .0
196 .extend(other_card.reviews().iter().map(|review| review.to_owned()));
197 self.card
198 .meta
199 .tags
200 .extend(other_card.card.meta.tags.iter().map(|s| s.to_owned()));
201 other_card.delete(cache);
202 self.persist();
203 }
204
205 pub fn stability(&self) -> Option<Duration> {
206 self.card.history.stability()
207 }
208
209 pub fn recall_rate(&self) -> Option<f32> {
210 self.card.history.recall_rate()
211 }
212
213 pub fn recall_rate_sortable(&self) -> Option<u32> {
215 self.card
216 .history
217 .recall_rate()
218 .map(|rate| rate as u32 * 10000)
219 }
220
221 pub fn trad_recall_rate_at_time(&self, time: Duration) -> Option<f32> {
222 let stability = self.stability()?;
223 let time_passed = time.checked_sub(self.reviews().last().unwrap().timestamp)?;
224 Reviews::calculate_recall_rate(&time_passed, &stability).into()
225 }
226
227 pub fn strength(&self) -> Option<Duration> {
228 self.card.history.strength()
229 }
230
231 pub fn time_since_last_review(&self) -> Option<Duration> {
232 self.card.time_passed_since_last_review()
233 }
234
235 pub fn back_text(&self) -> &str {
236 &self.card.back.text
237 }
238
239 pub fn contains_tag(&self, tag: &str) -> bool {
240 self.card.meta.tags.contains(tag)
241 }
242
243 pub fn id(&self) -> Id {
244 self.card.meta.id
245 }
246
247 pub fn dependent_ids(&self) -> &BTreeSet<Id> {
248 &self.card.meta.dependents
249 }
250
251 pub fn dependency_ids(&self) -> &BTreeSet<Id> {
252 &self.card.meta.dependencies
253 }
254
255 pub fn set_suspended(&mut self, suspended: IsSuspended) {
256 self.card.meta.suspended = suspended;
257 self.persist();
258 }
259
260 pub fn set_finished(&mut self, finished: bool) {
261 self.card.meta.finished = finished;
262 self.persist();
263 }
264
265 pub fn insert_tag(&mut self, tag: String) {
266 self.card.meta.tags.insert(tag);
267 self.persist();
268 }
269
270 pub fn check_cycle(cache: &mut CardCache, id: Id, is_dependent: bool) -> Option<Vec<Id>> {
271 let mut visited = HashSet::new();
272 let mut path = Vec::new();
273 Self::has_cycle_helper(cache, id, is_dependent, &mut visited, &mut path)
274 }
275
276 fn has_cycle_helper(
277 cache: &mut CardCache,
278 id: Id,
279 is_dependent: bool,
280 visited: &mut HashSet<Id>,
281 path: &mut Vec<Id>,
282 ) -> Option<Vec<Id>> {
283 if path.contains(&id) {
284 return Some(path.clone());
285 }
286
287 path.push(id);
288
289 let dependencies = if is_dependent {
290 cache.dependencies(id)
291 } else {
292 cache.dependents(id)
293 };
294
295 for dependency in dependencies {
296 if !visited.contains(&dependency) {
297 if let Some(cycle) =
298 Self::has_cycle_helper(cache, dependency, is_dependent, visited, path)
299 {
300 return Some(cycle);
301 }
302 }
303 }
304
305 visited.insert(id);
306 path.pop();
307
308 None
309 }
310
311 pub fn set_dependency(&mut self, id: Id, cache: &mut CardCache) -> Option<String> {
312 self.card.meta.dependencies.insert(id);
313 self.persist();
314
315 let mut other_card = cache.get_owned(id);
316 other_card.card.meta.dependents.insert(self.card.meta.id);
317 other_card.persist();
318 if let Some(mut path) = Self::check_cycle(cache, id, false) {
319 path.reverse();
320
321 while path[0] != self.id() {
322 path.rotate_right(1);
323 }
324 path.push(self.id());
325 let veclen = path.len();
326
327 let textvec = path
328 .into_iter()
329 .map(|id| cache.get_ref(id).front_text().to_string())
330 .collect::<Vec<_>>();
331
332 let mut s = String::new();
333
334 for (i, item) in textvec.iter().enumerate().take(veclen) {
335 s.push_str(&format!("'{}'", item));
336
337 if i == 0 {
338 s.push_str(" depends on ")
339 } else if i < (veclen - 2) {
340 s.push_str(" which depends on ")
341 } else if i != (veclen - 1) {
342 s.push_str(" which creates a cycle, as it depends on ")
343 }
344 }
345
346 self.card.meta.dependencies.remove(&id);
347 self.persist();
348
349 other_card.card.meta.dependents.remove(&self.id());
350 other_card.persist();
351 return Some(s);
352 }
353 None
354 }
355
356 pub fn set_dependent(&mut self, id: Id, cache: &mut CardCache) -> Option<String> {
357 self.card.meta.dependents.insert(id);
358 self.persist();
359
360 let mut other_card = cache.get_owned(id);
361 other_card.card.meta.dependencies.insert(self.card.meta.id);
362 other_card.persist();
363
364 if let Some(mut path) = Self::check_cycle(cache, id, true) {
365 while path[0] != self.id() {
366 path.rotate_right(1);
367 }
368 path.push(self.id());
369 let veclen = path.len();
370
371 let textvec = path
372 .into_iter()
373 .map(|id| cache.get_ref(id).front_text().to_string())
374 .collect::<Vec<_>>();
375
376 let mut s = String::new();
377
378 for (i, item) in textvec.iter().enumerate().take(veclen) {
379 s.push_str(&format!("'{}'", item));
380
381 if i == 0 {
382 s.push_str(" depends on ")
383 } else if i < (veclen - 2) {
384 s.push_str(" which depends on ")
385 } else if i != (veclen - 1) {
386 s.push_str(" which creates a cycle, as it depends on ")
387 }
388 }
389
390 self.card.meta.dependents.remove(&id);
391 self.persist();
392
393 other_card.card.meta.dependencies.remove(&self.id());
394 other_card.persist();
395
396 return Some(s);
397 }
398 None
399 }
400
401 pub fn _insert_dependency_raw(
406 dependent_id: &Id,
407 dependency_id: &Id,
408 insertion_id: &Id,
409 cache: &mut CardCache,
410 ) {
411 let mut dependent = Self::from_id(dependent_id).unwrap();
412 let _insertion = Self::from_id(insertion_id).unwrap();
413
414 dependent.remove_dependency(dependency_id, cache);
415 }
418
419 pub fn remove_dependency(&mut self, id: &Id, _cache: &mut CardCache) {
420 self.card.meta.dependencies.remove(id);
421 self.persist();
422
423 if let Some(mut other_card) = Self::from_id(id) {
424 other_card.card.meta.dependents.remove(&self.id());
425 other_card.persist();
426 }
427 }
428
429 pub fn remove_dependent(&mut self, id: &Id, _cache: &mut CardCache) {
430 self.card.meta.dependencies.remove(id);
431 self.persist();
432
433 if let Some(mut other_card) = Self::from_id(id) {
434 other_card.card.meta.dependents.remove(&self.id());
435 other_card.persist();
436 }
437 }
438
439 pub fn as_path(&self) -> PathBuf {
440 self.location.as_path()
441 }
442
443 pub fn pending_filter(card: Id, cache: &mut CardCache) -> bool {
444 let card = cache.get_ref(card);
445 card.card.history.is_empty()
446 && !card.is_suspended()
447 && card.is_finished()
448 && card.is_confidently_resolved(cache)
449 }
450
451 pub fn random_filter(card: Id, cache: &mut CardCache) -> bool {
452 let card = cache.get_ref(card);
453 card.is_finished() && !card.is_suspended() && card.is_resolved(cache)
454 }
455
456 pub fn unfinished_filter(card: Id, cache: &mut CardCache) -> bool {
457 let card = cache.get_ref(card);
458 !card.is_finished() && !card.is_suspended() && card.is_resolved(cache)
459 }
460
461 pub fn review_filter(card: Id, cache: &mut CardCache) -> bool {
462 let card = cache.get_ref(card);
463 let (Some(stability), Some(last_review_time)) =
464 (card.stability(), card.card.history.time_since_last_review())
465 else {
466 return false;
467 };
468
469 card.is_finished()
470 && !card.is_suspended()
471 && stability < last_review_time
472 && card.is_confidently_resolved(cache)
473 }
474
475 pub fn is_outdated(&self) -> bool {
477 let file_last_modified = {
478 let path = self.as_path();
479 let system_time = std::fs::metadata(path).unwrap().modified().unwrap();
480 system_time_as_unix_time(system_time)
481 };
482
483 let in_memory_last_modified = self.last_modified;
484
485 match in_memory_last_modified.cmp(&file_last_modified) {
486 Ordering::Less => true,
487 Ordering::Equal => false,
488 Ordering::Greater => panic!("Card in-memory shouldn't have a last_modified more recent than its corresponding file"),
489 }
490 }
491
492 pub fn is_resolved(&self, cache: &mut CardCache) -> bool {
493 cache
494 .recursive_dependencies(self.id())
495 .iter()
496 .all(|id| cache.get_ref(*id).is_finished())
497 }
498
499 fn is_strong(&self, min_stability: Duration, min_recall: f32) -> bool {
500 let (Some(stability), Some(recall)) = (self.stability(), self.card.history.recall_rate())
501 else {
502 return false;
503 };
504
505 self.card.meta.finished && stability > min_stability && recall > min_recall
506 }
507
508 pub fn is_confidently_resolved(&self, cache: &mut CardCache) -> bool {
510 let min_stability = Duration::from_secs(86400 * 2);
511 let min_recall: f32 = 0.95;
512
513 cache
514 .recursive_dependencies(self.id())
515 .iter()
516 .all(|id| cache.get_ref(*id).is_strong(min_stability, min_recall))
517 }
518
519 pub fn move_card(self, destination: &Category, cache: &mut CardCache) -> Self {
522 if self.location.category == *destination {
523 return self;
524 }
525 assert!(self.as_path().exists());
526 std::fs::remove_file(self.as_path()).unwrap();
527 assert!(!self.as_path().exists());
528 self.into_card().save_new_card(destination, cache)
529 }
530
531 pub fn get_review_type(&self) -> ReviewType {
532 match (self.card.history.is_empty(), self.is_finished()) {
533 (_, false) => ReviewType::Unfinished,
534 (false, true) => ReviewType::Normal,
535 (true, true) => ReviewType::Pending,
536 }
537 }
538
539 pub fn delete(self, cache: &mut CardCache) {
540 let path = self.as_path();
541 std::fs::remove_file(&path).unwrap();
542 assert!(!path.exists());
543
544 let self_id = self.card.meta.id;
545
546 for dependency in self.card.meta.dependencies {
547 let mut dependency = cache.get_owned(dependency);
548 dependency.card.meta.dependents.remove(&self_id);
549 dependency.persist();
550 }
551
552 for dependent in self.card.meta.dependents {
553 let mut dependent = cache.get_owned(dependent);
554 dependent.card.meta.dependencies.remove(&self_id);
555 dependent.persist();
556 }
557 cache.remove(self_id);
558 }
559
560 pub fn get_cards_from_category_recursively(category: &Category) -> HashSet<SavedCard> {
561 let mut cards = HashSet::new();
562 let cats = category.get_following_categories();
563 for cat in cats {
564 cards.extend(cat.get_containing_cards());
565 }
566 cards
567 }
568
569 pub fn search_in_cards<'a>(
570 input: &'a str,
571 cards: &'a HashSet<SavedCard>,
572 excluded_cards: &'a HashSet<Id>,
573 ) -> Vec<&'a SavedCard> {
574 cards
575 .iter()
576 .filter(|card| {
577 (card
578 .card
579 .front
580 .text
581 .to_ascii_lowercase()
582 .contains(&input.to_ascii_lowercase())
583 || card
584 .card
585 .back
586 .text
587 .to_ascii_lowercase()
588 .contains(&input.to_ascii_lowercase()))
589 && !excluded_cards.contains(&card.id())
590 })
591 .collect()
592 }
593
594 pub fn from_id(id: &Id) -> Option<Self> {
596 Self::load_all_cards()
597 .into_iter()
598 .find(|card| &card.card.meta.id == id)
599 }
600
601 pub fn load_all_cards() -> HashSet<SavedCard> {
602 Self::get_cards_from_category_recursively(&Category::root())
603 }
604
605 pub fn edit_with_vim(&self) -> Self {
606 let path = self.as_path();
607 open_file_with_vim(path.as_path()).unwrap();
608 Self::from_path(path.as_path())
609 }
610
611 pub fn from_path(path: &Path) -> Self {
612 let content = read_to_string(path).expect("Could not read the TOML file");
613 let card: Card = toml::from_str(&content).unwrap();
614 let location = CardLocation::new(path);
615
616 let last_modified = {
617 let system_time = std::fs::metadata(path).unwrap().modified().unwrap();
618 system_time_as_unix_time(system_time)
619 };
620
621 Self {
622 card,
623 location,
624 last_modified,
625 }
626 }
627
628 pub fn into_card(self) -> Card {
629 self.card
630 }
631
632 fn persist(&mut self) {
634 if self.is_outdated() {
635 let _x = format!("{:?}", self);
639 }
641
642 let path = self.as_path();
643 if !path.exists() {
644 let msg = format!("following path doesn't really exist: {}", path.display());
645 panic!("{msg}");
646 }
647
648 let toml = toml::to_string(&self.card).unwrap();
649
650 std::fs::write(&path, toml).unwrap();
651 *self = SavedCard::from_path(path.as_path())
652 }
653
654 pub fn new_review(&mut self, grade: Grade, time: Duration) {
655 let review = Review::new(grade, time);
656 self.card.history.add_review(review);
657 self.persist();
658 }
659
660 pub fn fake_new_review(&mut self, grade: Grade, time: Duration, at_time: Duration) {
661 let review = Review {
662 timestamp: at_time,
663 grade,
664 time_spent: time,
665 };
666 self.card.history.add_review(review);
667 }
668
669 pub fn lapses(&self) -> u32 {
670 self.card.history.lapses()
671 }
672}
673
674#[derive(Ord, PartialOrd, Eq, Hash, PartialEq, Deserialize, Serialize, Debug, Default, Clone)]
675pub struct Card {
676 pub front: Side,
677 pub back: Side,
678 pub meta: Meta,
679 #[serde(default, skip_serializing_if = "Reviews::is_empty")]
680 pub history: Reviews,
681}
682
683impl Card {
684 pub fn new(front: Side, back: Side, meta: Meta) -> Self {
685 Card {
686 front,
687 back,
688 meta,
689 history: Reviews::default(),
690 }
691 }
692
693 pub fn import_cards(filename: &Path) -> Option<Vec<Self>> {
694 let mut lines = std::io::BufReader::new(std::fs::File::open(filename).ok()?).lines();
695 let mut cards = vec![];
696
697 while let Some(Ok(question)) = lines.next() {
698 if let Some(Ok(answer)) = lines.next() {
699 cards.push(Self::new_simple(question, answer));
700 }
701 }
702 cards.into()
703 }
704
705 pub fn new_simple(front: String, back: String) -> Self {
706 Card {
707 front: Side {
708 text: front,
709 ..Default::default()
710 },
711 back: Side {
712 text: back,
713 ..Default::default()
714 },
715 ..Default::default()
716 }
717 }
718
719 pub fn save_new_card(self, category: &Category, cache: &mut CardCache) -> SavedCard {
720 let toml = toml::to_string(&self).unwrap();
721 std::fs::create_dir_all(category.as_path()).unwrap();
722 let max_char_len = 40;
723 let front_text = self
724 .front
725 .text
726 .chars()
727 .filter(|c| c.is_ascii_alphanumeric() || c.is_ascii_whitespace())
728 .collect::<String>()
729 .replace(' ', "_");
730 let mut file_name = PathBuf::from(truncate_string(front_text, max_char_len));
731
732 if file_name.exists() {
733 file_name = PathBuf::from(self.meta.id.to_string());
734 }
735
736 let mut path = category.as_path().join(file_name).with_extension("toml");
737
738 if path.exists() {
739 file_name = PathBuf::from(self.meta.id.to_string());
740 path = category.as_path().join(file_name).with_extension("toml");
741 }
742
743 std::fs::write(&path, toml).unwrap();
744
745 let full_card = SavedCard::from_path(path.as_path());
746 cache.insert(full_card.clone());
747 full_card
748 }
749
750 fn time_passed_since_last_review(&self) -> Option<Duration> {
751 if current_time() < self.history.0.last()?.timestamp {
752 return Duration::default().into();
753 }
754
755 Some(current_time() - self.history.0.last()?.timestamp)
756 }
757}
758
759#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize, Debug, Default, Clone)]
760#[serde(rename_all = "lowercase")]
761pub enum Grade {
762 #[default]
764 None,
765 Late,
767 Some,
769 Perfect,
771}
772
773impl Grade {
774 pub fn get_factor(&self) -> f32 {
775 match self {
776 Grade::None => 0.1,
777 Grade::Late => 0.25,
778 Grade::Some => 2.,
779 Grade::Perfect => 3.,
780 }
781 }
783
784 pub fn ml_normalized(&self) -> String {
785 match self {
786 Grade::None => 0.,
787 Grade::Late => 0.2,
788 Grade::Some => 0.8,
789 Grade::Perfect => 1.,
790 }
791 .to_string()
792 }
793
794 pub fn ml_binary(&self) -> String {
795 (self.is_succesful_recall() as u32).to_string()
796 }
797
798 pub fn is_succesful_recall(&self) -> bool {
799 match self {
800 Grade::None | Grade::Late => false,
801 Grade::Some | Grade::Perfect => true,
802 }
803 }
804}
805
806impl std::str::FromStr for Grade {
807 type Err = ();
808
809 fn from_str(s: &str) -> Result<Self, Self::Err> {
810 match s {
811 "1" => Ok(Self::None),
812 "2" => Ok(Self::Late),
813 "3" => Ok(Self::Some),
814 "4" => Ok(Self::Perfect),
815 _ => Err(()),
816 }
817 }
818}
819
820use crate::common::{
821 open_file_with_vim, serde_duration_as_float_secs, serde_duration_as_secs,
822 system_time_as_unix_time, truncate_string,
823};
824
825#[derive(Ord, PartialOrd, Eq, Hash, PartialEq, Debug, Default, Clone)]
826pub struct Reviews(pub Vec<Review>);
827
828impl Reviews {
829 pub fn strength(&self) -> Option<Duration> {
830 let days_passed = self.time_since_last_review()?;
831 let stability = self.stability()?;
832 let strength = calculate_memory_strength(0.9, days_passed, stability);
833 Duration::from_secs_f32(strength * 86400.).into()
834 }
835
836 pub fn next_strength(&self, grade: Grade) -> Duration {
837 let mut myself = self.clone();
838 let new_review = Review::new(grade, Duration::default());
839 myself.add_review(new_review);
840 myself.strength().unwrap_or_default()
841 }
842
843 pub fn expected_gain(&self) -> Option<f32> {
845 let recall_rate = self.recall_rate()?;
846
847 let current_strength = self.strength()?;
848
849 let fail_strength = self.next_strength(Grade::Late);
851
852 let win_strength = self.next_strength(Grade::Some);
854
855 Some(Self::pure_expected_gain(
856 recall_rate,
857 current_strength,
858 fail_strength,
859 win_strength,
860 ))
861 }
862
863 pub fn pure_expected_gain(
864 recall_rate: f32,
865 current_strength: Duration,
866 fail_strength: Duration,
867 win_strength: Duration,
868 ) -> f32 {
869 let current_strength = current_strength.as_secs_f32() / 86400.;
870 let winstrength = win_strength.as_secs_f32() / 86400.;
871 let failstrength = fail_strength.as_secs_f32() / 86400.;
872
873 let expected_win = (winstrength) * recall_rate;
874 let expected_loss = (failstrength) * (1. - recall_rate);
875
876 let expected_strength = expected_win + expected_loss;
877
878 expected_strength - current_strength
879 }
880
881 pub fn is_empty(&self) -> bool {
882 self.0.is_empty()
883 }
884
885 pub fn len(&self) -> usize {
886 self.0.len()
887 }
888
889 pub fn into_inner(self) -> Vec<Review> {
890 self.0
891 }
892
893 pub fn from_raw(reviews: Vec<Review>) -> Self {
894 Self(reviews)
895 }
896
897 pub fn add_review(&mut self, review: Review) {
898 self.0.push(review);
899 }
900
901 pub fn lapses(&self) -> u32 {
902 self.0.iter().fold(0, |lapses, review| match review.grade {
903 Grade::None | Grade::Late => lapses + 1,
904 Grade::Some | Grade::Perfect => 0,
905 })
906 }
907
908 pub fn new_stability(
909 grade: &Grade,
910 time_passed: Option<Duration>,
911 current_stability: Duration,
912 ) -> Duration {
913 let grade_factor = grade.get_factor();
914 let time_passed = time_passed.unwrap_or(Duration::from_secs(86400));
915
916 if grade_factor < 1.0 {
917 time_passed.min(current_stability).mul_f32(grade_factor)
919 } else {
920 let alternative_stability = time_passed.mul_f32(grade_factor);
922 if alternative_stability > current_stability {
923 alternative_stability
924 } else {
925 let interpolation_ratio =
926 time_passed.as_secs_f32() / current_stability.as_secs_f32() * grade_factor;
927 current_stability
928 + Duration::from_secs_f32(current_stability.as_secs_f32() * interpolation_ratio)
929 }
930 }
931 }
932
933 pub fn stability(&self) -> Option<Duration> {
934 let reviews = &self.0;
935 if reviews.is_empty() {
936 return None;
937 }
938
939 let mut stability =
940 Self::new_stability(&reviews[0].grade, None, Duration::from_secs(86400));
941 let mut prev_timestamp = reviews[0].timestamp;
942
943 for review in &reviews[1..] {
944 if prev_timestamp > review.timestamp {
945 return None;
946 }
947 let time_passed = review.timestamp - prev_timestamp; stability = Self::new_stability(&review.grade, Some(time_passed), stability);
949 prev_timestamp = review.timestamp; }
951
952 Some(stability)
953 }
954
955 pub fn recall_rate(&self) -> Option<RecallRate> {
956 let days_passed = self.time_since_last_review()?;
957 let stability = self.stability()?;
958 Some(Self::calculate_recall_rate(&days_passed, &stability))
959 }
960
961 pub fn calculate_recall_rate(days_passed: &Duration, stability: &Duration) -> RecallRate {
962 let base: f32 = 0.9;
963 let ratio = days_passed.as_secs_f32() / stability.as_secs_f32();
964 (base.ln() * ratio).exp()
965 }
966
967 pub fn time_since_last_review(&self) -> Option<Duration> {
968 self.0.last().map(Review::time_passed)
969 }
970}
971
972impl Serialize for Reviews {
973 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
974 where
975 S: Serializer,
976 {
977 self.0.serialize(serializer)
978 }
979}
980
981impl<'de> Deserialize<'de> for Reviews {
982 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
983 where
984 D: Deserializer<'de>,
985 {
986 let mut reviews = Vec::<Review>::deserialize(deserializer)?;
987 reviews.sort_by_key(|review| review.timestamp);
988 Ok(Reviews(reviews))
989 }
990}
991
992#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Clone, Serialize, Debug, Default)]
993pub struct Review {
994 #[serde(with = "serde_duration_as_secs")]
996 pub timestamp: Duration,
997 pub grade: Grade,
999 #[serde(with = "serde_duration_as_float_secs")]
1001 pub time_spent: Duration,
1002}
1003
1004impl Review {
1005 fn new(grade: Grade, time_spent: Duration) -> Self {
1006 Self {
1007 timestamp: current_time(),
1008 grade,
1009 time_spent,
1010 }
1011 }
1012
1013 fn time_passed(&self) -> Duration {
1014 let unix = self.timestamp;
1015 let current_unix = current_time();
1016 current_unix.checked_sub(unix).unwrap_or_default()
1017 }
1018}
1019
1020#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Clone, Serialize, Debug, Default)]
1021pub struct Side {
1022 pub text: String,
1023 #[serde(flatten)]
1024 pub audio: AudioSource,
1025 }
1028
1029use serde::de::Deserializer;
1030
1031#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Clone)]
1032pub enum IsSuspended {
1033 False,
1034 True,
1035 TrueUntil(Duration),
1037}
1038
1039impl From<bool> for IsSuspended {
1040 fn from(value: bool) -> Self {
1041 match value {
1042 true => Self::True,
1043 false => Self::False,
1044 }
1045 }
1046}
1047
1048impl Default for IsSuspended {
1049 fn default() -> Self {
1050 Self::False
1051 }
1052}
1053
1054impl IsSuspended {
1055 fn verify_time(self) -> Self {
1056 if let Self::TrueUntil(dur) = self {
1057 if dur < current_time() {
1058 return Self::False;
1059 }
1060 }
1061 self
1062 }
1063
1064 pub fn is_suspended(&self) -> bool {
1065 !matches!(self, IsSuspended::False)
1066 }
1067}
1068
1069impl Serialize for IsSuspended {
1070 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1071 where
1072 S: serde::ser::Serializer,
1073 {
1074 match self.clone().verify_time() {
1075 IsSuspended::False => serializer.serialize_bool(false),
1076 IsSuspended::True => serializer.serialize_bool(true),
1077 IsSuspended::TrueUntil(duration) => serializer.serialize_u64(duration.as_secs()),
1078 }
1079 }
1080}
1081
1082impl<'de> Deserialize<'de> for IsSuspended {
1083 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1084 where
1085 D: Deserializer<'de>,
1086 {
1087 let value: Value = Deserialize::deserialize(deserializer)?;
1088
1089 match value {
1090 Value::Boolean(b) => Ok(b.into()),
1091 Value::Integer(i) => {
1092 if let Ok(secs) = std::convert::TryInto::<u64>::try_into(i) {
1093 Ok(IsSuspended::TrueUntil(Duration::from_secs(secs)).verify_time())
1094 } else {
1095 Err(de::Error::custom("Invalid duration format"))
1096 }
1097 }
1098
1099 _ => Err(serde::de::Error::custom("Invalid value for IsDisabled")),
1100 }
1101 }
1102}
1103
1104fn default_finished() -> bool {
1105 true
1106}
1107
1108#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Clone)]
1110pub struct Priority(u32);
1111
1112impl Priority {
1113 pub fn as_float(&self) -> f32 {
1114 self.to_owned().into()
1115 }
1116
1117 pub fn is_default(&self) -> bool {
1118 Self::default() == *self
1119 }
1120}
1121
1122impl TryFrom<char> for Priority {
1123 type Error = ();
1124
1125 fn try_from(value: char) -> Result<Self, Self::Error> {
1126 let pri = match value {
1127 '1' => 16,
1128 '2' => 33,
1129 '3' => 66,
1130 '4' => 83,
1131 _ => return Err(()),
1132 };
1133 Ok(Self(pri))
1134 }
1135}
1136
1137impl From<u32> for Priority {
1138 fn from(value: u32) -> Self {
1139 Self(value.clamp(0, 100))
1140 }
1141}
1142
1143impl Default for Priority {
1144 fn default() -> Self {
1145 Self(50)
1146 }
1147}
1148
1149impl From<Priority> for f32 {
1150 fn from(value: Priority) -> Self {
1151 value.0 as f32 / 100.
1152 }
1153}
1154
1155impl Serialize for Priority {
1156 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1157 where
1158 S: Serializer,
1159 {
1160 self.0.serialize(serializer)
1161 }
1162}
1163
1164impl<'de> Deserialize<'de> for Priority {
1165 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1166 where
1167 D: Deserializer<'de>,
1168 {
1169 let value = u32::deserialize(deserializer)?;
1170 if value > 100 {
1171 Err(serde::de::Error::custom("Invalid priority value"))
1172 } else {
1173 Ok(Priority(value))
1174 }
1175 }
1176}
1177
1178#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize, Debug, Clone)]
1179pub struct Meta {
1180 pub id: Id,
1181 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1182 pub dependencies: BTreeSet<Id>,
1183 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1184 pub dependents: BTreeSet<Id>,
1185 #[serde(default)]
1186 pub requires_typing: bool,
1187 #[serde(default)]
1189 pub context: Option<Id>,
1190 #[serde(default)]
1191 pub suspended: IsSuspended,
1192 #[serde(default = "default_finished")]
1193 pub finished: bool,
1194 #[serde(default, skip_serializing_if = "Priority::is_default")]
1195 pub priority: Priority,
1196 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1197 pub tags: BTreeSet<String>,
1198}
1199
1200impl Default for Meta {
1201 fn default() -> Self {
1202 Self {
1203 id: Uuid::new_v4(),
1204 dependencies: BTreeSet::new(),
1205 dependents: BTreeSet::new(),
1206 requires_typing: false,
1207 suspended: IsSuspended::False,
1208 context: None,
1209 finished: default_finished(),
1210 priority: Priority::default(),
1211 tags: BTreeSet::new(),
1212 }
1213 }
1214}
1215
1216pub fn calculate_memory_strength(base: f64, time_passed: Duration, stability: Duration) -> f32 {
1217 let t = time_passed.as_secs_f64() / 86400.;
1218 let base = base.ln();
1219 let value = -1.0 / base + (1.0 - f64::exp(t * base)) / base;
1220 value as f32 * stability.as_secs_f32() / 86400.
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225
1226 use super::*;
1227
1228 #[test]
1229 fn test_stability() {
1230 let reviews = vec![
1231 Review {
1232 timestamp: Duration::from_secs(1687124756),
1233 grade: Grade::None,
1234 time_spent: Duration::default(),
1235 },
1236 Review {
1237 timestamp: Duration::from_secs(1687158818),
1238 grade: Grade::Some,
1239 time_spent: Duration::default(),
1240 },
1241 Review {
1242 timestamp: Duration::from_secs(1687248985),
1243 grade: Grade::Some,
1244 time_spent: Duration::default(),
1245 },
1246 Review {
1247 timestamp: Duration::from_secs(1687439802),
1248 grade: Grade::Some,
1249 time_spent: Duration::default(),
1250 },
1251 Review {
1252 timestamp: Duration::from_secs(1687853599),
1253 grade: Grade::Late,
1254 time_spent: Duration::default(),
1255 },
1256 Review {
1257 timestamp: Duration::from_secs(1687853599),
1258 grade: Grade::Some,
1259 time_spent: Duration::default(),
1260 },
1261 ];
1262
1263 let reviews = Reviews(reviews);
1266 let x = reviews.stability().unwrap().as_secs_f32() / 86400.;
1267 dbg!(x);
1268 }
1269
1270 fn debug_review(passed: f32, success: bool) -> Review {
1271 Review {
1272 timestamp: Duration::default() + Duration::from_secs_f32(passed * 86400.),
1273 grade: if success { Grade::Some } else { Grade::Late },
1274 time_spent: Duration::default(),
1275 }
1276 }
1277
1278 #[test]
1279 fn test_expected_gain() {
1280 let reviews = vec![
1281 debug_review(0., true),
1282 debug_review(1., false),
1283 debug_review(10., false),
1284 ];
1285 let reviews = Reviews(reviews);
1286 dbg!(reviews.expected_gain());
1287 }
1288
1289 #[test]
1290 fn foobar() {
1291 let vec: Vec<i32> = vec![];
1292 dbg!(vec.iter().all(|x| x == &0));
1293 }
1294
1295 #[test]
1296 fn test_load_cards_from_folder() {
1297 let _category = Category(vec!["maths".into(), "calculus".into()]);
1298
1299 }
1302
1303 #[test]
1304 fn debug_strength() {
1305 let stability = Duration::from_secs_f32(100. * 86400.);
1306 let time_passed = Duration::from_secs_f32(0. * 86400.);
1307 let x = calculate_memory_strength(0.9, time_passed, stability);
1308 dbg!(x);
1309 }
1310
1311 #[test]
1312 fn test_strength() {
1313 let stability = Duration::from_secs(86400);
1314 let days_passed = Duration::default();
1315 let recall_rate = Reviews::calculate_recall_rate(&days_passed, &stability);
1316 assert_eq!(recall_rate, 1.0);
1317
1318 let days_passed = Duration::from_secs(86400);
1319 let recall_rate = Reviews::calculate_recall_rate(&days_passed, &stability);
1320 assert_eq!(recall_rate, 0.9);
1321 }
1322}