use crate::value::{IntClampedCast, Value};
pub mod unicode;
pub const DEFAULT_PRECISION: usize = 2;
#[must_use]
pub fn default_fixed_range<T: Value>() -> Option<std::ops::Range<T>> {
const RANGE: std::ops::Range<f64> = 1e-3..1e3;
let s = T::try_cast(RANGE.start).ok()?;
let e = T::try_cast(RANGE.end).ok()?;
Some(s..e)
}
pub trait PolynomialDisplay<T: Value> {
fn format_term(&self, degree: i32, coef: T) -> Option<Term>;
fn format_scaling_formula(&self) -> Option<String> {
None
}
fn format_polynomial<B: std::fmt::Write>(
&self,
buffer: &mut B,
coefficients: &[T],
) -> std::fmt::Result {
let degree = coefficients.len() - 1;
let mut terms = Vec::new();
for (i, &coef) in coefficients.iter().rev().enumerate() {
let degree_ = degree - i;
if let Some(term) = self.format_term(degree_.clamped_cast(), coef) {
terms.push(term);
}
}
if let Some(scaling) = self.format_scaling_formula() {
write!(buffer, "{scaling}, ")?;
}
write!(buffer, "y(x) = ")?;
if terms.is_empty() {
write!(buffer, "0")?;
return Ok(());
}
let term_n = terms.remove(0);
if term_n.sign == Sign::Negative {
write!(buffer, "{}", term_n.sign.char())?;
}
write!(buffer, "{}", term_n.body)?;
for term in terms {
let sign = term.sign.char();
let body = term.body;
write!(buffer, " {sign} {body}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sign {
Positive,
Negative,
}
impl Sign {
pub fn from_coef<T: Value>(coef: T) -> Self {
if coef.is_sign_negative() {
Self::Negative
} else {
Self::Positive
}
}
#[must_use]
pub fn char(&self) -> char {
match self {
Sign::Positive => '+',
Sign::Negative => '-',
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Term {
pub sign: Sign,
pub body: String,
}
impl Term {
#[must_use]
pub fn new(sign: Sign, body: String) -> Self {
Self { sign, body }
}
}
pub fn format_coefficient<T: Value + std::fmt::LowerExp>(
coef: T,
degree: i32,
precision: usize,
) -> Option<String> {
let abs = Value::abs(coef);
if coef.is_zero() || abs <= T::epsilon() {
return None;
}
if coef.abs_sub(T::one()) <= T::epsilon() && degree != 0 {
return Some(String::new());
}
let sci_cutoff = default_fixed_range();
Some(unicode::float(abs, sci_cutoff, precision))
}
#[must_use]
pub fn format_variable(base: &str, subscript: Option<&str>, exp: i32) -> String {
match exp {
0 => String::new(),
1 => base.to_string(),
_ => {
let lbl = unicode::subscript(subscript.unwrap_or_default());
let sup = unicode::superscript(&exp.to_string());
format!("{base}{lbl}{sup}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct DummyBasis;
impl PolynomialDisplay<f64> for DummyBasis {
fn format_term(&self, degree: i32, coef: f64) -> Option<Term> {
let sign = Sign::from_coef(coef);
let coef_str = format_coefficient(coef, 1, DEFAULT_PRECISION)?;
let body = if degree == 0 {
coef_str
} else {
format!("{coef_str}{}", format_variable("x", None, degree))
};
Some(Term::new(sign, body))
}
}
#[test]
fn test_sign_from_coef() {
assert_eq!(Sign::from_coef(1.0), Sign::Positive);
assert_eq!(Sign::from_coef(-1.0), Sign::Negative);
assert_eq!(Sign::from_coef(0.0), Sign::Positive);
}
#[test]
fn test_sign_char() {
assert_eq!(Sign::Positive.char(), '+');
assert_eq!(Sign::Negative.char(), '-');
}
#[test]
fn test_term_new() {
let t = Term::new(Sign::Negative, "3x²".to_string());
assert_eq!(t.sign, Sign::Negative);
assert_eq!(t.body, "3x²");
}
#[test]
fn test_format_coefficient_decimal() {
assert_eq!(format_coefficient(2.5, 1, 2), Some("2.50".to_string()));
assert_eq!(format_coefficient(-2.5, 1, 2), Some("2.50".to_string()));
}
#[test]
fn test_format_coefficient_zero() {
assert_eq!(format_coefficient(0.0, 1, 2), None);
assert_eq!(format_coefficient(1e-20, 1, 2), None);
}
#[test]
fn test_format_coefficient_scientific() {
assert_eq!(format_coefficient(1e5, 1, 2), Some("1.00e5".to_string()));
assert_eq!(format_coefficient(1e-5, 2, 2), Some("1.00e-5".to_string()));
}
#[test]
fn test_format_variable() {
assert_eq!(format_variable("x", None, 0), "");
assert_eq!(format_variable("x", None, 1), "x");
assert_eq!(format_variable("x", Some("1"), 2), "x₁²");
assert_eq!(format_variable("T", None, 3), "T³");
assert_eq!(format_variable("x", None, -2), "x⁻²");
}
#[test]
fn test_format_polynomial_basic() {
let basis = DummyBasis;
let mut buf = String::new();
basis
.format_polynomial(&mut buf, &[2.0, -3.0, 0.0, 4.0])
.unwrap();
assert_eq!(buf, "y(x) = 4.00x³ - 3.00x + 2.00");
}
#[test]
fn test_format_polynomial_all_zero() {
let basis = DummyBasis;
let mut buf = String::new();
basis.format_polynomial(&mut buf, &[0.0, 0.0, 0.0]).unwrap();
assert_eq!(buf, "y(x) = 0");
}
#[test]
fn test_format_polynomial_leading_negative() {
let basis = DummyBasis;
let mut buf = String::new();
basis.format_polynomial(&mut buf, &[-1.0, 2.0]).unwrap();
assert_eq!(buf, "y(x) = 2.00x - 1.00");
}
#[test]
fn test_format_polynomial_single_term() {
let basis = DummyBasis;
let mut buf = String::new();
basis.format_polynomial(&mut buf, &[0.0, 0.0, 5.0]).unwrap();
assert_eq!(buf, "y(x) = 5.00x²");
}
}