rust_expression/
graph.rs

1use crate::{
2    ast::{Function, Number},
3    calc::{calc_operand, Env, TopLevelEnv},
4};
5
6use num::iter::range_step_from;
7
8use thiserror::Error;
9
10use std::cmp::PartialEq;
11use std::fmt::Debug;
12
13#[derive(Error, Debug, PartialEq, Eq)]
14pub enum GraphError {
15    #[error("Unknown function `{0}` to plot")]
16    UnknownFunction(String),
17}
18
19struct ArgEnv<'a> {
20    name: &'a str,
21    value: Number,
22    env: &'a dyn Env,
23}
24
25impl<'a> Env for ArgEnv<'a> {
26    fn get(&self, sym: &str) -> Option<&Number> {
27        if sym == self.name {
28            Some(&self.value)
29        } else {
30            self.env.get(sym)
31        }
32    }
33
34    fn get_fun(&self, fun: &str) -> Option<&Function> {
35        self.env.get_fun(fun)
36    }
37}
38
39#[derive(Debug, PartialEq)]
40pub struct Graph {
41    env: TopLevelEnv,
42    fun: Function,
43}
44
45impl Graph {
46    pub fn new(name: &str, env: &TopLevelEnv) -> Result<Graph, GraphError> {
47        let env = env.clone();
48        let graph = Graph {
49            fun: env
50                .get_fun(name)
51                .ok_or_else(|| GraphError::UnknownFunction(name.to_string()))?
52                .clone(),
53            env,
54        };
55
56        Ok(graph)
57    }
58
59    fn x_name(&self) -> &str {
60        match self.fun {
61            Function::Custom(ref fun) => &fun.args[0],
62            Function::BuildIn(ref fun) => &fun.arg,
63        }
64    }
65
66    fn calc(&self, x: Number) -> Option<Number> {
67        match self.fun {
68            Function::Custom(ref fun) => {
69                let call_env = ArgEnv {
70                    name: self.x_name(),
71                    value: x,
72                    env: &self.env,
73                };
74                calc_operand(&fun.body, &call_env).ok()
75            }
76            Function::BuildIn(ref fun) => Some((fun.body)(x)),
77        }
78    }
79
80    pub fn plot(&self, area: &Area, screen: &Area) -> Result<Plot, GraphError> {
81        Plot::new(self, area, screen)
82    }
83}
84
85#[derive(Debug, PartialEq, Clone, Copy)]
86pub struct Range {
87    pub min: Number,
88    pub max: Number,
89}
90
91impl Range {
92    pub fn new(min: Number, max: Number) -> Range {
93        if min >= max {
94            panic!("min {:?} must be smaller than max {:?}", min, max);
95        }
96        Range { min, max }
97    }
98
99    pub fn contains(&self, pos: Number) -> bool {
100        (self.min..self.max).contains(&pos)
101    }
102
103    pub fn get_distance(&self) -> Number {
104        self.max - self.min
105    }
106
107    pub fn project_inclusive(&self, pixel: Number, to: &Range) -> Option<Number> {
108        if self.contains(pixel) {
109            Some(self.project(pixel, to))
110        } else {
111            None
112        }
113    }
114
115    pub fn project(&self, pixel: Number, to: &Range) -> Number {
116        to.min + (((pixel - self.min) / self.get_distance()) * to.get_distance())
117    }
118
119    pub fn move_by(&mut self, delta: Number) {
120        self.min += delta;
121        self.max += delta;
122    }
123
124    pub fn zoom_by(&mut self, factor: Number) {
125        let distance = self.get_distance();
126        let new_distance = distance * factor;
127        let diff = (new_distance - distance) / 2.;
128        self.min -= diff;
129        self.max += diff;
130    }
131}
132
133#[derive(Debug, PartialEq, Clone, Copy)]
134pub struct Area {
135    pub x: Range,
136    pub y: Range,
137}
138
139impl Area {
140    pub fn new(x_min: Number, y_min: Number, x_max: Number, y_max: Number) -> Area {
141        Area {
142            x: Range::new(x_min, x_max),
143            y: Range::new(y_min, y_max),
144        }
145    }
146
147    pub fn move_by(&mut self, x_delta: Number, y_delta: Number) {
148        self.x.move_by(x_delta);
149        self.y.move_by(y_delta);
150    }
151
152    pub fn zoom_by(&mut self, factor: Number) {
153        self.x.zoom_by(factor);
154        self.y.zoom_by(factor);
155    }
156}
157
158#[derive(Debug, PartialEq, Copy, Clone)]
159pub struct Tic {
160    pub pos: Number,
161    pub label: Number,
162}
163
164impl Tic {
165    pub fn new(pos: Number, label: Number) -> Tic {
166        Tic { pos, label }
167    }
168
169    pub fn create_tics(screen: &Range, area: &Range) -> Vec<Tic> {
170        let width = area.max - area.min;
171        let step = 10f64.powf((width.log10() - 1.0).round());
172        if area.contains(0.0) {
173            let left: Vec<Tic> = range_step_from(0f64, -step)
174                .take_while(|label| label > &area.min)
175                .map(|label| Tic::new(area.project_inclusive(label, screen).unwrap(), label))
176                .collect();
177
178            let right: Vec<Tic> = range_step_from(step, step)
179                .take_while(|label| label < &area.max)
180                .map(|label| Tic::new(area.project_inclusive(label, screen).unwrap(), label))
181                .collect();
182
183            left.iter().rev().chain(right.iter()).copied().collect()
184        } else {
185            let start = (area.min / step).ceil() * step;
186
187            range_step_from(start, step)
188                .take_while(|label| label < &area.max)
189                .map(|label| Tic::new(area.project_inclusive(label, screen).unwrap(), label))
190                .collect()
191        }
192    }
193}
194
195#[derive(Debug, PartialEq)]
196pub struct Axis {
197    pub pos: Number,
198    pub tics: Vec<Tic>,
199}
200
201impl Axis {
202    pub fn new(pos: Option<Number>, screen: &Range, area: &Range) -> Option<Axis> {
203        pos.map(|pos| Axis {
204            pos,
205            tics: Tic::create_tics(screen, area),
206        })
207    }
208}
209
210#[derive(Debug, PartialEq)]
211pub struct Plot {
212    pub points: Vec<Option<Number>>,
213    pub screen: Area,
214    pub x_axis: Option<Axis>,
215    pub y_axis: Option<Axis>,
216}
217
218impl Plot {
219    pub fn new(graph: &Graph, area: &Area, screen: &Area) -> Result<Plot, GraphError> {
220        let points = ((screen.x.min as i32)..(screen.x.max as i32))
221            .map(|w| {
222                let x = screen.x.project_inclusive(w as f64, &area.x).unwrap();
223                graph.calc(x).map(|y| area.y.project(y, &screen.y))
224            })
225            .collect();
226        let x_axis = Axis::new(area.y.project_inclusive(0., &screen.y), &screen.x, &area.x);
227        let y_axis = Axis::new(area.x.project_inclusive(0., &screen.x), &screen.y, &area.y);
228
229        Ok(Plot {
230            points,
231            screen: *screen,
232            x_axis,
233            y_axis,
234        })
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::ast::{CustomFunction, Operand, Operation, Term};
242    use crate::calc::TopLevelEnv;
243    use assert_approx_eq::assert_approx_eq;
244
245    #[test]
246    fn function_arg_x() {
247        let mut env = TopLevelEnv::default();
248        env.put("x".to_string(), -19.0).unwrap();
249        let name = "x";
250        let value = 42.0;
251        let env = ArgEnv {
252            name,
253            value,
254            env: &env,
255        };
256        assert_eq!(Some(&42.0), env.get("x"));
257    }
258
259    #[test]
260    fn function_arg_y() {
261        let mut env = TopLevelEnv::default();
262        env.put("y".to_string(), -19.0).unwrap();
263        let name = "x";
264        let value = 42.0;
265        let env = ArgEnv {
266            name,
267            value,
268            env: &env,
269        };
270        assert_eq!(Some(&-19.0), env.get("y"));
271    }
272
273    #[test]
274    fn function_call() {
275        let fun = Function::Custom(CustomFunction {
276            args: vec!["x".to_string()],
277            body: Operand::Symbol("x".to_string()),
278        });
279        let env = TopLevelEnv::default();
280        let graph = Graph { fun: fun, env };
281        assert_eq!(Some(1.0), graph.calc(1.0));
282    }
283
284    #[test]
285    #[should_panic(expected = "min 4.0 must be smaller than max 3.0")]
286    fn range_construct_failure() {
287        let _ = Range::new(4., 3.);
288    }
289
290    #[test]
291    fn range_distance_f64() {
292        assert_eq!(4.0, Range::new(10.0, 14.0).get_distance());
293    }
294
295    #[test]
296    fn range_project_plot_to_screen() {
297        let plot = Range::new(-100., 100.);
298        let screen = Range::new(0., 400.);
299
300        assert_eq!(Some(200.0), plot.project_inclusive(0., &screen));
301        assert_eq!(Some(300.0), plot.project_inclusive(50., &screen));
302        assert_eq!(Some(100.0), plot.project_inclusive(-50., &screen));
303    }
304
305    #[test]
306    fn range_project_plot_to_screen_out_of_range() {
307        let plot = Range::new(-100., 100.);
308        let screen = Range::new(0., 400.);
309
310        assert_eq!(None, plot.project_inclusive(-101., &screen));
311        assert_eq!(None, plot.project_inclusive(100., &screen));
312    }
313
314    #[test]
315    fn range_project_screen_to_plot() {
316        let screen = Range::new(0., 400.);
317        let plot = Range::new(-100., 100.);
318
319        assert_eq!(Some(-100.0), screen.project_inclusive(0., &plot));
320        assert_eq!(Some(-50.0), screen.project_inclusive(100., &plot));
321        assert_eq!(Some(0.0), screen.project_inclusive(200., &plot));
322        assert_eq!(Some(50.0), screen.project_inclusive(300., &plot));
323        assert_eq!(Some(99.5), screen.project_inclusive(399., &plot));
324    }
325
326    #[test]
327    fn range_project_screen_to_plot_out_of_range() {
328        let screen = Range::new(0., 400.);
329        let plot = Range::new(-100., 100.);
330
331        assert_eq!(None, screen.project_inclusive(-1., &plot));
332        assert_eq!(None, screen.project_inclusive(400., &plot));
333    }
334
335    #[test]
336    fn construct_plot() {
337        let mut env = TopLevelEnv::default();
338        let term = {
339            let lhs = Operand::Symbol("x".to_string());
340            let rhs = Operand::Number(2.0);
341            let op = Operation::Mul;
342            Term { lhs, rhs, op }
343        };
344        let body = Operand::Term(Box::new(term));
345        let fun = Function::Custom(CustomFunction {
346            args: vec!["x".to_string()],
347            body,
348        });
349        env.put_fun("f".to_string(), fun);
350        let graph = Graph::new("f", &env).unwrap();
351        let area = Area::new(-100., -100., 100., 100.);
352        let screen = Area::new(0., 0., 40., 40.);
353        let plot = graph.plot(&area, &screen).unwrap();
354
355        assert_eq!(20., plot.x_axis.unwrap().pos);
356        assert_eq!(20., plot.y_axis.unwrap().pos);
357        assert_eq!(40, plot.points.len());
358        assert_eq!(Some(-20.), plot.points[0]);
359        assert_eq!(Some(18.), plot.points[19]);
360        assert_eq!(Some(58.), plot.points[39]);
361    }
362
363    #[test]
364    fn range_move_by_positive() {
365        let mut r = Range::new(0., 10.);
366        r.move_by(2.);
367        assert_approx_eq!(2., r.min);
368        assert_approx_eq!(12., r.max);
369    }
370
371    #[test]
372    fn range_move_by_negative() {
373        let mut r = Range::new(2., 12.);
374        r.move_by(-5.);
375        assert_approx_eq!(-3., r.min);
376        assert_approx_eq!(7., r.max);
377    }
378
379    #[test]
380    fn area_move_by() {
381        let mut a = Area::new(0., 0., 10., 10.);
382        a.move_by(2., -3.);
383        assert_approx_eq!(2., a.x.min);
384        assert_approx_eq!(12., a.x.max);
385        assert_approx_eq!(-3., a.y.min);
386        assert_approx_eq!(7., a.y.max);
387    }
388
389    #[test]
390    fn range_zoom_by_out() {
391        let mut r = Range::new(2., 12.);
392        r.zoom_by(1.2);
393        assert_approx_eq!(1., r.min);
394        assert_approx_eq!(13., r.max);
395    }
396
397    #[test]
398    fn range_zoom_by_in() {
399        let mut r = Range::new(2., 12.);
400        r.zoom_by(0.8);
401        assert_approx_eq!(3., r.min);
402        assert_approx_eq!(11., r.max);
403    }
404
405    #[test]
406    fn create_tics_with_zero() {
407        use float_cmp::approx_eq;
408
409        let act = Tic::create_tics(&Range::new(-100., 100.), &Range::new(-5., 15.));
410        let exp: Vec<Tic> = range_step_from(-90., 10.)
411            .zip(range_step_from(-4., 1.))
412            .take(19)
413            .map(|(pos, label)| Tic::new(pos, label))
414            .collect();
415
416        assert_eq!(exp.len(), act.len());
417        assert!(exp.iter().zip(act.iter()).all(|(exp, act)| approx_eq!(
418            f64,
419            exp.pos,
420            act.pos,
421            epsilon = 0.00001
422        ) && approx_eq!(
423            f64,
424            exp.label,
425            act.label,
426            epsilon = 0.00001
427        )));
428    }
429
430    #[test]
431    fn create_tics_above_zero() {
432        use float_cmp::approx_eq;
433
434        let act = Tic::create_tics(&Range::new(0., 400.), &Range::new(3., 19.));
435        let exp: Vec<Tic> = range_step_from(0., 25.)
436            .zip(range_step_from(3., 1.))
437            .take(16)
438            .map(|(pos, label)| Tic::new(pos, label))
439            .collect();
440
441        assert_eq!(exp.len(), act.len());
442        println!("{:?}", act);
443        assert!(exp.iter().zip(act.iter()).all(|(exp, act)| approx_eq!(
444            f64,
445            exp.pos,
446            act.pos,
447            epsilon = 0.00001
448        ) && approx_eq!(
449            f64,
450            exp.label,
451            act.label,
452            epsilon = 0.00001
453        )));
454    }
455
456    #[test]
457    fn create_tics_below_zero() {
458        use float_cmp::approx_eq;
459
460        let act = Tic::create_tics(&Range::new(0., 400.), &Range::new(-19., -3.));
461        let exp: Vec<Tic> = range_step_from(0., 25.)
462            .zip(range_step_from(-19., 1.))
463            .take(16)
464            .map(|(pos, label)| Tic::new(pos, label))
465            .collect();
466
467        assert_eq!(exp.len(), act.len());
468        println!("{:?}", act);
469        assert!(exp.iter().zip(act.iter()).all(|(exp, act)| approx_eq!(
470            f64,
471            exp.pos,
472            act.pos,
473            epsilon = 0.00001
474        ) && approx_eq!(
475            f64,
476            exp.label,
477            act.label,
478            epsilon = 0.00001
479        )));
480    }
481}