rust_expression/
lib.rs

1mod ast;
2mod calc;
3mod graph;
4mod parser;
5mod solver;
6
7pub use crate::ast::Number;
8use crate::ast::Statement;
9use crate::calc::{calc_operand, CalcError, TopLevelEnv};
10use crate::graph::GraphError;
11pub use crate::graph::{Area, Graph, Range};
12use crate::parser::{parse, ParserError};
13use crate::solver::{solve_for, SolverError};
14
15use thiserror::Error;
16
17/// Calculator error
18#[derive(Debug, PartialEq, Eq, Error)]
19pub enum Error {
20    /// errors derived from parsers
21    #[error(transparent)]
22    ParserError(#[from] ParserError),
23    /// errors derived from calculator
24    #[error(transparent)]
25    CalcError(#[from] CalcError),
26    /// errors derived from solver
27    #[error(transparent)]
28    SolverError(#[from] SolverError),
29    /// errors derived from graph
30    #[error(transparent)]
31    GraphError(#[from] GraphError),
32}
33
34#[derive(Debug, PartialEq)]
35pub enum Value {
36    Void,
37    Number(Number),
38    Solved { variable: String, value: Number },
39    Graph(Graph),
40}
41
42/// # Calculator
43///
44/// See it in action on [https://msuesskraut.github.io/calc/index.html](https://msuesskraut.github.io/calc/index.html).
45/// Further examples are in [`Calculator::execute`].
46#[derive(Debug, Default)]
47pub struct Calculator {
48    env: TopLevelEnv,
49}
50
51impl Calculator {
52    /// constructs an calculator without any known variables
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Executes a command line.
58    /// These kinds of statements are supported:
59    /// - Expression:
60    ///   ```
61    ///   use rust_expression::{Calculator, Value};
62    ///   let mut c = Calculator::new();
63    ///   assert_eq!(Ok(Value::Number(3.0)), c.execute("1 + 2"));
64    ///   ```
65    /// - Variable assignments:
66    ///   ```
67    ///   # use rust_expression::{Calculator, Value};
68    ///   # let mut c = Calculator::new();
69    ///   assert_eq!(Ok(Value::Void), c.execute("a := 6"));
70    ///   assert_eq!(Ok(Value::Number(36.0)), c.execute("a ^ 2"));
71    ///   ```
72    /// - Solving linear expressions:
73    ///   ```
74    ///   # use rust_expression::{Calculator, Value};
75    ///   # let mut c = Calculator::new();
76    ///   # c.execute("a := 6");
77    ///   assert_eq!(Ok(Value::Solved {variable: "x".to_string(), value: 4.0}), c.execute("solve 3 * x - 2 = x + a for x"));
78    ///   ```
79    /// - Function definition:
80    ///   ```
81    ///   # use rust_expression::{Calculator, Value};
82    ///   # let mut c = Calculator::new();
83    ///   # c.execute("a := 6");
84    ///   assert_eq!(Ok(Value::Void), c.execute("fun(x, y) := y - x"));
85    ///   assert_eq!(Ok(Value::Number(2.0)), c.execute("fun(1.5 * 2, 3 + a) - 4"));
86    ///   ```
87    /// - Create a plot:
88    ///   ```
89    ///   # use rust_expression::{Calculator, Value};
90    ///   # use rust_expression::Area;
91    ///   # let mut c = Calculator::new();
92    ///   assert_eq!(Ok(Value::Void), c.execute("f(x) := x ^ 2"));
93    ///
94    ///   match c.execute("plot f") {
95    ///       Ok(Value::Graph(graph)) => {
96    ///           let area = Area::new(-100., -100., 100., 100.);
97    ///           let screen = Area::new(0., 0., 60., 40.);
98    ///           let plot = graph.plot(&area, &screen).unwrap();
99    ///           assert_eq!(Some(20.), plot.points[30]);
100    ///       }
101    ///       // ...
102    ///   #   _ => unimplemented!(),
103    ///   }
104    ///   ```
105    pub fn execute(&mut self, line: &str) -> Result<Value, Error> {
106        let st = parse(line)?;
107        match st {
108            Statement::Expression { op } => Ok(Value::Number(calc_operand(&op, &self.env)?)),
109            Statement::Assignment { sym, op } => {
110                self.env.put(sym, calc_operand(&op, &self.env)?)?;
111                Ok(Value::Void)
112            }
113            Statement::SolveFor { lhs, rhs, sym } => Ok(Value::Solved {
114                variable: sym.to_string(),
115                value: solve_for(&lhs, &rhs, &sym, &self.env)?,
116            }),
117            Statement::Function { name, fun } => {
118                self.env.put_fun(name, fun);
119                Ok(Value::Void)
120            }
121            Statement::Plot { name } => Ok(Value::Graph(Graph::new(&name, &self.env)?)),
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn simple_calc() {
132        let mut calc = Calculator::new();
133        assert_eq!(Ok(Value::Number(3.0)), calc.execute("1 + 2"));
134    }
135
136    #[test]
137    fn simple_assign() {
138        let mut calc = Calculator::new();
139        assert_eq!(Ok(Value::Void), calc.execute("a := 1"));
140        assert_eq!(Ok(Value::Number(1.0)), calc.execute("a"));
141    }
142
143    #[test]
144    fn simple_function() {
145        let mut calc = Calculator::new();
146        assert_eq!(Ok(Value::Void), calc.execute("fun(x, y) := y - x"));
147        assert_eq!(
148            Ok(Value::Number(20.0)),
149            calc.execute("fun(1 + 2, 3 * 9) - 4")
150        );
151    }
152
153    #[test]
154    fn simple_solve_for() {
155        let mut calc = Calculator::new();
156        assert_eq!(
157            Ok(Value::Solved {
158                variable: "y".to_string(),
159                value: 4.0
160            }),
161            calc.execute("solve 3 * y - 2 = y + 6 for y")
162        );
163    }
164
165    #[test]
166    fn simple_plot() {
167        let mut calc = Calculator::new();
168        assert_eq!(Ok(Value::Void), calc.execute("f(x) := x ^ 2"));
169        let graph = calc.execute("plot f").unwrap();
170        assert!(matches!(&graph, Value::Graph(_)));
171        if let Value::Graph(graph) = graph {
172            let plot = graph
173                .plot(
174                    &Area::new(-100., -100., 100., 100.),
175                    &Area::new(0., 0., 80., 30.),
176                )
177                .unwrap();
178            assert!(!plot.points.is_empty());
179        }
180    }
181}
182
183pub const HELP_SUMMARY: &str = include_str!("../doc/summary.md");