use crate::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LosslessFloat(f64);
#[derive(Debug, Clone, Copy)]
pub struct LosslessFloatError(f64);
impl fmt::Display for LosslessFloatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "value {} is not precisely representable as f64", self.0)
}
}
impl TryFrom<f64> for LosslessFloat {
type Error = LosslessFloatError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
if value.is_infinite() || value.is_nan() {
return Err(LosslessFloatError(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(LosslessFloatError(value));
}
Ok(Self(value))
}
}
impl serde_core::Serialize for LosslessFloat {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde_core::Serializer,
{
serializer.serialize_f64(self.0)
}
}
impl<'de> serde_core::Deserialize<'de> for LosslessFloat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde_core::Deserializer<'de>,
{
let v = f64::deserialize(deserializer)?;
Self::try_from(v).map_err(serde_core::de::Error::custom)
}
}
impl LosslessFloat {
#[must_use]
pub fn get(self) -> f64 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_precise() {
let f: LosslessFloat = crate::from_str("1.23456789").unwrap();
assert!((f.get() - 1.234_567_89).abs() < 1e-15);
}
#[test]
fn rejects_infinity() {
let result: Result<LosslessFloat, _> = crate::from_str(".inf");
assert!(result.is_err());
}
#[test]
fn rejects_nan() {
let result: Result<LosslessFloat, _> = crate::from_str(".nan");
assert!(result.is_err());
}
#[test]
fn zero() {
let f: LosslessFloat = crate::from_str("0.0").unwrap();
assert!((f.get()).abs() < 1e-15);
}
#[test]
fn negative() {
let f: LosslessFloat = crate::from_str("-1.5").unwrap();
assert!((f.get() + 1.5).abs() < 1e-15);
}
#[test]
fn serializes_transparently() {
let f = LosslessFloat::try_from(2.5).unwrap();
let yaml = crate::to_string(&f).unwrap();
assert!(yaml.contains("2.5"));
}
}