symbolic: init
This commit is contained in:
parent
8a5d9f1cd5
commit
9677e8f8b4
@ -46,7 +46,7 @@ fn button_area_button<'a>(text: impl Into<WidgetText>) -> Button<'a> {
|
||||
impl FunctionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
functions: Vec::new()
|
||||
functions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ mod function_entry;
|
||||
mod function_manager;
|
||||
mod math_app;
|
||||
mod misc;
|
||||
pub mod symbolic;
|
||||
mod unicode_helper;
|
||||
mod widgets;
|
||||
|
||||
|
||||
14
src/misc.rs
14
src/misc.rs
@ -150,20 +150,6 @@ pub fn step_helper(max_i: usize, min_x: f64, step: f64) -> Vec<f64> {
|
||||
.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;
|
||||
|
||||
/// Represents bytes used to represent hash info
|
||||
|
||||
210
src/symbolic.rs
Normal file
210
src/symbolic.rs
Normal 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
229
tests/symbolic.rs
Normal 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");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user