der_die_das 0.5.0

der_die_das: Learn german genders like a true geek.
Documentation
use std::collections::HashMap;

use time::{Duration, OffsetDateTime};

use crate::{noun_attempt::NounAttemptSaved, nouns::SavedNoun};

pub struct History {
    pub nouns: Vec<SavedNoun>,
    pub attempts: Vec<NounAttemptSaved>,
}

impl History {
    #[must_use]
    pub fn attempt_per_noun(&self) -> Vec<(SavedNoun, Vec<NounAttemptSaved>)> {
        let n: Vec<_> = self
            .nouns
            .iter()
            .map(|n| {
                let attempts: Vec<_> = self
                    .attempts
                    .iter()
                    .filter_map(|s| {
                        if s.noun_attempt.for_word == n.id {
                            Some(s.clone())
                        } else {
                            None
                        }
                    })
                    .collect();
                (n.to_owned(), attempts)
            })
            .collect();
        n
    }

    #[must_use]
    pub fn confidence_map(&self) -> Vec<(SavedNoun, u8)> {
        self.attempt_per_noun()
            .iter()
            .map(|s| {
                let confidence =
                    s.1.iter()
                        .fold(0u8, |confidence, a| match a.noun_attempt.what_happened {
                            crate::noun_attempt::Conclusion::Success => match confidence {
                                u8::MAX => confidence,
                                _ => confidence + 1,
                            },
                            crate::noun_attempt::Conclusion::WrongArticle(_) => match confidence {
                                u8::MIN => confidence,
                                _ => confidence - 1,
                            },
                        });
                (s.0.clone(), confidence)
            })
            .collect()
    }

    #[must_use]
    pub fn grouped_confidence_map(&self) -> HashMap<String, Vec<(SavedNoun, u8)>> {
        self.confidence_map().into_iter().fold(
            HashMap::new(),
            |mut acc: HashMap<String, Vec<(SavedNoun, u8)>>, (n, c)| {
                match acc.get_mut(&n.noun.group) {
                    Some(cu) => cu.push((n, c)),
                    None => _ = acc.insert(n.noun.group.clone(), vec![(n, c)]),
                };
                acc
            },
        )
    }

    #[must_use]
    pub fn next(&self) -> Option<(SavedNoun, u8)> {
        let mut sets = self.confidence_map();

        sets.sort_unstable_by_key(|s| s.1);

        sets.first().cloned()
    }

    #[must_use]
    pub fn next_group(&self, threshold: u8) -> Option<Vec<(SavedNoun, u8)>> {
        let sets = self.grouped_confidence_map();
        if sets.is_empty() {
            return None;
        }
        let under_threshold: HashMap<_, _> = sets
            .into_iter()
            .filter(|(_, nouns)| nouns.iter().any(|(_, c)| c <= &threshold))
            .collect();

        if under_threshold.is_empty() {
            return self.next_group(u8::MAX);
        }
        let mut ks: Vec<_> = under_threshold.keys().collect();
        ks.sort_unstable();
        let mut left = under_threshold.get(ks.first()?.to_owned())?.to_owned();
        left.sort_unstable_by_key(|s| s.1);
        Some(left)
    }

    #[must_use]
    pub fn next_with_group(&self, threshold: u8) -> Option<(SavedNoun, u8)> {
        let sets = self.grouped_confidence_map();
        if sets.is_empty() {
            return None;
        }

        let under_threshold: HashMap<_, _> = sets
            .into_iter()
            .filter(|(_, nouns)| nouns.iter().any(|(_, c)| c <= &threshold))
            .collect();

        if under_threshold.is_empty() {
            return self.next_with_group(u8::MAX);
        }

        let mut ks: Vec<_> = under_threshold.keys().collect();
        ks.sort_unstable();
        let mut left = under_threshold.get(ks.first()?.to_owned())?.to_owned();

        left.sort_unstable_by_key(|s| s.1);
        left.first().cloned()
    }

    /// Gives you the number of unique words attempted in each day.
    ///
    /// # Errors
    ///
    /// This function will return an error if there is an IO error.
    pub fn number_of_words_at(
        &self,
        from_time: OffsetDateTime,
        duration: Duration,
    ) -> miette::Result<usize> {
        let to_time = from_time.checked_add(duration).ok_or_else(|| {
            miette::miette!("duration could not be added to the time for some reason!")
        })?;

        let found_attempts = self
            .attempts
            .iter()
            .filter(|a| a.at.ge(&from_time) && a.at.lt(&to_time))
            .fold(vec![], |mut accu, att| {
                if !accu.contains(&att.noun_attempt.for_word) {
                    accu.push(att.noun_attempt.for_word.clone());
                }
                accu
            });
        Ok(found_attempts.len())
    }
}