use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
#[serde(transparent)]
pub struct StrictFloat(f64);
#[derive(Debug, Clone, Copy)]
pub struct StrictFloatError(f64);
impl fmt::Display for StrictFloatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "value {} is not precisely representable as f64", self.0)
}
}
impl TryFrom<f64> for StrictFloat {
type Error = StrictFloatError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
if value.is_infinite() || value.is_nan() {
return Err(StrictFloatError(value));
}
#[cfg(feature = "fast-float")]
let repr = ryu::Buffer::new().format(value).to_owned();
#[cfg(not(feature = "fast-float"))]
let repr = format!("{value:?}");
let roundtrip: f64 = repr.parse().unwrap_or(f64::NAN);
if roundtrip != value {
return Err(StrictFloatError(value));
}
Ok(Self(value))
}
}
impl<'de> Deserialize<'de> for StrictFloat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = f64::deserialize(deserializer)?;
StrictFloat::try_from(v).map_err(serde::de::Error::custom)
}
}
impl StrictFloat {
#[must_use]
pub fn get(self) -> f64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
#[serde(transparent)]
pub struct Radians(pub f64);
impl<'de> Deserialize<'de> for Radians {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let degrees = f64::deserialize(deserializer)?;
Ok(Radians(degrees.to_radians()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Degrees(pub f64);
impl Degrees {
#[must_use]
pub fn to_radians(self) -> Radians {
Radians(self.0.to_radians())
}
}
impl Radians {
#[must_use]
pub fn to_degrees(self) -> Degrees {
Degrees(self.0.to_degrees())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_float_accepts_precise() {
let sf: StrictFloat = crate::from_str("1.23456789").unwrap();
assert!((sf.get() - 1.234_567_89).abs() < 1e-15);
}
#[test]
fn strict_float_rejects_infinity() {
let result: Result<StrictFloat, _> = crate::from_str(".inf");
assert!(result.is_err());
}
#[test]
fn strict_float_rejects_nan() {
let result: Result<StrictFloat, _> = crate::from_str(".nan");
assert!(result.is_err());
}
#[test]
fn strict_float_zero() {
let sf: StrictFloat = crate::from_str("0.0").unwrap();
assert!((sf.get()).abs() < 1e-15);
}
#[test]
fn strict_float_negative() {
let sf: StrictFloat = crate::from_str("-1.5").unwrap();
assert!((sf.get() + 1.5).abs() < 1e-15);
}
#[test]
fn radians_from_degrees() {
let r: Radians = crate::from_str("180.0").unwrap();
assert!((r.0 - core::f64::consts::PI).abs() < 1e-10);
}
#[test]
fn radians_90() {
let r: Radians = crate::from_str("90.0").unwrap();
assert!((r.0 - core::f64::consts::FRAC_PI_2).abs() < 1e-10);
}
#[test]
fn radians_zero() {
let r: Radians = crate::from_str("0.0").unwrap();
assert!((r.0).abs() < 1e-15);
}
#[test]
fn degrees_roundtrip() {
let d: Degrees = crate::from_str("45.0").unwrap();
let r = d.to_radians();
let back = r.to_degrees();
assert!((back.0 - 45.0).abs() < 1e-10);
}
#[test]
fn degrees_deserialize() {
let d: Degrees = crate::from_str("90.0").unwrap();
assert!((d.0 - 90.0).abs() < 1e-15);
}
#[test]
fn strict_float_serialize() {
let sf = StrictFloat::try_from(2.5).unwrap();
let yaml = crate::to_string(&sf).unwrap();
assert!(yaml.contains("2.5"));
}
#[test]
fn radians_serialize() {
let r = Radians(core::f64::consts::PI);
let yaml = crate::to_string(&r).unwrap();
assert!(yaml.contains("3.14159"));
}
}