use std::{fmt, str::FromStr};
use crate::Error;
pub const MAX_POLYNOMIAL_DEGREE: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Spline {
Linear,
Quadratic,
Cubic,
Polynomial(usize, Option<f64>),
}
impl Spline {
#[must_use]
#[allow(clippy::trivially_copy_pass_by_ref)] pub const fn degree(&self) -> usize {
match *self {
Self::Linear => 1,
Self::Quadratic => 2,
Self::Cubic => 3,
Self::Polynomial(degree, _) => degree,
}
}
#[must_use]
pub const fn min_points(self) -> usize {
self.degree().saturating_add(1)
}
#[must_use]
#[allow(clippy::trivially_copy_pass_by_ref)] pub const fn bounds_factor(&self) -> Option<f64> {
match *self {
Self::Polynomial(_, bounds) => bounds,
_ => None,
}
}
pub fn validate(self) -> Result<(), Error> {
if let Self::Polynomial(degree, bounds) = self {
if !(1..=MAX_POLYNOMIAL_DEGREE).contains(°ree) {
return Err(Error::InvalidDegree { degree, max: MAX_POLYNOMIAL_DEGREE });
}
if let Some(b) = bounds
&& !(b.is_finite() && b >= 0.0)
{
return Err(Error::InvalidBoundsFactor(b));
}
}
Ok(())
}
#[must_use]
pub const fn fallback_for(self, distinct: usize) -> Self {
if distinct >= self.min_points() {
return self;
}
match self {
Self::Linear => Self::Linear,
Self::Quadratic | Self::Cubic => {
if distinct >= 3 {
Self::Quadratic
} else {
Self::Linear
}
}
Self::Polynomial(_, bounds) => Self::Polynomial(if distinct > 1 { distinct - 1 } else { 1 }, bounds),
}
}
}
impl fmt::Display for Spline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Linear => f.write_str("Linear"),
Self::Quadratic => f.write_str("Quadratic"),
Self::Cubic => f.write_str("Cubic"),
Self::Polynomial(degree, Some(b)) => write!(f, "Polynomial(degree: {degree}, bounds_factor: {b})"),
Self::Polynomial(degree, None) => write!(f, "Polynomial(degree: {degree}, bounds_factor: None)"),
}
}
}
impl FromStr for Spline {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let fail = || Error::Parse { kind: "spline", input: s.to_owned() };
let t = s.trim();
let spline = if t.eq_ignore_ascii_case("linear") {
Self::Linear
} else if t.eq_ignore_ascii_case("quadratic") {
Self::Quadratic
} else if t.eq_ignore_ascii_case("cubic") {
Self::Cubic
} else {
let args = t.strip_prefix("Polynomial(").and_then(|r| r.strip_suffix(')')).ok_or_else(fail)?;
let (degree, bounds) = args.split_once(',').ok_or_else(fail)?;
let degree = degree.trim().strip_prefix("degree:").ok_or_else(fail)?.trim().parse().map_err(|_| fail())?;
let bounds = bounds.trim().strip_prefix("bounds_factor:").ok_or_else(fail)?.trim();
let bounds = if bounds == "None" { None } else { Some(bounds.parse().map_err(|_| fail())?) };
Self::Polynomial(degree, bounds)
};
spline.validate()?;
Ok(spline)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_round_trips() {
for s in [Spline::Linear, Spline::Quadratic, Spline::Cubic, Spline::Polynomial(5, Some(1.5)), Spline::Polynomial(2, None)] {
assert_eq!(s.to_string().parse::<Spline>(), Ok(s));
}
assert_eq!("cubic".parse::<Spline>(), Ok(Spline::Cubic));
assert!("Akima".parse::<Spline>().is_err());
assert!(matches!("Polynomial(degree: 0, bounds_factor: None)".parse::<Spline>(), Err(Error::InvalidDegree { .. })));
}
#[test]
fn validate_rejects_bad_parameters() {
assert!(Spline::Polynomial(0, None).validate().is_err());
assert!(Spline::Polynomial(MAX_POLYNOMIAL_DEGREE + 1, None).validate().is_err());
assert!(Spline::Polynomial(3, Some(-1.0)).validate().is_err());
assert!(Spline::Polynomial(3, Some(f64::NAN)).validate().is_err());
assert!(Spline::Polynomial(MAX_POLYNOMIAL_DEGREE, Some(0.0)).validate().is_ok());
}
#[test]
fn absurd_degrees_do_not_overflow() {
let s = Spline::Polynomial(usize::MAX, None);
assert_eq!(s.min_points(), usize::MAX);
assert_eq!(s.fallback_for(3), Spline::Polynomial(2, None));
assert!(s.validate().is_err());
}
#[test]
fn fallback_steps_down_within_the_family() {
assert_eq!(Spline::Cubic.fallback_for(4), Spline::Cubic);
assert_eq!(Spline::Cubic.fallback_for(3), Spline::Quadratic);
assert_eq!(Spline::Cubic.fallback_for(2), Spline::Linear);
assert_eq!(Spline::Quadratic.fallback_for(2), Spline::Linear);
assert_eq!(Spline::Polynomial(6, Some(1.0)).fallback_for(4), Spline::Polynomial(3, Some(1.0)));
assert_eq!(Spline::Polynomial(6, None).fallback_for(1), Spline::Polynomial(1, None));
}
}