lots of changes

This commit is contained in:
Simon Gardling 2022-03-23 10:14:29 -04:00
parent b3963bb852
commit 6be78c763a
7 changed files with 442 additions and 382 deletions

18
TODO.md
View File

@ -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`

View File

@ -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<String, FontData> = 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();

View File

@ -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<Value> = {
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<Vec<Value>> {
let newtons_method_output: Vec<f64> = 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::<Vec<f64>>()
.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<Value> = 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<Value> = 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,
);
}

View File

@ -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
}
}
}

View File

@ -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))),
);
}

View File

@ -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<f64>` 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<Vec<f64>> for SteppedVector {
}
}
#[test]
fn stepped_vector_test() {
let min: i32 = -10;
let max: i32 = 10;
let data: Vec<f64> = (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<f64>, data: Vec<eframe::egui::plot::Value>,
f: &dyn Fn(f64) -> f64, f_1: &dyn Fn(f64) -> f64,
threshold: f64, range: std::ops::Range<f64>, data: Vec<EguiValue>, f: &dyn Fn(f64) -> f64,
f_1: &dyn Fn(f64) -> f64,
) -> Vec<f64> {
let mut output_list: Vec<f64> = Vec::new();
let mut last_ele_option: Option<eframe::egui::plot::Value> = None;
let mut last_ele_option: Option<EguiValue> = 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<f64> {
(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<f64> = (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]);
}
}

View File

@ -204,7 +204,7 @@ pub fn test_func(function_string: &str) -> Option<String> {
}
#[cfg(test)]
mod test {
mod tests {
use super::*;
/// returns if function with string `func_str` is valid after processing