Skip to main content

apple_plist/value/
real.rs

1//! The real (floating-point) model carrying the binary codec's width flag.
2
3use std::cmp::Ordering;
4
5/// A property-list real number.
6///
7/// The value is always stored as an `f64`; a crate-internal flag records
8/// whether it originated as a 64-bit float ("wide") or a 32-bit one. The flag
9/// drives the binary codec's `0x22`/`0x23` tag choice and object uniquing but
10/// never participates in the public API: equality and ordering compare the
11/// numeric value only, with IEEE semantics (`NaN` is not equal to itself).
12///
13/// `From<f32>` builds a narrow real, `From<f64>` a wide one; XML- and
14/// text-parsed reals are always wide.
15///
16/// # Examples
17///
18/// ```
19/// use apple_plist::Real;
20///
21/// assert_eq!(Real::from(32.0f32), Real::from(32.0f64));
22/// assert_ne!(Real::from(f64::NAN), Real::from(f64::NAN));
23/// ```
24#[derive(Clone, Copy, Debug)]
25pub struct Real {
26    value: f64,
27    wide: bool,
28}
29
30impl Real {
31    /// Returns the numeric value as an `f64`.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use apple_plist::Real;
37    ///
38    /// assert_eq!(Real::from(1.5f64).value(), 1.5);
39    /// assert_eq!(Real::from(1.5f32).value(), 1.5);
40    /// ```
41    #[must_use]
42    pub const fn value(self) -> f64 {
43        self.value
44    }
45
46    /// Whether this real is conceptually 64-bit. Read by the binary
47    /// generator (tag and dedup width) and the serde serializer.
48    #[cfg_attr(
49        not(any(test, feature = "serde", feature = "binary")),
50        expect(
51            dead_code,
52            reason = "consumed by the serde bridge and the binary codec; dead only when both are compiled out"
53        )
54    )]
55    pub(crate) const fn wide(self) -> bool {
56        self.wide
57    }
58}
59
60impl From<f32> for Real {
61    fn from(value: f32) -> Self {
62        Self {
63            value: f64::from(value),
64            wide: false,
65        }
66    }
67}
68
69impl From<f64> for Real {
70    fn from(value: f64) -> Self {
71        Self { value, wide: true }
72    }
73}
74
75impl PartialEq for Real {
76    // IEEE numeric equality on the value alone is the contract; NaN != NaN.
77    fn eq(&self, other: &Self) -> bool {
78        self.value == other.value
79    }
80}
81
82impl PartialOrd for Real {
83    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
84        self.value.partial_cmp(&other.value)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    #![expect(clippy::float_cmp, reason = "test code: bit-exact float expectations")]
91
92    use super::*;
93
94    #[test]
95    fn width_follows_the_source_type() {
96        assert!(!Real::from(32.0f32).wide());
97        assert!(Real::from(32.0f64).wide());
98    }
99
100    #[test]
101    fn narrow_construction_widens_exactly() {
102        assert_eq!(Real::from(f32::MAX).value(), f64::from(f32::MAX));
103        assert_eq!(Real::from(0.5f32).value(), 0.5);
104    }
105
106    #[test]
107    fn equality_is_numeric_and_ignores_width() {
108        assert_eq!(Real::from(32.0f32), Real::from(32.0f64));
109        assert_ne!(Real::from(32.0f64), Real::from(64.0f64));
110        assert_eq!(Real::from(-0.0f64), Real::from(0.0f64));
111    }
112
113    #[test]
114    fn nan_is_not_equal_to_itself() {
115        let nan = Real::from(f64::NAN);
116        assert_ne!(nan, nan);
117        assert_eq!(nan.partial_cmp(&nan), None);
118    }
119
120    #[test]
121    fn ordering_is_numeric() {
122        assert!(Real::from(1.0f32) < Real::from(2.0f64));
123        assert_eq!(
124            Real::from(2.0f32).partial_cmp(&Real::from(2.0f64)),
125            Some(Ordering::Equal)
126        );
127    }
128}