vultan 1.0.1

Terminal-based, Anki-compatible spaced-repetition study tool that reads flashcards from a directory of markdown notes.
Documentation
pub mod interval_coefficients;

use super::card::Card;
use super::tools::{Merge, Near, UID};
pub use interval_coefficients::IntervalCoefficients;
use itertools::Itertools;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)]
pub struct Deck {
    pub name: String,
    pub card_paths: Vec<String>,
    pub interval_coefficients: IntervalCoefficients,
    /// Override for the global factor threshold above which a card is "mastered".
    #[serde(default)]
    pub factor_mastered_threshold: Option<f64>,
    /// Override for the global interval threshold (days) above which a card is "mastered".
    #[serde(default)]
    pub interval_mastered_threshold: Option<f64>,
    /// Override for the global factor threshold below which a card is "forgotten".
    #[serde(default)]
    pub factor_forgotten_threshold: Option<f64>,
}

impl Deck {
    pub fn new(
        name: &str,
        card_paths: Vec<&str>,
        interval_coefficients: IntervalCoefficients,
    ) -> Self {
        Self {
            name: name.to_string(),
            card_paths: card_paths.iter().map(|s| s.to_string()).collect(),
            interval_coefficients,
            factor_mastered_threshold: None,
            interval_mastered_threshold: None,
            factor_forgotten_threshold: None,
        }
    }

    pub fn with_interval_coefficients(self, interval_coefficients: IntervalCoefficients) -> Self {
        Self {
            interval_coefficients,
            ..self
        }
    }
}

impl UID for Deck {
    fn uid(&self) -> &str {
        &self.name[..]
    }
}

impl Merge<Deck> for Deck {
    fn merge(self, other: &Deck) -> Self {
        self.with_interval_coefficients(other.interval_coefficients.clone())
    }
}

impl Near<Deck> for Deck {
    fn is_near(&self, other: &Deck) -> bool {
        self == other
    }
}

pub fn many_from_cards(cards: &[Card]) -> Vec<Deck> {
    let deck_name_to_paths = cards.iter().fold(
        std::collections::HashMap::new(),
        |mut deck_name_to_paths, card| {
            card.decks.iter().for_each(|deck_name| {
                deck_name_to_paths
                    .entry(deck_name)
                    .or_insert_with(Vec::new)
                    .push(card.path.as_str())
            });

            deck_name_to_paths
        },
    );

    deck_name_to_paths
        .into_iter()
        .map(|(deck_name, card_paths)| {
            Deck::new(deck_name, card_paths, IntervalCoefficients::default())
        })
        .collect_vec()
}

pub mod fake {
    use super::*;
    pub fn deck(name: &str, paths: &[&str]) -> Deck {
        Deck::new(
            name,
            paths.iter().map(|s| s.to_owned()).collect_vec(),
            IntervalCoefficients::default(),
        )
    }
}

pub mod assertions {
    use super::*;

    pub fn assert_decks_eq(mut expected: Vec<Deck>, mut actual: Vec<Deck>) {
        assert!(!expected.is_empty() && !actual.is_empty() && expected.len() == actual.len());
        let comparator = |a: &Deck, b: &Deck| a.name.cmp(&b.name);
        expected.sort_by(comparator);
        actual.sort_by(comparator);
        assert_eq!(expected, actual);
    }
}

#[cfg(test)]
mod unit_tests {

    use super::*;
    use crate::state::card::fake::card as fake_card;
    use crate::state::tools::test_tools::ignore;

    #[test]
    fn many_decks_from_cards() {
        let card_a = fake_card("1.md", vec!["a", "b"], ignore(), ignore(), ignore());
        let card_b = fake_card("2.md", vec!["b", "c"], ignore(), ignore(), ignore());
        let card_c = fake_card("3.md", vec!["c", "d"], ignore(), ignore(), ignore());
        let cards = vec![card_a, card_b, card_c];
        let expected = vec![
            fake::deck("a", &["1.md"]),
            fake::deck("b", &["1.md", "2.md"]),
            fake::deck("c", &["2.md", "3.md"]),
            fake::deck("d", &["3.md"]),
        ];
        let actual = many_from_cards(&cards);
        assertions::assert_decks_eq(expected, actual);
    }

    #[test]
    fn new() {
        let name = "cephelapoda";
        let card_paths = vec!["octopus", "squid", "cuttlefish", "nautilus"];
        let expected_card_paths = vec![
            String::from("octopus"),
            String::from("squid"),
            String::from("cuttlefish"),
            String::from("nautilus"),
        ];
        let interval_coefficients = IntervalCoefficients {
            pass_coef: 8.0,
            easy_coef: 9.0,
            fail_coef: 10.0,
        };
        let expected = Deck {
            name: name.to_string(),
            card_paths: expected_card_paths,
            interval_coefficients: interval_coefficients.clone(),
            factor_mastered_threshold: None,
            interval_mastered_threshold: None,
            factor_forgotten_threshold: None,
        };
        let actual = Deck::new(name, card_paths, interval_coefficients);
        assert_eq!(expected, actual);
    }

    #[test]
    fn with_interval_coefficients() {
        let name = "deck";
        let old_interval_coefficients = IntervalCoefficients::default();
        let new_interval_coefficients = IntervalCoefficients::new(8.0, 9.0, 10.0);
        let deck = Deck::new(name, vec!["a"], old_interval_coefficients);
        let mut expected = deck.clone();
        expected.interval_coefficients = new_interval_coefficients.clone();
        let actual = deck.with_interval_coefficients(new_interval_coefficients);
        assert_eq!(expected, actual);
    }

    #[test]
    fn uid() {
        let name = "The Deck";
        let deck = Deck::new(name, vec![], IntervalCoefficients::default());
        assert_eq!(name, deck.uid());
    }

    #[test]
    fn merge() {
        let a = Deck::new("a", vec![], IntervalCoefficients::default());
        let b = Deck::new("b", vec![], IntervalCoefficients::new(8.0, 9.0, 10.0));
        let mut expected = a.clone();
        expected.interval_coefficients = b.interval_coefficients.clone();
        assert_eq!(expected, a.merge(&b));
    }
}