cmg 0.1.0

A math game for children
Documentation
use std::fmt::Display;

use rand::Rng;
use serde::Serialize;
use thiserror::Error;

#[derive(Serialize, Clone, Debug, Error)]
pub enum CgmError {
    #[error("invalid integer token {0}")]
    ParseIntegerError(String),
    #[error("invalid float token {0}")]
    ParseFloatError(String),
    #[error("unknown token {0}")]
    UnknownToken(String),
}

pub type CgmResult<T> = Result<T, CgmError>;

#[derive(Serialize, Clone, Default, Copy)]
pub enum ProblemLevel {
    #[default]
    One = 1,
    Two = 2,
    Three = 3,
}

#[derive(Serialize, Clone, Default, Copy)]
pub enum ProblemType {
    #[default]
    Addition,
    Subtraction,
    Multiplication,
    Division,
}

impl From<ProblemType> for Symb {
    fn from(value: ProblemType) -> Self {
        match value {
            ProblemType::Addition => Symb::Plus,
            ProblemType::Subtraction => Symb::Minus,
            ProblemType::Multiplication => Symb::Multiplication,
            ProblemType::Division => Symb::Division,
        }
    }
}

fn generate_numbers(pl: ProblemLevel) -> Vec<u32> {
    let mut rng = rand::rng();
    let max_value = 10_u32.pow(pl as u32);
    let ns = (pl as usize).max(2);
    (0..ns).map(|_| rng.random_range(1..=max_value)).collect()
}

fn problems(pl: ProblemLevel, pt: ProblemType) -> Question {
    let n = generate_numbers(pl);
    let v = n.into_iter().map(|zn| (zn, pt)).collect();
    let tokens = TokenRepr::from_problem(v);
    Question {
        stringified: TokenRepr::to_expression(&tokens),
        repr: tokens,
    }
}

#[derive(Serialize, Clone, Default)]
pub struct Request {
    plevel: ProblemLevel,
    ptype: ProblemType,
}

#[allow(dead_code)]
pub fn request(r: Option<Request>) -> Var {
    let (pl, pt) = match r {
        Some(r) => (r.plevel, r.ptype),
        None => (ProblemLevel::default(), ProblemType::default()),
    };
    let question = problems(pl, pt);
    Var {
        answer: TokenRepr::to_answer(&question),
        question,
        choices: Vec::new(),
    }
}

#[derive(Serialize, Clone, Default, Debug, Copy, PartialEq)]
enum Symb {
    Plus,
    Minus,
    Multiplication,
    Division,
    #[default]
    End,
}

impl Display for Symb {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Symb::Plus => "+",
            Symb::Minus => "-",
            Symb::Multiplication => "*",
            Symb::Division => "/",
            Symb::End => "\n",
        };
        write!(f, "{}", s)
    }
}

#[derive(Serialize, Clone, Default, Debug, PartialEq)]
enum TokenRepr {
    Symbol(Symb),
    Number(u32),
    #[default]
    End,
}

impl TokenRepr {
    fn to_answer(question: &Question) -> Answer {
        let mut n: f32 = 0.0;
        let mut action = Symb::End;
        let tokens = question.tokens();
        for token in tokens {
            match token {
                TokenRepr::Symbol(symb) => action = *symb,
                TokenRepr::Number(zn) => match action {
                    Symb::Plus => n += *zn as f32,
                    Symb::Minus => n -= *zn as f32,
                    Symb::Multiplication => n *= *zn as f32,
                    Symb::Division => n /= *zn as f32,
                    Symb::End => n += *zn as f32,
                },
                TokenRepr::End => {}
            }
        }
        Answer {
            stringified: n.to_string(),
            numerical: n,
        }
    }

    fn from_problem(n: Vec<(u32, ProblemType)>) -> Vec<TokenRepr> {
        let mut tokens = Vec::new();
        for (number, pt) in n.into_iter() {
            tokens.push(TokenRepr::Number(number));
            tokens.push(TokenRepr::Symbol(pt.into()))
        }
        tokens.pop();

        tokens
    }

    fn to_expression(tokens: &Vec<TokenRepr>) -> String {
        let mut s = "".to_string();
        for token in tokens {
            match token {
                TokenRepr::Symbol(symb) => s.push_str(&symb.to_string()),
                TokenRepr::Number(i) => s.push_str(&i.to_string()),
                TokenRepr::End => {
                    println!("end");
                    break;
                }
            }
        }
        s
    }
}

#[derive(Serialize, Clone, Default, Debug)]
pub struct Question {
    stringified: String,
    repr: Vec<TokenRepr>,
}
impl Question {
    fn tokens(&self) -> &Vec<TokenRepr> {
        &self.repr
    }
}

#[derive(Serialize, Clone, Default, Debug)]
pub struct Answer {
    stringified: String,
    numerical: f32,
}

#[derive(Serialize, Clone, Default, Debug)]
pub struct Var {
    question: Question,
    answer: Answer,
    choices: Vec<Answer>,
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn json_output() {
        let var = Var::default();
        let output = serde_json::to_string(&var).ok();
        if let Some(ref out) = output {
            println!("{:?}", out);
        }
    }

    #[test]
    fn generate_problems() {
        for _ in 0..=100 {
            let n = generate_numbers(ProblemLevel::One);
            assert!(!n.iter().any(|n| *n > 10), "{:?}", n);
        }
    }

    #[test]
    fn make_request() {
        for _ in 0..100 {
            let req = Request {
                plevel: ProblemLevel::One,
                ptype: ProblemType::Addition,
            };
            let r = request(Some(req));
            assert!(r.answer.numerical <= 20.0, "{:?}", r);
        }
    }

    #[test]
    fn problem_three() {
        let pl = ProblemLevel::Three;
        let r = problems(pl, ProblemType::Addition);
        assert_eq!(r.tokens().len(), 5, "{:?}", r);
    }
}