speki_backend/
card.rs

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/// Represents a card that has been saved as a toml file, which is basically anywhere in the codebase
87/// except for when youre constructing a new card.
88/// Don't save this in containers or pass to functions, rather use the Id, and get new instances of SavedCard from the cache.
89/// Also, every time you mutate it, call the persist() method.
90#[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    /// If you accidentally made two identical cards, this will merge the stuff
181    /// like depencies, reviews etc.. and delete the other card
182    pub fn merge_with(&mut self, cache: &mut CardCache, other_card: Id) {
183        let other_card = cache.get_owned(other_card);
184
185        // we cant just add them directly, we have to check for loops in the DAG
186        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    /// dumb lol no internet will fix later
214    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    /// a = span means foo
402    /// b = change span desc by..
403    /// inserted: c = what is a span desc?
404
405    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        //dependent.set_dependency(insertion_id);
416        //insertion.set_dependency(dependency_id);
417    }
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    /// Checks if corresponding file has been modified after this type got deserialized from the file.
476    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    /// Checks that its dependencies are not only marked finished, but they're also strong memories.
509    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    /// Moves card by deleting it and then creating it again in a new location
520    /// warning: will refresh file name
521    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    // expensive function!
595    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    // Call this function every time SavedCard is mutated.
633    fn persist(&mut self) {
634        if self.is_outdated() {
635            // When you persist, the last_modified in the card should match the ones from the file.
636            // This shouldn't be possible, as this function mutates itself to get a fresh copy, so
637            // i'll panic here to alert me of the logic bug.
638            let _x = format!("{:?}", self);
639            // panic!("{}", x);
640        }
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    // No recall, not even when you saw the answer.
763    #[default]
764    None,
765    // No recall, but you remember the answer when you read it.
766    Late,
767    // Struggled but you got the answer right or somewhat right.
768    Some,
769    // No hesitation, perfect recall.
770    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        //factor * Self::randomize_factor()
782    }
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    // Expected gain in memory strength after a review.
844    pub fn expected_gain(&self) -> Option<f32> {
845        let recall_rate = self.recall_rate()?;
846
847        let current_strength = self.strength()?;
848
849        // The estimated strength if you fail at this point in time.
850        let fail_strength = self.next_strength(Grade::Late);
851
852        // The estimated strength if you succeed at this point in time.
853        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            // the grade is wrong
918            time_passed.min(current_stability).mul_f32(grade_factor)
919        } else {
920            // the grade is correct
921            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; // Calculate the time passed since the previous review
948            stability = Self::new_stability(&review.grade, Some(time_passed), stability);
949            prev_timestamp = review.timestamp; // Update the timestamp for the next iteration
950        }
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    // When (unix time) did the review take place?
995    #[serde(with = "serde_duration_as_secs")]
996    pub timestamp: Duration,
997    // Recall grade.
998    pub grade: Grade,
999    // How long you spent before attempting recall.
1000    #[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    //#[serde(deserialize_with = "deserialize_image_path")]
1026    //pub image: ImagePath,
1027}
1028
1029use serde::de::Deserializer;
1030
1031#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Clone)]
1032pub enum IsSuspended {
1033    False,
1034    True,
1035    // Card is temporarily suspended, until contained unix time has passed.
1036    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/// How important a given card is, where 0 is the least important, 100 is most important.
1109#[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    // this card is needed to understand the current card. useful so that you won't need to write a bunch of boilerplate context
1188    #[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        //reviews.pop();
1264
1265        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        //let cards = Card::load_cards_from_folder(&category);
1300        //insta::assert_debug_snapshot!(cards);
1301    }
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}