use ytbn_graphing_software::{AppSettings, EguiHelper, FunctionEntry, Riemann}; fn app_settings_constructor( sum: Riemann, integral_min_x: f64, integral_max_x: f64, pixel_width: usize, integral_num: usize, min_x: f64, max_x: f64, ) -> AppSettings { AppSettings { riemann_sum: sum, integral_min_x, integral_max_x, min_x, max_x, integral_changed: true, integral_num, do_extrema: false, do_roots: false, plot_width: pixel_width, } } static BACK_TARGET: [(f64, f64); 11] = [ (-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), (1.0, 1.0), ]; static DERIVATIVE_TARGET: [(f64, f64); 11] = [ (-1.0, -2.0), (-0.8, -1.6), (-0.6, -1.2), (-0.4, -0.8), (-0.19999999999999996, -0.3999999999999999), (0.0, 0.0), (0.19999999999999996, 0.3999999999999999), (0.3999999999999999, 0.7999999999999998), (0.6000000000000001, 1.2000000000000002), (0.8, 1.6), (1.0, 2.0), ]; #[cfg(test)] fn do_test(sum: Riemann, area_target: f64) { let settings = app_settings_constructor(sum, -1.0, 1.0, 10, 10, -1.0, 1.0); let mut function = FunctionEntry::default(); function.update_string("x^2"); function.integral = true; function.derivative = true; let mut settings = settings; { function.calculate(true, true, false, settings); assert!(!function.back_data.is_empty()); assert_eq!(function.back_data.len(), settings.plot_width + 1); assert!(function.integral); assert!(function.derivative); assert_eq!(!function.root_data.is_empty(), settings.do_roots); assert_eq!(!function.extrema_data.is_empty(), settings.do_extrema); assert!(!function.derivative_data.is_empty()); assert!(function.integral_data.is_some()); assert_eq!(function.integral_data.clone().unwrap().1, area_target); let a = function.derivative_data.clone().to_tuple(); assert_eq!(a.len(), DERIVATIVE_TARGET.len()); for i in 0..a.len() { if !emath::almost_equal(a[i].0 as f32, DERIVATIVE_TARGET[i].0 as f32, f32::EPSILON) | !emath::almost_equal(a[i].1 as f32, DERIVATIVE_TARGET[i].1 as f32, f32::EPSILON) { panic!("Expected: {:?}\nGot: {:?}", DERIVATIVE_TARGET, a); } } let a_1 = function.back_data.clone().to_tuple(); assert_eq!(a_1.len(), BACK_TARGET.len()); assert_eq!(a.len(), BACK_TARGET.len()); for i in 0..a.len() { if !emath::almost_equal(a_1[i].0 as f32, BACK_TARGET[i].0 as f32, f32::EPSILON) | !emath::almost_equal(a_1[i].1 as f32, BACK_TARGET[i].1 as f32, f32::EPSILON) { panic!("Expected: {:?}\nGot: {:?}", BACK_TARGET, a_1); } } } { settings.min_x += 1.0; settings.max_x += 1.0; function.calculate(true, true, false, settings); let a = function .derivative_data .clone() .to_tuple() .iter() .take(6) .cloned() .collect::>(); let b = DERIVATIVE_TARGET .iter() .rev() .take(6) .rev() .cloned() .collect::>(); assert_eq!(a.len(), b.len()); for i in 0..a.len() { if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON) | !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON) { panic!("Expected: {:?}\nGot: {:?}", b, a); } } let a_1 = function .back_data .clone() .to_tuple() .iter() .take(6) .cloned() .collect::>(); let b_1 = BACK_TARGET .iter() .rev() .take(6) .rev() .cloned() .collect::>(); assert_eq!(a_1.len(), b_1.len()); assert_eq!(a.len(), b_1.len()); for i in 0..a.len() { if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON) | !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON) { panic!("Expected: {:?}\nGot: {:?}", b_1, a_1); } } } { settings.min_x -= 2.0; settings.max_x -= 2.0; function.calculate(true, true, false, settings); let a = function .derivative_data .clone() .to_tuple() .iter() .rev() .take(6) .rev() .cloned() .collect::>(); let b = DERIVATIVE_TARGET .iter() .take(6) .cloned() .collect::>(); assert_eq!(a.len(), b.len()); for i in 0..a.len() { if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON) | !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON) { panic!("Expected: {:?}\nGot: {:?}", b, a); } } let a_1 = function .back_data .clone() .to_tuple() .iter() .rev() .take(6) .rev() .cloned() .collect::>(); let b_1 = BACK_TARGET .iter() .take(6) .cloned() .collect::>(); assert_eq!(a_1.len(), b_1.len()); assert_eq!(a.len(), b_1.len()); for i in 0..a.len() { if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON) | !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON) { panic!("Expected: {:?}\nGot: {:?}", b_1, a_1); } } } { function.update_string("sin(x)"); assert!(function.get_test_result().is_none()); assert_eq!(&function.raw_func_str, "sin(x)"); function.integral = false; function.derivative = false; assert!(!function.integral); assert!(!function.derivative); assert!(function.back_data.is_empty()); assert!(function.integral_data.is_none()); assert!(function.root_data.is_empty()); assert!(function.extrema_data.is_empty()); assert!(function.derivative_data.is_empty()); settings.min_x -= 1.0; settings.max_x -= 1.0; function.calculate(true, true, false, settings); assert!(!function.back_data.is_empty()); assert!(function.integral_data.is_none()); assert!(function.root_data.is_empty()); assert!(function.extrema_data.is_empty()); assert!(!function.derivative_data.is_empty()); } } #[test] fn left_function() { do_test(Riemann::Left, 0.9600000000000001); } #[test] fn middle_function() { do_test(Riemann::Middle, 0.92); } #[test] fn right_function() { do_test(Riemann::Right, 0.8800000000000001); } #[test] fn test_extrema() { let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0); settings.do_extrema = true; let mut function = FunctionEntry::default(); function.update_string("x^2 - 4"); // Parabola with vertex at (0, -4) function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // For f(x) = x^2 - 4, f'(x) = 2x // Extrema occurs where f'(x) = 0, so at x = 0 assert!(!function.extrema_data.is_empty()); // Should have exactly one extremum at x = 0 assert_eq!(function.extrema_data.len(), 1); let extremum = function.extrema_data[0]; assert!(emath::almost_equal(extremum.x as f32, 0.0, f32::EPSILON)); assert!(emath::almost_equal(extremum.y as f32, -4.0, f32::EPSILON)); } #[test] fn test_extrema_multiple() { let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0); settings.do_extrema = true; let mut function = FunctionEntry::default(); function.update_string("x^3 - 3*x"); // Cubic with local max and min function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // For f(x) = x^3 - 3x, f'(x) = 3x^2 - 3 // Extrema occur where f'(x) = 0, so at x = ±1 assert!(!function.extrema_data.is_empty()); // Should have exactly two extrema assert_eq!(function.extrema_data.len(), 2); // Sort by x coordinate for consistent testing let mut extrema = function.extrema_data.clone(); extrema.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap()); // First extremum at x = -1, f(-1) = -1 + 3 = 2 assert!(emath::almost_equal(extrema[0].x as f32, -1.0, 0.01)); assert!(emath::almost_equal(extrema[0].y as f32, 2.0, 0.01)); // Second extremum at x = 1, f(1) = 1 - 3 = -2 assert!(emath::almost_equal(extrema[1].x as f32, 1.0, 0.01)); assert!(emath::almost_equal(extrema[1].y as f32, -2.0, 0.01)); } #[test] fn test_extrema_disabled() { let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0); settings.do_extrema = false; // Disable extrema let mut function = FunctionEntry::default(); function.update_string("x^2 - 4"); function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // Extrema data should be empty when disabled assert!(function.extrema_data.is_empty()); } #[test] fn test_roots() { let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0); settings.do_roots = true; let mut function = FunctionEntry::default(); function.update_string("x^2 - 4"); // Parabola crossing x-axis at ±2 function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // For f(x) = x^2 - 4, roots occur where x^2 = 4, so at x = ±2 assert!(!function.root_data.is_empty()); // Should have exactly two roots assert_eq!(function.root_data.len(), 2); // Sort by x coordinate for consistent testing let mut roots = function.root_data.clone(); roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap()); // First root at x = -2 assert!(emath::almost_equal(roots[0].x as f32, -2.0, 0.01)); assert!(emath::almost_equal(roots[0].y as f32, 0.0, 0.001)); // Second root at x = 2 assert!(emath::almost_equal(roots[1].x as f32, 2.0, 0.01)); assert!(emath::almost_equal(roots[1].y as f32, 0.0, 0.001)); } #[test] fn test_roots_single() { let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0); settings.do_roots = true; let mut function = FunctionEntry::default(); function.update_string("x - 1"); // Linear function crossing x-axis at x = 1 function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // For f(x) = x - 1, root occurs at x = 1 assert!(!function.root_data.is_empty()); // Should have exactly one root assert_eq!(function.root_data.len(), 1); let root = function.root_data[0]; assert!(emath::almost_equal(root.x as f32, 1.0, 0.01)); assert!(emath::almost_equal(root.y as f32, 0.0, f32::EPSILON)); } #[test] fn test_roots_disabled() { let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0); settings.do_roots = false; // Disable roots let mut function = FunctionEntry::default(); function.update_string("x^2 - 4"); function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // Root data should be empty when disabled assert!(function.root_data.is_empty()); } #[test] fn test_extrema_and_roots_together() { let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0); settings.do_extrema = true; settings.do_roots = true; let mut function = FunctionEntry::default(); function.update_string("x^2 - 1"); // Parabola with vertex at (0, -1) and roots at ±1 function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // Should have one extremum at x = 0 assert!(!function.extrema_data.is_empty()); assert_eq!(function.extrema_data.len(), 1); let extremum = function.extrema_data[0]; assert!(emath::almost_equal(extremum.x as f32, 0.0, 0.01)); assert!(emath::almost_equal(extremum.y as f32, -1.0, 0.01)); // Should have two roots at x = ±1 assert!(!function.root_data.is_empty()); assert_eq!(function.root_data.len(), 2); let mut roots = function.root_data.clone(); roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap()); assert!(emath::almost_equal(roots[0].x as f32, -1.0, 0.01)); assert!(emath::almost_equal(roots[1].x as f32, 1.0, 0.01)); } #[test] fn test_extrema_no_extrema() { let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0); settings.do_extrema = true; let mut function = FunctionEntry::default(); function.update_string("x"); // Linear function has no extrema function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // Linear function should have no extrema assert!(function.extrema_data.is_empty()); } #[test] fn test_roots_no_roots() { let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0); settings.do_roots = true; let mut function = FunctionEntry::default(); function.update_string("x^2 + 1"); // Parabola that never crosses x-axis function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // Function that never crosses x-axis should have no roots assert!(function.root_data.is_empty()); } #[test] fn test_extrema_and_roots_with_trig() { let mut settings = app_settings_constructor(Riemann::Middle, -4.0, 4.0, 300, 300, -4.0, 4.0); settings.do_extrema = true; settings.do_roots = true; let mut function = FunctionEntry::default(); function.update_string("sin(x)"); // Sine function has extrema at odd multiples of π/2 function.integral = false; function.derivative = false; function.calculate(true, true, false, settings); // Sine function should have extrema in the given range assert!(!function.extrema_data.is_empty()); // Should have multiple extrema (local max/min) assert!(function.extrema_data.len() >= 2); // Check that extrema are at approximately the right locations // Local max at π/2 ≈ 1.57, local min at 3π/2 ≈ 4.71 (outside range) // Local min at -π/2 ≈ -1.57, local max at -3π/2 ≈ -4.71 (outside range) let extrema_x: Vec = function.extrema_data.iter().map(|p| p.x as f32).collect(); // Should have extrema near ±π/2 assert!(extrema_x .iter() .any(|&x| emath::almost_equal(x, std::f32::consts::PI / 2.0, 0.1))); assert!(extrema_x .iter() .any(|&x| emath::almost_equal(x, -std::f32::consts::PI / 2.0, 0.1))); let roots_x: Vec = function.root_data.iter().map(|p| p.x as f32).collect(); assert!(roots_x .iter() .any(|&x| emath::almost_equal(x, std::f32::consts::PI, 0.1))); assert!(roots_x .iter() .any(|&x| emath::almost_equal(x, -std::f32::consts::PI, 0.1))); assert!(roots_x.iter().any(|&x| emath::almost_equal(x, 0.0, 0.1))); }