symbolic: init

This commit is contained in:
Simon Gardling 2025-12-05 12:17:18 -05:00
parent 8a5d9f1cd5
commit 9677e8f8b4
Signed by: titaniumtown
GPG Key ID: 9AB28AC10ECE533D
5 changed files with 441 additions and 15 deletions

View File

@ -46,7 +46,7 @@ fn button_area_button<'a>(text: impl Into<WidgetText>) -> Button<'a> {
impl FunctionManager { impl FunctionManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
functions: Vec::new() functions: Vec::new(),
} }
} }

View File

@ -6,6 +6,7 @@ mod function_entry;
mod function_manager; mod function_manager;
mod math_app; mod math_app;
mod misc; mod misc;
pub mod symbolic;
mod unicode_helper; mod unicode_helper;
mod widgets; mod widgets;

View File

@ -150,20 +150,6 @@ pub fn step_helper(max_i: usize, min_x: f64, step: f64) -> Vec<f64> {
.collect() .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<char> {
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; pub const HASH_LENGTH: usize = 8;
/// Represents bytes used to represent hash info /// Represents bytes used to represent hash info

210
src/symbolic.rs Normal file
View File

@ -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<SymbolicValue> {
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<SymbolicRepr> {
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<SymbolicRepr> {
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
}

229
tests/symbolic.rs Normal file
View File

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