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 {
|
impl FunctionManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
functions: Vec::new()
|
functions: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
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()
|
.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
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