use core::cmp::Ordering;
use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(from = "serde_json::Number", into = "serde_json::Number")]
pub struct Real {
text: String,
value: f64,
}
impl Real {
#[must_use]
pub fn from_f64(value: f64) -> Self {
Self {
text: format!("{value}"),
value,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
#[must_use]
pub fn as_f64(&self) -> f64 {
self.value
}
#[must_use]
pub fn is_finite(&self) -> bool {
self.value.is_finite()
}
#[must_use]
pub fn semantic_cmp(&self, other: &Self) -> Option<Ordering> {
self.value.partial_cmp(&other.value)
}
}
impl PartialEq for Real {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
}
}
impl fmt::Display for Real {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.text)
}
}
impl From<f64> for Real {
fn from(value: f64) -> Self {
Self::from_f64(value)
}
}
impl From<serde_json::Number> for Real {
fn from(n: serde_json::Number) -> Self {
let value = n.as_f64().unwrap_or(f64::INFINITY);
Self {
text: n.to_string(),
value,
}
}
}
impl From<Real> for serde_json::Number {
fn from(r: Real) -> Self {
serde_json::from_str(&r.text).unwrap_or_else(|_| {
serde_json::Number::from_f64(r.value)
.unwrap_or_else(|| serde_json::Number::from(0))
})
}
}
impl core::str::FromStr for Real {
type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let n: serde_json::Number = serde_json::from_str(s)
.map_err(|_| crate::error::ParseError::new("REAL", "not a number", s))?;
Ok(Self::from(n))
}
}
impl crate::base::SemanticOrd for Real {
fn semantic_cmp(&self, other: &Self) -> Option<Ordering> {
Self::semantic_cmp(self, other)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_measured_precision_survives_a_round_trip() {
let fine: Real = "1.50".parse().unwrap();
assert_eq!(fine.as_str(), "1.50");
#[allow(clippy::float_cmp, reason = "the whole point is that these are the same f64")]
{
assert_eq!(fine.as_f64(), 1.5);
}
let json = serde_json::to_string(&fine).unwrap();
assert_eq!(json, "1.50", "the digits were normalised away");
let back: Real = serde_json::from_str(&json).unwrap();
assert_eq!(back, fine);
assert_eq!(back.as_str(), "1.50");
}
#[test]
fn equality_and_order_answer_different_questions() {
let coarse: Real = "1.5".parse().unwrap();
let fine: Real = "1.50".parse().unwrap();
assert_ne!(coarse, fine, "different records");
assert_eq!(
coarse.semantic_cmp(&fine),
Some(Ordering::Equal),
"the same number"
);
let bigger: Real = "2".parse().unwrap();
assert_eq!(coarse.semantic_cmp(&bigger), Some(Ordering::Less));
assert_eq!(bigger.semantic_cmp(&coarse), Some(Ordering::Greater));
}
#[test]
fn a_computed_value_does_not_invent_digits() {
assert_eq!(Real::from_f64(1.5).as_str(), "1.5");
assert_eq!(Real::from_f64(184.0).as_str(), "184");
assert_eq!(Real::from_f64(-0.25).as_str(), "-0.25");
}
#[test]
fn a_real_that_never_saw_a_constructor_is_still_readable() {
let r: Real = serde_json::from_str("1e400").expect("JSON permits it");
assert!(!r.is_finite(), "out of range reads as infinite, not a panic");
assert_eq!(r.as_str(), "1e+400", "the digits are still what arrived");
}
#[test]
fn every_digit_survives_and_only_the_exponent_form_is_normalised() {
for text in [
"1.50", "0.10",
"184.0",
"0.000",
"-0.0",
"184",
"1e-5",
"1e+5",
"12345678901234567890", "3.14159265358979323846", ] {
let r: Real = text.parse().expect(text);
assert_eq!(r.as_str(), text, "{text} was normalised");
}
for (written, stored) in [("1e5", "1e+5"), ("1E5", "1e+5")] {
let r: Real = written.parse().expect(written);
assert_eq!(r.as_str(), stored);
#[allow(clippy::float_cmp, reason = "an exponent form denotes an exact integer")]
{
assert_eq!(r.as_f64(), 100_000.0, "the value is untouched");
}
}
}
#[test]
fn a_real_reports_finiteness_both_ways_and_displays_its_digits() {
let ordinary: Real = "1.50".parse().unwrap();
assert!(ordinary.is_finite(), "a plain number is finite");
assert_eq!(ordinary.to_string(), "1.50", "Display must write the digits");
let huge: Real = "1e400".parse().unwrap();
assert!(!huge.is_finite(), "out of `f64` range is not finite");
assert_eq!(huge.to_string(), "1e+400");
}
#[test]
fn an_interval_over_reals_contains_what_it_should() {
use crate::base::{Interval, SemanticOrd};
let low: Real = "1.0".parse().unwrap();
let high: Real = "10.00".parse().unwrap();
let range = Interval::closed(low.clone(), high.clone()).unwrap();
assert!(range.contains(&"5".parse::<Real>().unwrap()));
assert!(range.contains(&low), "a closed bound is included");
assert!(range.contains(&high));
assert!(range.contains(&"10.0".parse::<Real>().unwrap()));
assert!(!range.contains(&"10.5".parse::<Real>().unwrap()));
assert_eq!(
SemanticOrd::semantic_cmp(&low, &high),
Some(Ordering::Less)
);
}
#[test]
fn text_that_is_not_a_number_is_refused() {
assert!("".parse::<Real>().is_err());
assert!("1.2.3".parse::<Real>().is_err());
assert!("NaN".parse::<Real>().is_err());
}
}