1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! Utilities to compare floating-point numbers.

use float_cmp::ApproxEq;

// The following are copied from cairo/src/{cairo-fixed-private.h,
// cairo-fixed-type-private.h}

const CAIRO_FIXED_FRAC_BITS: u64 = 8;
const CAIRO_MAGIC_NUMBER_FIXED: f64 = (1u64 << (52 - CAIRO_FIXED_FRAC_BITS)) as f64 * 1.5;

fn cairo_magic_double(d: f64) -> f64 {
    d + CAIRO_MAGIC_NUMBER_FIXED
}

fn cairo_fixed_from_double(d: f64) -> i32 {
    let bits = cairo_magic_double(d).to_bits();
    let lower = bits & 0xffffffff;
    lower as i32
}

/// Implements a method to check whether two `f64` numbers would have
/// the same fixed-point representation in Cairo.
///
/// This generally means that the absolute difference between them,
/// when taken as floating-point numbers, is less than the smallest
/// representable fraction that Cairo can represent in fixed-point.
///
/// Implementation detail: Cairo fixed-point numbers use 24 bits for
/// the integral part, and 8 bits for the fractional part.  That is,
/// the smallest fraction they can represent is 1/256.
pub trait FixedEqCairo {
    #[allow(dead_code)] // https://github.com/rust-lang/rust/issues/120770
    fn fixed_eq_cairo(&self, other: &Self) -> bool;
}

impl FixedEqCairo for f64 {
    fn fixed_eq_cairo(&self, other: &f64) -> bool {
        // FIXME: Here we have the same problem as Cairo itself: we
        // don't check for overflow in the conversion of double to
        // fixed-point.
        cairo_fixed_from_double(*self) == cairo_fixed_from_double(*other)
    }
}

/// Checks whether two floating-point numbers are approximately equal,
/// considering Cairo's limitations on numeric representation.
///
/// Cairo uses fixed-point numbers internally.  We implement this
/// trait for `f64`, so that two numbers can be considered "close
/// enough to equal" if their absolute difference is smaller than the
/// smallest fixed-point fraction that Cairo can represent.
///
/// Note that this trait is reliable even if the given numbers are
/// outside of the range that Cairo's fixed-point numbers can
/// represent.  In that case, we check for the absolute difference,
/// and finally allow a difference of 1 unit-in-the-last-place (ULP)
/// for very large f64 values.
pub trait ApproxEqCairo: ApproxEq {
    fn approx_eq_cairo(self, other: Self) -> bool;
}

impl ApproxEqCairo for f64 {
    fn approx_eq_cairo(self, other: f64) -> bool {
        let cairo_smallest_fraction = 1.0 / f64::from(1 << CAIRO_FIXED_FRAC_BITS);
        self.approx_eq(other, (cairo_smallest_fraction, 1))
    }
}

// Macro for usage in unit tests
#[doc(hidden)]
#[macro_export]
macro_rules! assert_approx_eq_cairo {
    ($left:expr, $right:expr) => {{
        match ($left, $right) {
            (l, r) => {
                if !l.approx_eq_cairo(r) {
                    panic!(
                        r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#,
                        l, r
                    )
                }
            }
        }
    }};
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn numbers_equal_in_cairo_fixed_point() {
        assert!(1.0_f64.fixed_eq_cairo(&1.0_f64));

        assert!(1.0_f64.fixed_eq_cairo(&1.001953125_f64)); // 1 + 1/512 - cairo rounds to 1

        assert!(!1.0_f64.fixed_eq_cairo(&1.00390625_f64)); // 1 + 1/256 - cairo can represent it
    }

    #[test]
    fn numbers_approx_equal() {
        // 0 == 1/256 - cairo can represent it, so not equal
        assert!(!0.0_f64.approx_eq_cairo(0.00390635_f64));

        // 1 == 1 + 1/256 - cairo can represent it, so not equal
        assert!(!1.0_f64.approx_eq_cairo(1.00390635_f64));

        // 0 == 1/256 - cairo can represent it, so not equal
        assert!(!0.0_f64.approx_eq_cairo(-0.00390635_f64));

        // 1 == 1 - 1/256 - cairo can represent it, so not equal
        assert!(!1.0_f64.approx_eq_cairo(0.99609365_f64));

        // 0 == 1/512 - cairo approximates to 0, so equal
        assert!(0.0_f64.approx_eq_cairo(0.001953125_f64));

        // 1 == 1 + 1/512 - cairo approximates to 1, so equal
        assert!(1.0_f64.approx_eq_cairo(1.001953125_f64));

        // 0 == -1/512 - cairo approximates to 0, so equal
        assert!(0.0_f64.approx_eq_cairo(-0.001953125_f64));

        // 1 == 1 - 1/512 - cairo approximates to 1, so equal
        assert!(1.0_f64.approx_eq_cairo(0.998046875_f64));

        // This is 2^53 compared to (2^53 + 2).  When represented as
        // f64, they are 1 unit-in-the-last-place (ULP) away from each
        // other, since the mantissa has 53 bits (52 bits plus 1
        // "hidden" bit).  The first number is an exact double, and
        // the second one is the next biggest double.  We consider a
        // difference of 1 ULP to mean that numbers are "equal", to
        // account for slight imprecision in floating-point
        // calculations.  Most of the time, for small values, we will
        // be using the cairo_smallest_fraction from the
        // implementation of approx_eq_cairo() above.  For large
        // values, we want the ULPs.
        //
        // In the second assertion, we compare 2^53 with (2^53 + 4).  Those are
        // 2 ULPs away, and we don't consider them equal.
        assert!(9_007_199_254_740_992.0.approx_eq_cairo(9_007_199_254_740_994.0));
        assert!(!9_007_199_254_740_992.0.approx_eq_cairo(9_007_199_254_740_996.0));
    }

    #[test]
    fn assert_approx_eq_cairo_should_not_panic() {
        assert_approx_eq_cairo!(42_f64, 42_f64);
    }

    #[test]
    #[should_panic]
    fn assert_approx_eq_cairo_should_panic() {
        assert_approx_eq_cairo!(3_f64, 42_f64);
    }
}