diff --git a/src/function_manager.rs b/src/function_manager.rs index 8f49905..cc2ee50 100644 --- a/src/function_manager.rs +++ b/src/function_manager.rs @@ -46,7 +46,7 @@ fn button_area_button<'a>(text: impl Into) -> Button<'a> { impl FunctionManager { pub fn new() -> Self { Self { - functions: Vec::new() + functions: Vec::new(), } } diff --git a/src/lib.rs b/src/lib.rs index f4df882..3f43f39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ mod function_entry; mod function_manager; mod math_app; mod misc; +pub mod symbolic; mod unicode_helper; mod widgets; diff --git a/src/misc.rs b/src/misc.rs index ef3eee9..feb4366 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -150,20 +150,6 @@ pub fn step_helper(max_i: usize, min_x: f64, step: f64) -> Vec { .collect() } -// TODO: use in hovering over points -/// Attempts to see what variable `x` is almost -#[allow(dead_code)] -pub fn almost_variable(x: f64) -> Option { - const EPSILON: f32 = f32::EPSILON * 2.0; - if emath::almost_equal(x as f32, std::f32::consts::E, EPSILON) { - Some('e') - } else if emath::almost_equal(x as f32, std::f32::consts::PI, EPSILON) { - Some('π') - } else { - None - } -} - pub const HASH_LENGTH: usize = 8; /// Represents bytes used to represent hash info diff --git a/src/symbolic.rs b/src/symbolic.rs new file mode 100644 index 0000000..79b3420 --- /dev/null +++ b/src/symbolic.rs @@ -0,0 +1,210 @@ +use std::fmt; + +/// Maximum denominator to consider when checking for rational approximations. +const MAX_DENOMINATOR: i64 = 12; + +/// Maximum coefficient to consider for multiples of special constants. +const MAX_COEFFICIENT: i64 = 12; + +/// Represents a symbolic mathematical value. +#[derive(Debug, Clone, PartialEq)] +pub struct SymbolicValue { + /// The original numeric value + value: f64, + /// The symbolic representation + repr: SymbolicRepr, +} + +/// The type of symbolic representation. +#[derive(Debug, Clone, PartialEq)] +enum SymbolicRepr { + /// An integer value + Integer(i64), + /// A simple fraction: numerator / denominator + Fraction { numerator: i64, denominator: i64 }, + /// A multiple of a constant: (numerator / denominator) * constant + ConstantMultiple { + numerator: i64, + denominator: i64, + constant: Constant, + }, +} + +/// Known mathematical constants. +#[derive(Debug, Clone, Copy, PartialEq)] +enum Constant { + Pi, + E, + Sqrt(i64), +} + +impl Constant { + fn value(self) -> f64 { + match self { + Constant::Pi => std::f64::consts::PI, + Constant::E => std::f64::consts::E, + Constant::Sqrt(n) => (n as f64).sqrt(), + } + } + + fn name(self) -> String { + match self { + Constant::Pi => "pi".to_string(), + Constant::E => "e".to_string(), + Constant::Sqrt(n) => format!("sqrt({})", n), + } + } +} + +/// All constants to try, in order of priority. +const CONSTANTS: &[Constant] = &[ + Constant::Pi, + Constant::E, + Constant::Sqrt(2), + Constant::Sqrt(3), + Constant::Sqrt(5), + Constant::Sqrt(6), + Constant::Sqrt(7), +]; + +impl SymbolicValue { + /// Returns the original numeric value. + pub fn numeric_value(&self) -> f64 { + self.value + } +} + +impl fmt::Display for SymbolicValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.repr { + SymbolicRepr::Integer(n) => write!(f, "{}", n), + SymbolicRepr::Fraction { + numerator, + denominator, + } => write!(f, "{}/{}", numerator, denominator), + SymbolicRepr::ConstantMultiple { + numerator, + denominator, + constant, + } => format_constant_multiple(f, *numerator, *denominator, &constant.name()), + } + } +} + +/// Helper function to format a constant multiple like "2pi/3" or "-pi/2" +fn format_constant_multiple( + f: &mut fmt::Formatter<'_>, + numerator: i64, + denominator: i64, + constant: &str, +) -> fmt::Result { + let sign = if numerator < 0 { "-" } else { "" }; + let abs_num = numerator.abs(); + + match (abs_num, denominator) { + (1, 1) => write!(f, "{}{}", sign, constant), + (_, 1) => write!(f, "{}{}{}", sign, abs_num, constant), + (1, _) => write!(f, "{}{}/{}", sign, constant, denominator), + (_, _) => write!(f, "{}{}{}/{}", sign, abs_num, constant, denominator), + } +} + +/// Attempts to find a symbolic representation for the given numeric value. +/// +/// Returns `Some(SymbolicValue)` if the value can be represented symbolically, +/// or `None` if no suitable symbolic representation is found. +/// +/// # Examples +/// +/// ``` +/// use ytbn_graphing_software::symbolic::try_symbolic; +/// use std::f64::consts::PI; +/// +/// let sym = try_symbolic(PI).unwrap(); +/// assert_eq!(sym.to_string(), "pi"); +/// +/// let sym = try_symbolic(PI / 2.0).unwrap(); +/// assert_eq!(sym.to_string(), "pi/2"); +/// ``` +pub fn try_symbolic(x: f64) -> Option { + if !x.is_finite() { + return None; + } + + // Check for zero + if x.abs() < f64::EPSILON { + return Some(SymbolicValue { + value: x, + repr: SymbolicRepr::Integer(0), + }); + } + + // Try each constant in order of preference + for &constant in CONSTANTS { + if let Some(repr) = try_constant_multiple(x, constant) { + return Some(SymbolicValue { value: x, repr }); + } + } + + // Fall back to rational approximation + try_rational(x).map(|repr| SymbolicValue { value: x, repr }) +} + +/// Try to represent x as (numerator/denominator) * constant +fn try_constant_multiple(x: f64, constant: Constant) -> Option { + let c = constant.value(); + + for denom in 1..=MAX_DENOMINATOR { + let num_f = x * (denom as f64) / c; + let num = num_f.round() as i64; + + // Skip if coefficient is zero or too large + if num == 0 || num.abs() > MAX_COEFFICIENT * denom { + continue; + } + + let expected = (num as f64) * c / (denom as f64); + if (x - expected).abs() < f64::EPSILON { + let g = gcd(num.abs(), denom); + return Some(SymbolicRepr::ConstantMultiple { + numerator: num / g, + denominator: denom / g, + constant, + }); + } + } + + None +} + +/// Try to represent x as a simple fraction: numerator/denominator +fn try_rational(x: f64) -> Option { + for denom in 1..=MAX_DENOMINATOR { + let num_f = x * (denom as f64); + let num = num_f.round() as i64; + + if (x - (num as f64) / (denom as f64)).abs() < f64::EPSILON { + let g = gcd(num.abs(), denom); + let (num, denom) = (num / g, denom / g); + + return Some(if denom == 1 { + SymbolicRepr::Integer(num) + } else { + SymbolicRepr::Fraction { + numerator: num, + denominator: denom, + } + }); + } + } + + None +} + +/// Compute the greatest common divisor using Euclidean algorithm. +fn gcd(mut a: i64, mut b: i64) -> i64 { + while b != 0 { + (a, b) = (b, a % b); + } + a +} diff --git a/tests/symbolic.rs b/tests/symbolic.rs new file mode 100644 index 0000000..bc24c3b --- /dev/null +++ b/tests/symbolic.rs @@ -0,0 +1,229 @@ +use std::f64::consts::{E, PI, SQRT_2}; +use ytbn_graphing_software::symbolic::try_symbolic; + +#[test] +fn exact_pi() { + let result = try_symbolic(PI); + assert!(result.is_some()); + let sym = result.unwrap(); + assert_eq!(sym.to_string(), "pi"); +} + +#[test] +fn multiples_of_pi() { + // 2*pi + let result = try_symbolic(2.0 * PI); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "2pi"); + + // 3*pi + let result = try_symbolic(3.0 * PI); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "3pi"); + + // -pi + let result = try_symbolic(-PI); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "-pi"); + + // -2*pi + let result = try_symbolic(-2.0 * PI); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "-2pi"); +} + +#[test] +fn fractions_of_pi() { + // pi/2 + let result = try_symbolic(PI / 2.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "pi/2"); + + // pi/3 + let result = try_symbolic(PI / 3.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "pi/3"); + + // pi/4 + let result = try_symbolic(PI / 4.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "pi/4"); + + // pi/6 + let result = try_symbolic(PI / 6.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "pi/6"); + + // 2pi/3 + let result = try_symbolic(2.0 * PI / 3.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "2pi/3"); + + // 3pi/4 + let result = try_symbolic(3.0 * PI / 4.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "3pi/4"); + + // 5pi/6 + let result = try_symbolic(5.0 * PI / 6.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "5pi/6"); + + // -pi/2 + let result = try_symbolic(-PI / 2.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "-pi/2"); +} + +#[test] +fn exact_e() { + let result = try_symbolic(E); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "e"); +} + +#[test] +fn multiples_of_e() { + // 2e + let result = try_symbolic(2.0 * E); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "2e"); + + // -e + let result = try_symbolic(-E); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "-e"); +} + +#[test] +fn sqrt_2() { + let result = try_symbolic(SQRT_2); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "sqrt(2)"); + + // -sqrt(2) + let result = try_symbolic(-SQRT_2); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "-sqrt(2)"); + + // 2*sqrt(2) + let result = try_symbolic(2.0 * SQRT_2); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "2sqrt(2)"); +} + +#[test] +fn sqrt_3() { + let sqrt_3 = 3.0_f64.sqrt(); + let result = try_symbolic(sqrt_3); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "sqrt(3)"); + + // sqrt(3)/2 - common in trigonometry + let result = try_symbolic(sqrt_3 / 2.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "sqrt(3)/2"); +} + +#[test] +fn simple_fractions() { + // 1/2 + let result = try_symbolic(0.5); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "1/2"); + + // 1/3 + let result = try_symbolic(1.0 / 3.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "1/3"); + + // 2/3 + let result = try_symbolic(2.0 / 3.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "2/3"); + + // 1/4 + let result = try_symbolic(0.25); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "1/4"); + + // 3/4 + let result = try_symbolic(0.75); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "3/4"); + + // -1/2 + let result = try_symbolic(-0.5); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "-1/2"); +} + +#[test] +fn integers() { + // 0 + let result = try_symbolic(0.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "0"); + + // 1 + let result = try_symbolic(1.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "1"); + + // -1 + let result = try_symbolic(-1.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "-1"); + + // 5 + let result = try_symbolic(5.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "5"); +} + +#[test] +fn non_symbolic_values() { + // Some arbitrary irrational number that isn't special + let result = try_symbolic(1.234567890123); + assert!(result.is_none()); + + // A number that's close to but not quite pi + let result = try_symbolic(3.15); + assert!(result.is_none()); +} + +#[test] +fn numeric_value() { + // SymbolicValue should provide the original numeric value + let sym = try_symbolic(PI).unwrap(); + assert!((sym.numeric_value() - PI).abs() < 1e-10); + + let sym = try_symbolic(PI / 2.0).unwrap(); + assert!((sym.numeric_value() - PI / 2.0).abs() < 1e-10); +} + +#[test] +fn zero() { + let result = try_symbolic(0.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "0"); + + // Also test -0.0 + let result = try_symbolic(-0.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "0"); +} + +#[test] +fn special_trig_values() { + // Common values that appear in trigonometry + // sin(pi/4) = cos(pi/4) = sqrt(2)/2 + let result = try_symbolic(SQRT_2 / 2.0); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "sqrt(2)/2"); + + // sin(pi/6) = cos(pi/3) = 1/2 + let result = try_symbolic(0.5); + assert!(result.is_some()); + assert_eq!(result.unwrap().to_string(), "1/2"); +}