diff --git a/TODO.md b/TODO.md index f468eb1..e5892cf 100644 --- a/TODO.md +++ b/TODO.md @@ -2,21 +2,23 @@ 1. Multiple functions in one graph. - Backend support - Integrals between functions (too hard to implement, maybe will shelve) - - UI - - Different colors (kinda) + - Display intersection between functions + - Different colors 2. Rerwite of function parsing code - Non `y=` functions. 3. Smart display of graph - Display of intersections between functions -4. re-add euler's number (well it works if you use capital e like `E^x`) -5. allow constants in min/max integral input (like pi or euler's number) -6. sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos) +4. Properly parse lowercase `e` as euler's number +5. Allow constants in min/max integral input (like pi or euler's number) +6. Sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos) 7. nth derivative support (again) -8. rewrite FunctionEntry to move more information and handling to egui_app (such as config changes) +8. Rewrite `FunctionEntry` to move more information and handling to egui_app (such as config changes) 9. Threading -10. fix integral display +10. Fix integral display 11. Improve loading indicator - Dynamically hide and unhide it when lagging -12. add comments in `parsing::process_func_str` for it to be better understandable +12. Add comments in `parsing::process_func_str` for it to be better understandable 13. Look into other, better methods of compression that would be faster 14. Better handling of panics and errors to display to the user +15. Display native/web dark mode detection as it's unneeded and can cause compat issues (see [egui #1305](https://github.com/emilk/egui/issues/1305)) +16. Add tests for `SerdeValueHelper` diff --git a/src/egui_app.rs b/src/egui_app.rs index b341ad3..5440a54 100644 --- a/src/egui_app.rs +++ b/src/egui_app.rs @@ -1,4 +1,4 @@ -use crate::function::{FunctionEntry, RiemannSum, EMPTY_FUNCTION_ENTRY}; +use crate::function::{FunctionEntry, RiemannSum, DEFAULT_FUNCTION_ENTRY}; use crate::misc::{JsonFileOutput, SerdeValueHelper}; use crate::parsing::{process_func_str, test_func}; @@ -186,7 +186,7 @@ lazy_static::lazy_static! { }; } -// Tests to make sure archived (and compressed) assets match expected data +/// Tests to make sure archived (and compressed) assets match expected data #[test] fn test_file_data() { let mut font_data: BTreeMap = BTreeMap::new(); @@ -228,7 +228,16 @@ fn test_file_data() { let json_data: SerdeValueHelper = SerdeValueHelper::new(include_str!("../assets/text.json")); - assert_eq!(ASSETS.get_json_file_output(), json_data.parse_values()); + let asset_json = ASSETS.get_json_file_output(); + let json_data_parsed = json_data.parse_values(); + + assert_eq!(asset_json, json_data_parsed); + + // NOTE: UPDATE THIS STRING IF `license_info` IN `text.json` IS MODIFIED + let target_license_info = "The AGPL license ensures that the end user, even if not hosting the program itself, is still guaranteed access to the source code of the project in question."; + + assert_eq!(target_license_info, asset_json.license_info); + assert_eq!(target_license_info, json_data_parsed.license_info); } cfg_if::cfg_if! { @@ -323,7 +332,7 @@ pub struct MathApp { impl Default for MathApp { fn default() -> Self { Self { - functions: vec![EMPTY_FUNCTION_ENTRY.clone().integral(true)], + functions: vec![DEFAULT_FUNCTION_ENTRY.clone().integral(true)], func_strs: vec![String::from(DEFAULT_FUNCION)], last_error: Vec::new(), last_info: (vec![0.0], Duration::ZERO), @@ -493,15 +502,7 @@ impl MathApp { if let Some(test_output_value) = test_func(&proc_func_str) { self.last_error.push((i, test_output_value)); } else { - function.update( - proc_func_str, - integral_enabled, - derivative_enabled, - self.settings.integral_min_x, - self.settings.integral_max_x, - self.settings.integral_num, - self.settings.sum, - ); + function.update(proc_func_str, integral_enabled, derivative_enabled); self.last_error = self .last_error .iter() @@ -569,7 +570,7 @@ impl epi::App for MathApp { .clicked() { self.functions.push( - EMPTY_FUNCTION_ENTRY + DEFAULT_FUNCTION_ENTRY .clone() .update_riemann(self.settings.sum), ); @@ -706,6 +707,10 @@ impl epi::App for MathApp { available_width, self.settings.extrema, self.settings.roots, + self.settings.integral_min_x, + self.settings.integral_max_x, + self.settings.integral_num, + self.settings.sum, ) }) .collect(); diff --git a/src/function.rs b/src/function.rs index d570998..3714523 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,14 +1,15 @@ #![allow(clippy::too_many_arguments)] // Clippy, shut -use crate::function_output::FunctionOutput; -#[allow(unused_imports)] -use crate::misc::{newtons_method, SteppedVector}; - use crate::egui_app::{DEFAULT_FUNCION, DEFAULT_RIEMANN}; +use crate::function_output::FunctionOutput; +use crate::misc::{newtons_method, SteppedVector}; use crate::parsing::BackingFunction; - -use eframe::egui::plot::PlotUi; -use eframe::egui::{plot::Value, widgets::plot::Bar}; +use eframe::{egui, epaint}; +use egui::{ + plot::{BarChart, Line, PlotUi, Points, Value, Values}, + widgets::plot::Bar, +}; +use epaint::Color32; use std::fmt::{self, Debug}; /// Represents the possible variations of Riemann Sums @@ -24,7 +25,8 @@ impl fmt::Display for RiemannSum { } lazy_static::lazy_static! { - pub static ref EMPTY_FUNCTION_ENTRY: FunctionEntry = FunctionEntry::empty(); + /// Represents a "default" instance of `FunctionEntry` + pub static ref DEFAULT_FUNCTION_ENTRY: FunctionEntry = FunctionEntry::default(); } /// `FunctionEntry` is a function that can calculate values, integrals, @@ -68,10 +70,10 @@ pub struct FunctionEntry { sum: RiemannSum, } -impl FunctionEntry { - /// Creates Empty Function instance - pub fn empty() -> Self { - Self { +impl Default for FunctionEntry { + /// Creates default FunctionEntry instance (which is empty) + fn default() -> FunctionEntry { + FunctionEntry { function: BackingFunction::new(DEFAULT_FUNCION), func_str: String::new(), min_x: -1.0, @@ -86,12 +88,11 @@ impl FunctionEntry { sum: DEFAULT_RIEMANN, } } +} +impl FunctionEntry { /// Update function settings - pub fn update( - &mut self, func_str: String, integral: bool, derivative: bool, integral_min_x: f64, - integral_max_x: f64, integral_num: usize, sum: RiemannSum, - ) { + pub fn update(&mut self, func_str: String, integral: bool, derivative: bool) { // If the function string changes, just wipe and restart from scratch if func_str != self.func_str { self.func_str = func_str.clone(); @@ -101,20 +102,6 @@ impl FunctionEntry { self.derivative = derivative; self.integral = integral; - - // Makes sure proper arguments are passed when integral is enabled - if integral - && (integral_min_x != self.integral_min_x) - | (integral_max_x != self.integral_max_x) - | (integral_num != self.integral_num) - | (sum != self.sum) - { - self.output.invalidate_integral(); - self.integral_min_x = integral_min_x; - self.integral_max_x = integral_max_x; - self.integral_num = integral_num; - self.sum = sum; - } } // TODO: refactor this @@ -126,9 +113,9 @@ impl FunctionEntry { let back_values: Vec = { if self.output.back.is_none() { self.output.back = Some( - (0..self.pixel_width) - .map(|x| (x as f64 / resolution as f64) + self.min_x) - .map(|x| Value::new(x, self.function.get(x))) + crate::misc::resolution_helper(self.pixel_width, self.min_x, resolution) + .iter() + .map(|x| Value::new(*x, self.function.get(*x))) .collect(), ); } @@ -254,11 +241,62 @@ impl FunctionEntry { self } + fn newtons_method_helper(&self, threshold: f64, derivative_level: usize) -> Option> { + let newtons_method_output: Vec = match derivative_level { + 0 => newtons_method( + threshold, + self.min_x..self.max_x, + self.output.back.to_owned().unwrap(), + &|x: f64| self.function.get(x), + &|x: f64| self.function.get_derivative_1(x), + ), + 1 => newtons_method( + threshold, + self.min_x..self.max_x, + self.output.derivative.to_owned().unwrap(), + &|x: f64| self.function.get_derivative_1(x), + &|x: f64| self.function.get_derivative_2(x), + ), + _ => unreachable!(), + }; + + if newtons_method_output.is_empty() { + None + } else { + Some( + newtons_method_output + .iter() + .map(|x| (*x, self.function.get(*x))) + .map(|(x, y)| Value::new(x, y)) + .collect(), + ) + } + } + /// Calculates and displays the function on PlotUI `plot_ui` pub fn display( &mut self, plot_ui: &mut PlotUi, min_x: f64, max_x: f64, pixel_width: usize, extrema: bool, - roots: bool, + roots: bool, integral_min_x: f64, integral_max_x: f64, integral_num: usize, + sum: RiemannSum, ) -> f64 { + let resolution: f64 = self.pixel_width as f64 / (max_x.abs() + min_x.abs()); + let resolution_iter = + crate::misc::resolution_helper(self.pixel_width, self.min_x, resolution); + + // Makes sure proper arguments are passed when integral is enabled + if self.integral + && (integral_min_x != self.integral_min_x) + | (integral_max_x != self.integral_max_x) + | (integral_num != self.integral_num) + | (sum != self.sum) + { + self.output.invalidate_integral(); + self.integral_min_x = integral_min_x; + self.integral_max_x = integral_max_x; + self.integral_num = integral_num; + self.sum = sum; + } + if pixel_width != self.pixel_width { self.output.invalidate_back(); self.output.invalidate_derivative(); @@ -266,7 +304,6 @@ impl FunctionEntry { self.max_x = max_x; self.pixel_width = pixel_width; } else if ((min_x != self.min_x) | (max_x != self.max_x)) && self.output.back.is_some() { - let resolution: f64 = self.pixel_width as f64 / (max_x.abs() + min_x.abs()); let back_cache = self.output.back.as_ref().unwrap(); let x_data: SteppedVector = back_cache @@ -275,31 +312,32 @@ impl FunctionEntry { .collect::>() .into(); - self.output.back = Some( - (0..self.pixel_width) - .map(|x| (x as f64 / resolution as f64) + min_x) - .map(|x| { - if let Some(i) = x_data.get_index(x) { - back_cache[i] - } else { - Value::new(x, self.function.get(x)) - } - }) - .collect(), - ); - // assert_eq!(self.output.back.as_ref().unwrap().len(), self.pixel_width); - - let derivative_cache = self.output.derivative.as_ref().unwrap(); - let new_data = (0..self.pixel_width) - .map(|x| (x as f64 / resolution as f64) + min_x) + let back_data: Vec = resolution_iter + .iter() + .cloned() .map(|x| { if let Some(i) = x_data.get_index(x) { - derivative_cache[i] + back_cache[i] } else { - Value::new(x, self.function.get_derivative_1(x)) + Value::new(x, self.function.get(x)) } }) .collect(); + assert_eq!(back_data.len(), self.pixel_width); + self.output.back = Some(back_data); + + let derivative_cache = self.output.derivative.as_ref().unwrap(); + let new_data: Vec = resolution_iter + .iter() + .map(|x| { + if let Some(i) = x_data.get_index(*x) { + derivative_cache[i] + } else { + Value::new(*x, self.function.get_derivative_1(*x)) + } + }) + .collect(); + assert_eq!(new_data.len(), self.pixel_width); self.output.derivative = Some(new_data); } else { @@ -326,194 +364,229 @@ impl FunctionEntry { // Calculates extrema if do_extrema { - self.output.extrema = Some( - newtons_method( - threshold, - self.min_x..self.max_x, - self.output.derivative.to_owned().unwrap(), - &|x: f64| self.function.get_derivative_1(x), - &|x: f64| self.function.get_derivative_2(x), - ) - .iter() - .map(|x| Value::new(*x, self.function.get(*x))) - .collect(), - ); + self.output.extrema = self.newtons_method_helper(threshold, 1); } // Calculates roots if do_roots { - self.output.roots = Some( - newtons_method( - threshold, - self.min_x..self.max_x, - self.output.back.to_owned().unwrap(), - &|x: f64| self.function.get(x), - &|x: f64| self.function.get_derivative_1(x), - ) - .iter() - .map(|x| Value::new(*x, self.function.get(*x))) - .collect(), - ); + self.output.roots = self.newtons_method_helper(threshold, 0); } - self.output.display( - plot_ui, - self.get_func_str(), - self.function.get_derivative_str(), - (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64), - self.derivative, - extrema, - roots, - ) + { + let func_str = self.get_func_str(); + let derivative_str = self.function.get_derivative_str(); + let step = + (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64); + let derivative_enabled = self.derivative; + // Plot back data + plot_ui.line( + Line::new(Values::from_values(self.output.back.clone().unwrap())) + .color(Color32::RED) + .name(func_str), + ); + + // Plot derivative data + if derivative_enabled { + if let Some(derivative_data) = self.output.derivative.clone() { + plot_ui.line( + Line::new(Values::from_values(derivative_data)) + .color(Color32::GREEN) + .name(derivative_str), + ); + } + } + + // Plot extrema points + if extrema { + if let Some(extrema_data) = self.output.extrema.clone() { + plot_ui.points( + Points::new(Values::from_values(extrema_data)) + .color(Color32::YELLOW) + .name("Extrema") + .radius(5.0), + ); + } + } + + // Plot roots points + if roots { + if let Some(roots_data) = self.output.roots.clone() { + plot_ui.points( + Points::new(Values::from_values(roots_data)) + .color(Color32::LIGHT_BLUE) + .name("Root") + .radius(5.0), + ); + } + } + + // Plot integral data + if let Some(integral_data) = self.output.integral.clone() { + plot_ui.bar_chart( + BarChart::new(integral_data.0) + .color(Color32::BLUE) + .width(step), + ); + + // return value rounded to 8 decimal places + crate::misc::decimal_round(integral_data.1, 8) + } else { + f64::NAN // return NaN if integrals are disabled + } + } } } #[cfg(test)] -fn verify_function( - integral_num: usize, pixel_width: usize, function: &mut FunctionEntry, - back_values_target: Vec<(f64, f64)>, area_target: f64, -) { - { - let (back_values, bars, derivative) = function.run_back(); - assert!(derivative.is_some()); - assert!(bars.is_none()); - assert_eq!(back_values.len(), pixel_width); - let back_values_tuple: Vec<(f64, f64)> = - back_values.iter().map(|ele| (ele.x, ele.y)).collect(); - assert_eq!(back_values_tuple, back_values_target); +mod tests { + use super::*; + + fn verify_function( + integral_num: usize, pixel_width: usize, function: &mut FunctionEntry, + back_values_target: Vec<(f64, f64)>, area_target: f64, + ) { + { + let (back_values, bars, derivative) = function.run_back(); + assert!(derivative.is_some()); + assert!(bars.is_none()); + assert_eq!(back_values.len(), pixel_width); + let back_values_tuple: Vec<(f64, f64)> = + back_values.iter().map(|ele| (ele.x, ele.y)).collect(); + assert_eq!(back_values_tuple, back_values_target); + } + + { + *function = function.clone().integral(true); + let (back_values, bars, derivative) = function.run_back(); + assert!(derivative.is_some()); + assert!(bars.is_some()); + assert_eq!(back_values.len(), pixel_width); + + assert_eq!(bars.clone().unwrap().1, area_target); + + let vec_bars = bars.unwrap().0; + assert_eq!(vec_bars.len(), integral_num); + + let back_values_tuple: Vec<(f64, f64)> = + back_values.iter().map(|ele| (ele.x, ele.y)).collect(); + assert_eq!(back_values_tuple, back_values_target); + } + + { + let (back_values, bars, derivative) = function.run_back(); + assert!(derivative.is_some()); + + assert!(bars.is_some()); + assert_eq!(back_values.len(), pixel_width); + assert_eq!(bars.clone().unwrap().1, area_target); + let bars_unwrapped = bars.unwrap(); + + assert_eq!(bars_unwrapped.0.iter().len(), integral_num); + } } - { - *function = function.clone().integral(true); - let (back_values, bars, derivative) = function.run_back(); - assert!(derivative.is_some()); - assert!(bars.is_some()); - assert_eq!(back_values.len(), pixel_width); + #[test] + fn left_function_test() { + let integral_num = 10; + let pixel_width = 10; - assert_eq!(bars.clone().unwrap().1, area_target); + let mut function = FunctionEntry::default() + .update_riemann(RiemannSum::Left) + .pixel_width(pixel_width) + .integral_num(integral_num) + .integral_bounds(-1.0, 1.0); - let vec_bars = bars.unwrap().0; - assert_eq!(vec_bars.len(), integral_num); + let back_values_target = vec![ + (-1.0, 1.0), + (-0.8, 0.6400000000000001), + (-0.6, 0.36), + (-0.4, 0.16000000000000003), + (-0.19999999999999996, 0.03999999999999998), + (0.0, 0.0), + (0.19999999999999996, 0.03999999999999998), + (0.3999999999999999, 0.15999999999999992), + (0.6000000000000001, 0.3600000000000001), + (0.8, 0.6400000000000001), + ]; - let back_values_tuple: Vec<(f64, f64)> = - back_values.iter().map(|ele| (ele.x, ele.y)).collect(); - assert_eq!(back_values_tuple, back_values_target); + let area_target = 0.9600000000000001; + + verify_function( + integral_num, + pixel_width, + &mut function, + back_values_target, + area_target, + ); } - { - let (back_values, bars, derivative) = function.run_back(); - assert!(derivative.is_some()); + #[test] + fn middle_function_test() { + let integral_num = 10; + let pixel_width = 10; - assert!(bars.is_some()); - assert_eq!(back_values.len(), pixel_width); - assert_eq!(bars.clone().unwrap().1, area_target); - let bars_unwrapped = bars.unwrap(); + let mut function = FunctionEntry::default() + .update_riemann(RiemannSum::Middle) + .pixel_width(pixel_width) + .integral_num(integral_num) + .integral_bounds(-1.0, 1.0); - assert_eq!(bars_unwrapped.0.iter().len(), integral_num); + let back_values_target = vec![ + (-1.0, 1.0), + (-0.8, 0.6400000000000001), + (-0.6, 0.36), + (-0.4, 0.16000000000000003), + (-0.19999999999999996, 0.03999999999999998), + (0.0, 0.0), + (0.19999999999999996, 0.03999999999999998), + (0.3999999999999999, 0.15999999999999992), + (0.6000000000000001, 0.3600000000000001), + (0.8, 0.6400000000000001), + ]; + + let area_target = 0.92; + + verify_function( + integral_num, + pixel_width, + &mut function, + back_values_target, + area_target, + ); + } + + #[test] + fn right_function_test() { + let integral_num = 10; + let pixel_width = 10; + + let mut function = FunctionEntry::default() + .update_riemann(RiemannSum::Right) + .pixel_width(pixel_width) + .integral_num(integral_num) + .integral_bounds(-1.0, 1.0); + + let back_values_target = vec![ + (-1.0, 1.0), + (-0.8, 0.6400000000000001), + (-0.6, 0.36), + (-0.4, 0.16000000000000003), + (-0.19999999999999996, 0.03999999999999998), + (0.0, 0.0), + (0.19999999999999996, 0.03999999999999998), + (0.3999999999999999, 0.15999999999999992), + (0.6000000000000001, 0.3600000000000001), + (0.8, 0.6400000000000001), + ]; + + let area_target = 0.8800000000000001; + + verify_function( + integral_num, + pixel_width, + &mut function, + back_values_target, + area_target, + ); } } - -#[test] -fn left_function_test() { - let integral_num = 10; - let pixel_width = 10; - - let mut function = FunctionEntry::empty() - .update_riemann(RiemannSum::Left) - .pixel_width(pixel_width) - .integral_num(integral_num) - .integral_bounds(-1.0, 1.0); - - let back_values_target = vec![ - (-1.0, 1.0), - (-0.8, 0.6400000000000001), - (-0.6, 0.36), - (-0.4, 0.16000000000000003), - (-0.19999999999999996, 0.03999999999999998), - (0.0, 0.0), - (0.19999999999999996, 0.03999999999999998), - (0.3999999999999999, 0.15999999999999992), - (0.6000000000000001, 0.3600000000000001), - (0.8, 0.6400000000000001), - ]; - - let area_target = 0.9600000000000001; - - verify_function( - integral_num, - pixel_width, - &mut function, - back_values_target, - area_target, - ); -} - -#[test] -fn middle_function_test() { - let integral_num = 10; - let pixel_width = 10; - - let mut function = FunctionEntry::empty() - .update_riemann(RiemannSum::Middle) - .pixel_width(pixel_width) - .integral_num(integral_num) - .integral_bounds(-1.0, 1.0); - - let back_values_target = vec![ - (-1.0, 1.0), - (-0.8, 0.6400000000000001), - (-0.6, 0.36), - (-0.4, 0.16000000000000003), - (-0.19999999999999996, 0.03999999999999998), - (0.0, 0.0), - (0.19999999999999996, 0.03999999999999998), - (0.3999999999999999, 0.15999999999999992), - (0.6000000000000001, 0.3600000000000001), - (0.8, 0.6400000000000001), - ]; - - let area_target = 0.92; - - verify_function( - integral_num, - pixel_width, - &mut function, - back_values_target, - area_target, - ); -} - -#[test] -fn right_function_test() { - let integral_num = 10; - let pixel_width = 10; - - let mut function = FunctionEntry::empty() - .update_riemann(RiemannSum::Right) - .pixel_width(pixel_width) - .integral_num(integral_num) - .integral_bounds(-1.0, 1.0); - - let back_values_target = vec![ - (-1.0, 1.0), - (-0.8, 0.6400000000000001), - (-0.6, 0.36), - (-0.4, 0.16000000000000003), - (-0.19999999999999996, 0.03999999999999998), - (0.0, 0.0), - (0.19999999999999996, 0.03999999999999998), - (0.3999999999999999, 0.15999999999999992), - (0.6000000000000001, 0.3600000000000001), - (0.8, 0.6400000000000001), - ]; - - let area_target = 0.8800000000000001; - - verify_function( - integral_num, - pixel_width, - &mut function, - back_values_target, - area_target, - ); -} diff --git a/src/function_output.rs b/src/function_output.rs index 4c5b68e..76c81d1 100644 --- a/src/function_output.rs +++ b/src/function_output.rs @@ -1,11 +1,4 @@ -use crate::misc::decimal_round; -use eframe::{ - egui::{ - plot::{BarChart, Line, PlotUi, Points, Value, Values}, - widgets::plot::Bar, - }, - epaint::Color32, -}; +use eframe::egui::{plot::Value, widgets::plot::Bar}; #[derive(Clone)] pub struct FunctionOutput { @@ -45,68 +38,4 @@ impl FunctionOutput { /// Invalidate Derivative data pub fn invalidate_derivative(&mut self) { self.derivative = None; } - - /// Display output on PlotUi `plot_ui` - /// Returns `f64` containing rounded integral area (if integrals are - /// disabled, it returns `f64::NAN`) - #[allow(clippy::too_many_arguments)] - pub fn display( - &self, plot_ui: &mut PlotUi, func_str: &str, derivative_str: &str, step: f64, - derivative_enabled: bool, extrema: bool, roots: bool, - ) -> f64 { - // Plot back data - plot_ui.line( - Line::new(Values::from_values(self.back.clone().unwrap())) - .color(Color32::RED) - .name(func_str), - ); - - // Plot derivative data - if derivative_enabled { - if let Some(derivative_data) = self.derivative.clone() { - plot_ui.line( - Line::new(Values::from_values(derivative_data)) - .color(Color32::GREEN) - .name(derivative_str), - ); - } - } - - // Plot extrema points - if extrema { - if let Some(extrema_data) = self.extrema.clone() { - plot_ui.points( - Points::new(Values::from_values(extrema_data)) - .color(Color32::YELLOW) - .name("Extrema") - .radius(5.0), - ); - } - } - - // Plot roots points - if roots { - if let Some(roots_data) = self.roots.clone() { - plot_ui.points( - Points::new(Values::from_values(roots_data)) - .color(Color32::LIGHT_BLUE) - .name("Root") - .radius(5.0), - ); - } - } - - // Plot integral data - if let Some(integral_data) = self.integral.clone() { - plot_ui.bar_chart( - BarChart::new(integral_data.0) - .color(Color32::BLUE) - .width(step), - ); - - decimal_round(integral_data.1, 8) // return value rounded to 8 decimal places - } else { - f64::NAN // return NaN if integrals are disabled - } - } } diff --git a/src/main.rs b/src/main.rs index a225327..9866301 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,15 +15,9 @@ fn main() { tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); - let options = eframe::NativeOptions { - transparent: true, - drag_and_drop_support: true, - ..Default::default() - }; - eframe::run_native( "(Yet-to-be-named) Graphing Software", - options, + eframe::NativeOptions::default(), Box::new(|cc| Box::new(egui_app::MathApp::new(cc))), ); } diff --git a/src/misc.rs b/src/misc.rs index 762b409..29743ba 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -1,3 +1,6 @@ +use eframe::egui::plot::Value as EguiValue; +use serde_json::Value as JsonValue; + /// `SteppedVector` is used in order to efficiently sort through an ordered /// `Vec` Used in order to speedup the processing of cached data when /// moving horizontally without zoom in `FunctionEntry`. Before this struct, the @@ -98,29 +101,54 @@ impl From> for SteppedVector { } } -#[test] -fn stepped_vector_test() { - let min: i32 = -10; - let max: i32 = 10; - let data: Vec = (min..=max).map(|x| x as f64).collect(); - let len_data = data.len(); - let stepped_vector: SteppedVector = data.into(); +#[derive(PartialEq, Debug)] +pub struct JsonFileOutput { + pub help_expr: String, + pub help_vars: String, + pub help_panel: String, + pub help_function: String, + pub help_other: String, + pub license_info: String, +} - assert_eq!(stepped_vector.get_min(), min as f64); - assert_eq!(stepped_vector.get_max(), max as f64); +/// Helps parsing text data from `text.json` +pub struct SerdeValueHelper { + value: JsonValue, +} - assert_eq!(stepped_vector.get_index(min as f64), Some(0)); - assert_eq!(stepped_vector.get_index(max as f64), Some(len_data - 1)); - - for i in min..=max { - assert_eq!( - stepped_vector.get_index(i as f64), - Some((i + min.abs()) as usize) - ); +impl SerdeValueHelper { + pub fn new(string: &str) -> Self { + Self { + value: serde_json::from_str(string).unwrap(), + } } - assert_eq!(stepped_vector.get_index((min - 1) as f64), None); - assert_eq!(stepped_vector.get_index((max + 1) as f64), None); + /// Parses an array of strings at `self.value[key]` as a multiline string + fn parse_multiline(&self, key: &str) -> String { + (&self.value[key]) + .as_array() + .unwrap() + .iter() + .map(|ele| ele.as_str().unwrap()) + .fold(String::new(), |s, l| s + l + "\n") + .trim_end() + .to_owned() + } + + /// Parses `self.value[key]` as a single line string + fn parse_singleline(&self, key: &str) -> String { self.value[key].as_str().unwrap().to_owned() } + + /// Used to parse `text.json` + pub fn parse_values(&self) -> JsonFileOutput { + JsonFileOutput { + help_expr: self.parse_multiline("help_expr"), + help_vars: self.parse_multiline("help_vars"), + help_panel: self.parse_multiline("help_panel"), + help_function: self.parse_multiline("help_function"), + help_other: self.parse_multiline("help_other"), + license_info: self.parse_singleline("license_info"), + } + } } /// Rounds f64 to `n` decimal places @@ -138,11 +166,11 @@ pub fn decimal_round(x: f64, n: usize) -> f64 { /// `f_1` is f'(x) aka the derivative of f(x) /// The function returns a Vector of `x` values where roots occur pub fn newtons_method( - threshold: f64, range: std::ops::Range, data: Vec, - f: &dyn Fn(f64) -> f64, f_1: &dyn Fn(f64) -> f64, + threshold: f64, range: std::ops::Range, data: Vec, f: &dyn Fn(f64) -> f64, + f_1: &dyn Fn(f64) -> f64, ) -> Vec { let mut output_list: Vec = Vec::new(); - let mut last_ele_option: Option = None; + let mut last_ele_option: Option = None; for ele in data.iter() { if last_ele_option.is_none() { last_ele_option = Some(*ele); @@ -198,48 +226,77 @@ pub fn newtons_method( output_list } -#[derive(PartialEq, Debug)] -pub struct JsonFileOutput { - pub help_expr: String, - pub help_vars: String, - pub help_panel: String, - pub help_function: String, - pub help_other: String, - pub license_info: String, +// Returns a vector of length `max_i` starting at value `min_x` with resolution +// of `resolution` +pub fn resolution_helper(max_i: usize, min_x: f64, resolution: f64) -> Vec { + (0..max_i) + .map(|x| (x as f64 / resolution as f64) + min_x) + .collect() } -pub struct SerdeValueHelper { - value: serde_json::Value, -} +#[cfg(test)] +mod tests { + use super::*; -impl SerdeValueHelper { - pub fn new(string: &str) -> Self { - Self { - value: serde_json::from_str(string).unwrap(), + /// Tests SteppedVector to ensure everything works properly (helped me find + /// a bunch of issues) + #[test] + fn stepped_vector_test() { + let min: i32 = -10; + let max: i32 = 10; + let data: Vec = (min..=max).map(|x| x as f64).collect(); + let len_data = data.len(); + let stepped_vector: SteppedVector = data.into(); + + assert_eq!(stepped_vector.get_min(), min as f64); + assert_eq!(stepped_vector.get_max(), max as f64); + + assert_eq!(stepped_vector.get_index(min as f64), Some(0)); + assert_eq!(stepped_vector.get_index(max as f64), Some(len_data - 1)); + + for i in min..=max { + assert_eq!( + stepped_vector.get_index(i as f64), + Some((i + min.abs()) as usize) + ); } + + assert_eq!(stepped_vector.get_index((min - 1) as f64), None); + assert_eq!(stepped_vector.get_index((max + 1) as f64), None); } - fn parse_multiline(&self, key: &str) -> String { - (&self.value[key]) - .as_array() - .unwrap() - .iter() - .map(|ele| ele.as_str().unwrap()) - .fold(String::new(), |s, l| s + l + "\n") - .trim_end() - .to_owned() + /// Ensures decimal_round returns correct values + #[test] + fn decimal_round_test() { + assert_eq!(decimal_round(0.00001, 1), 0.0); + assert_eq!(decimal_round(0.00001, 2), 0.0); + assert_eq!(decimal_round(0.00001, 3), 0.0); + assert_eq!(decimal_round(0.00001, 4), 0.0); + assert_eq!(decimal_round(0.00001, 5), 0.00001); + + assert_eq!(decimal_round(0.12345, 1), 0.1); + assert_eq!(decimal_round(0.12345, 2), 0.12); + assert_eq!(decimal_round(0.12345, 3), 0.123); + assert_eq!(decimal_round(0.12345, 4), 0.1235); // rounds up + assert_eq!(decimal_round(0.12345, 5), 0.12345); + + assert_eq!(decimal_round(1.9, 0), 2.0); + assert_eq!(decimal_round(1.9, 1), 1.9); } - fn parse_singleline(&self, key: &str) -> String { self.value[key].as_str().unwrap().to_owned() } + /// Tests `resolution_helper` to make sure it returns expected output + #[test] + fn resolution_helper_test() { + assert_eq!( + resolution_helper(10, 1.0, 1.0), + vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + ); - pub fn parse_values(&self) -> JsonFileOutput { - JsonFileOutput { - help_expr: self.parse_multiline("help_expr"), - help_vars: self.parse_multiline("help_vars"), - help_panel: self.parse_multiline("help_panel"), - help_function: self.parse_multiline("help_function"), - help_other: self.parse_multiline("help_other"), - license_info: self.parse_singleline("license_info"), - } + assert_eq!( + resolution_helper(5, -2.0, 1.0), + vec![-2.0, -1.0, 0.0, 1.0, 2.0] + ); + + assert_eq!(resolution_helper(3, -2.0, 1.0), vec![-2.0, -1.0, 0.0]); } } diff --git a/src/parsing.rs b/src/parsing.rs index 2c0a59d..13ab3c5 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -204,7 +204,7 @@ pub fn test_func(function_string: &str) -> Option { } #[cfg(test)] -mod test { +mod tests { use super::*; /// returns if function with string `func_str` is valid after processing