Skip to main content

shuyi_ui/
float.rs

1//! The float methods `core` lacks, from `libm`, for the `no_std` build
2//! (game ADR-0070 §3): import [`Float`] and [`Round`] only without `std`. With
3//! `std` the standard methods are called, so the numbers stay exactly what
4//! they were; the tests below hold the two together. A game built on the
5//! engine without `std` imports them too.
6
7/// The `f32` methods the frame and the game call that need `std`.
8pub trait Float: Sized {
9    /// Nearest integer, halves away from zero.
10    #[must_use]
11    fn round(self) -> Self;
12    /// Largest integer not above `self`.
13    #[must_use]
14    fn floor(self) -> Self;
15    /// Sine, in radians.
16    #[must_use]
17    fn sin(self) -> Self;
18    /// Cosine, in radians.
19    #[must_use]
20    fn cos(self) -> Self;
21    /// `sqrt(self² + other²)` without overflow.
22    #[must_use]
23    fn hypot(self, other: Self) -> Self;
24    /// The least non-negative remainder of `self / rhs`.
25    #[must_use]
26    fn rem_euclid(self, rhs: Self) -> Self;
27}
28
29impl Float for f32 {
30    fn round(self) -> Self {
31        libm::roundf(self)
32    }
33    fn floor(self) -> Self {
34        libm::floorf(self)
35    }
36    fn sin(self) -> Self {
37        libm::sinf(self)
38    }
39    fn cos(self) -> Self {
40        libm::cosf(self)
41    }
42    fn hypot(self, other: Self) -> Self {
43        libm::hypotf(self, other)
44    }
45    fn rem_euclid(self, rhs: Self) -> Self {
46        // As `std` writes it.
47        let r = self % rhs;
48        if r < 0.0 { r + rhs.abs() } else { r }
49    }
50}
51
52/// The one `f64` method the game calls that needs `std`.
53pub trait Round {
54    /// Nearest integer, halves away from zero.
55    #[must_use]
56    fn round(self) -> Self;
57}
58
59impl Round for f64 {
60    fn round(self) -> Self {
61        libm::round(self)
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::{Float, Round};
68    use proptest::prelude::*;
69
70    /// The very same number, down to the sign of a zero.
71    fn same(a: f32, b: f32) -> bool {
72        a.to_bits() == b.to_bits()
73    }
74
75    /// `sin`, `cos` and `hypot` may differ from the platform's in the last
76    /// bit; rounding and remainders must not differ at all.
77    fn close(a: f32, b: f32) -> bool {
78        (a - b).abs() <= 1e-6 * b.abs().max(1.0)
79    }
80
81    #[test]
82    fn rounding_matches_std_at_halves_and_signs() {
83        for v in [-2.5_f32, -1.5, -0.5, -0.4, 0.0, 0.4, 0.5, 1.5, 2.5, 7.99] {
84            assert!(same(Float::round(v), v.round()), "round {v}");
85            assert!(same(Float::floor(v), v.floor()), "floor {v}");
86            let w = f64::from(v);
87            assert_eq!(Round::round(w).to_bits(), w.round().to_bits(), "round {v}");
88        }
89    }
90
91    #[test]
92    fn rem_euclid_matches_std_at_signs_and_zero() {
93        for (v, w) in [
94            (-1.0_f32, 4.0),
95            (-1.0, -4.0),
96            (5.0, 4.0),
97            (-4.0, 4.0),
98            (0.5, 0.25),
99        ] {
100            assert!(same(Float::rem_euclid(v, w), v.rem_euclid(w)), "{v} % {w}");
101        }
102    }
103
104    proptest! {
105        #[test]
106        fn matches_std(v in -1000.0_f32..1000.0, w in -1000.0_f32..1000.0) {
107            prop_assert!(same(Float::round(v), v.round()));
108            prop_assert!(same(Float::floor(v), v.floor()));
109            let x = f64::from(v) * 3.7;
110            prop_assert_eq!(Round::round(x).to_bits(), x.round().to_bits());
111            prop_assert!(close(Float::sin(v / 100.0), (v / 100.0).sin()));
112            prop_assert!(close(Float::cos(v / 100.0), (v / 100.0).cos()));
113            prop_assert!(close(Float::hypot(v, w), v.hypot(w)));
114            if w != 0.0 {
115                prop_assert!(same(Float::rem_euclid(v, w), v.rem_euclid(w)));
116            }
117        }
118    }
119}