fixnum_approx_eq/
comparisons.rs

1use crate::traits::Abs;
2use fixnum::ops::{Bounded, CheckedAdd, One, RoundMode, RoundingMul, Zero};
3use fixnum::{FixedPoint, Precision};
4use thiserror::Error;
5
6#[derive(Error, Debug, Ord, PartialOrd, Eq, PartialEq)]
7pub enum ApproxEqError<F> {
8    #[error("Expected absolute tolerance to be non-negative, got {0:?}")]
9    NegativeAbsoluteTolerance(F),
10    #[error("Expected percentage to be in interval [0, 1), got {0:?}")]
11    IncorrectRelativePercentage(F),
12}
13
14#[inline]
15fn are_approx_eq_abs_unchecked<F>(left: F, right: F, tolerance: F) -> bool
16where
17    F: Ord + Zero + CheckedAdd<Output = F> + Bounded + Clone,
18{
19    left.clone() <= right.clone().saturating_add(tolerance.clone())
20        && right <= left.saturating_add(tolerance)
21}
22
23/// Calculate if two values are approximately equal
24/// up to some absolute tolerance (constant value)
25pub fn are_approx_eq_abs<F>(left: F, right: F, tolerance: F) -> Result<bool, ApproxEqError<F>>
26where
27    F: Ord + Zero + CheckedAdd<Output = F> + Bounded + Clone,
28{
29    if tolerance >= F::ZERO {
30        Ok(are_approx_eq_abs_unchecked(left, right, tolerance))
31    } else {
32        Err(ApproxEqError::NegativeAbsoluteTolerance(tolerance))
33    }
34}
35
36/// Calculate relative absolute tolerance for two numbers: percentage of their magnitude
37/// `a.abs() + b.abs()`
38fn calculate_relative_tolerance<I, P>(
39    a: FixedPoint<I, P>,
40    b: FixedPoint<I, P>,
41    percentage: FixedPoint<I, P>,
42) -> Result<FixedPoint<I, P>, ApproxEqError<FixedPoint<I, P>>>
43where
44    I: Ord + From<i16>,
45    P: Precision + Ord,
46    FixedPoint<I, P>: Zero
47        + One
48        + Abs
49        + Bounded
50        + CheckedAdd<Output = FixedPoint<I, P>>
51        + RoundingMul<Output = FixedPoint<I, P>>,
52{
53    let percentage_correct =
54        percentage >= FixedPoint::<I, P>::ZERO && percentage < FixedPoint::<I, P>::ONE;
55    if !percentage_correct {
56        return Err(ApproxEqError::IncorrectRelativePercentage(percentage));
57    }
58
59    let magnitude = a
60        .trait_abs()
61        .unwrap_or(FixedPoint::<I, P>::MAX)
62        .saturating_add(b.trait_abs().unwrap_or(FixedPoint::<I, P>::MAX));
63    // should not saturate as tolerance is in [0, 1)
64    Ok(magnitude.saturating_rmul(percentage, RoundMode::Ceil))
65}
66
67/// Calculate if two values are approximately equal
68/// up to some relative tolerance (percentage of their magnitude `a.abs() + b.abs()`)
69pub fn are_approx_eq_rel<I, P>(
70    left: FixedPoint<I, P>,
71    right: FixedPoint<I, P>,
72    percentage: FixedPoint<I, P>,
73) -> Result<bool, ApproxEqError<FixedPoint<I, P>>>
74where
75    I: Ord + From<i16>,
76    P: Precision + Ord,
77    FixedPoint<I, P>: Zero
78        + One
79        + Abs
80        + Bounded
81        + Clone
82        + CheckedAdd<Output = FixedPoint<I, P>>
83        + RoundingMul<Output = FixedPoint<I, P>>,
84{
85    let tolerance = calculate_relative_tolerance(left.clone(), right.clone(), percentage)?;
86    are_approx_eq_abs(left, right, tolerance)
87}
88
89/// Determine if two numbers `left` and `right` are equal up to some tolerance.
90///
91/// ## Tolerance
92/// Both relative and absolute tolerances are considered here.
93///
94/// Absolute tolerance is a constant `A > 0`. `left` is approx equal to `right` if
95/// `left + a = right` for some `-A <= a <= A`.
96///
97/// Relative tolerance for two numbers (`R > 0`) is calculated as percentage of their magnitude
98/// (`M = left.abs() + right.abs()`). So `left` is approx equal to `right` if
99/// `left + r = right` for some `-M*R <= r <= M*R`.
100///
101/// Satisfying any of the tolerances is enough to consider the numbers approximately equal.
102pub fn are_approx_eq<I, P>(
103    left: FixedPoint<I, P>,
104    right: FixedPoint<I, P>,
105    absolute_tolerance: FixedPoint<I, P>,
106    relative_percentage: FixedPoint<I, P>,
107) -> Result<bool, ApproxEqError<FixedPoint<I, P>>>
108where
109    I: Ord + From<i16>,
110    P: Precision + Ord,
111    FixedPoint<I, P>: Zero
112        + One
113        + Abs
114        + Bounded
115        + Clone
116        + CheckedAdd<Output = FixedPoint<I, P>>
117        + RoundingMul<Output = FixedPoint<I, P>>,
118{
119    let relative_tolerance =
120        calculate_relative_tolerance(left.clone(), right.clone(), relative_percentage)?;
121    // `max` may overshadow incorrect argument, so we need to check it here as well
122    if absolute_tolerance >= FixedPoint::<I, P>::ZERO {
123        Ok(are_approx_eq_abs_unchecked(
124            left,
125            right,
126            absolute_tolerance.max(relative_tolerance),
127        ))
128    } else {
129        Err(ApproxEqError::NegativeAbsoluteTolerance(absolute_tolerance))
130    }
131}
132
133#[cfg(test)]
134mod test {
135    use super::{are_approx_eq, are_approx_eq_abs, are_approx_eq_rel, ApproxEqError};
136    use fixnum::ops::{Bounded, CheckedSub, One, Zero};
137    use fixnum::typenum::U18;
138    use fixnum::{fixnum_const, FixedPoint};
139
140    type CustomPrecision = U18;
141
142    #[test]
143    fn should_approx_eq_equalize_exact_numbers() {
144        for number in [
145            FixedPoint::<i128, CustomPrecision>::ZERO,
146            FixedPoint::<i128, CustomPrecision>::MAX,
147            FixedPoint::<i128, CustomPrecision>::MIN,
148            FixedPoint::<i128, CustomPrecision>::from_bits(1),
149            FixedPoint::<i128, CustomPrecision>::from_bits(-1),
150        ] {
151            assert!(are_approx_eq(
152                number,
153                number,
154                FixedPoint::<i128, CustomPrecision>::ZERO,
155                FixedPoint::<i128, CustomPrecision>::ZERO
156            )
157            .unwrap());
158            // almost zero
159            assert!(are_approx_eq(
160                number,
161                number,
162                FixedPoint::<i128, CustomPrecision>::from_bits(1),
163                FixedPoint::<i128, CustomPrecision>::ZERO
164            )
165            .unwrap());
166            assert!(are_approx_eq(
167                number,
168                number,
169                FixedPoint::<i128, CustomPrecision>::ZERO,
170                FixedPoint::<i128, CustomPrecision>::from_bits(1)
171            )
172            .unwrap());
173            assert!(are_approx_eq(
174                number,
175                number,
176                FixedPoint::<i128, CustomPrecision>::from_bits(1),
177                FixedPoint::<i128, CustomPrecision>::from_bits(1)
178            )
179            .unwrap());
180            // max values
181            assert!(are_approx_eq(
182                number,
183                number,
184                FixedPoint::<i128, CustomPrecision>::MAX,
185                FixedPoint::<i128, CustomPrecision>::ZERO
186            )
187            .unwrap());
188            assert!(are_approx_eq(
189                number,
190                number,
191                FixedPoint::<i128, CustomPrecision>::ZERO,
192                FixedPoint::<i128, CustomPrecision>::ONE
193                    .csub(FixedPoint::<i128, CustomPrecision>::from_bits(1))
194                    .unwrap()
195            )
196            .unwrap());
197            assert!(are_approx_eq(
198                number,
199                number,
200                FixedPoint::<i128, CustomPrecision>::MAX,
201                FixedPoint::<i128, CustomPrecision>::ONE
202                    .csub(FixedPoint::from_bits(1))
203                    .unwrap()
204            )
205            .unwrap());
206        }
207    }
208
209    #[test]
210    fn should_approx_eq_abs_equalize_exact_numbers() {
211        for number in [
212            FixedPoint::<i128, CustomPrecision>::ZERO,
213            FixedPoint::<i128, CustomPrecision>::MAX,
214            FixedPoint::<i128, CustomPrecision>::MIN,
215            FixedPoint::<i128, CustomPrecision>::from_bits(1),
216            FixedPoint::<i128, CustomPrecision>::from_bits(-1),
217        ] {
218            assert!(
219                are_approx_eq_abs(number, number, FixedPoint::<i128, CustomPrecision>::ZERO)
220                    .unwrap()
221            );
222            assert!(are_approx_eq_abs(
223                number,
224                number,
225                FixedPoint::<i128, CustomPrecision>::from_bits(1)
226            )
227            .unwrap());
228            assert!(
229                are_approx_eq_abs(number, number, FixedPoint::<i128, CustomPrecision>::MAX)
230                    .unwrap()
231            );
232        }
233    }
234
235    #[test]
236    fn should_approx_eq_rel_equalize_exact_numbers() {
237        for number in [
238            FixedPoint::<i128, CustomPrecision>::ZERO,
239            FixedPoint::<i128, CustomPrecision>::MAX,
240            FixedPoint::<i128, CustomPrecision>::MIN,
241            FixedPoint::<i128, CustomPrecision>::from_bits(1),
242            FixedPoint::<i128, CustomPrecision>::from_bits(-1),
243        ] {
244            assert!(
245                are_approx_eq_rel(number, number, FixedPoint::<i128, CustomPrecision>::ZERO)
246                    .unwrap()
247            );
248            assert!(are_approx_eq_rel(
249                number,
250                number,
251                FixedPoint::<i128, CustomPrecision>::from_bits(1)
252            )
253            .unwrap());
254            assert!(are_approx_eq_rel(
255                number,
256                number,
257                FixedPoint::<i128, CustomPrecision>::ONE
258                    .csub(FixedPoint::from_bits(1))
259                    .unwrap()
260            )
261            .unwrap());
262        }
263    }
264
265    // abs tolerance is drawn as (<=.=>)
266    // rel tolerance is drawn as ({#.#})
267    struct ApproxEqTestCase {
268        left: FixedPoint<i128, CustomPrecision>,
269        right: FixedPoint<i128, CustomPrecision>,
270        absolute_tolerance: FixedPoint<i128, CustomPrecision>,
271        relative_percentage: FixedPoint<i128, CustomPrecision>,
272    }
273
274    impl ApproxEqTestCase {
275        const fn new(
276            left: FixedPoint<i128, CustomPrecision>,
277            right: FixedPoint<i128, CustomPrecision>,
278            absolute_tolerance: FixedPoint<i128, CustomPrecision>,
279            relative_percentage: FixedPoint<i128, CustomPrecision>,
280        ) -> Self {
281            Self {
282                left,
283                right,
284                absolute_tolerance,
285                relative_percentage,
286            }
287        }
288    }
289
290    // Test cases where the numbers are approx. equal only by absolute tolerance
291    const APPROX_EQ_ABS_MATCH_CASES: &[ApproxEqTestCase] = &[
292        // -5        0 1       5
293        // |         | |       |
294        // <=========.=========>
295        //           ^right    ^left
296        // abs tolerance: +-5
297        // rel tolerance: +-0.05
298        ApproxEqTestCase::new(
299            fixnum_const!(5, 18),
300            fixnum_const!(0, 18),
301            fixnum_const!(5, 18),
302            fixnum_const!(0.01, 18),
303        ),
304        // -5        0 1       5
305        // |         | |       |
306        // <=========.=========>
307        // ^left     ^right
308        // abs tolerance: +-5
309        // rel tolerance: +-0.05
310        ApproxEqTestCase::new(
311            fixnum_const!(-5, 18),
312            fixnum_const!(0, 18),
313            fixnum_const!(5, 18),
314            fixnum_const!(0.01, 18),
315        ),
316        // -5        0 1       5
317        // |         | |       |
318        // <=========.=========>
319        //           ^right
320        //            ^~left
321        // abs tolerance: +-5
322        // rel tolerance: +-0.05
323        ApproxEqTestCase::new(
324            FixedPoint::<i128, CustomPrecision>::from_bits(
325                fixnum::_priv::parse_fixed(stringify!(0.05), fixnum::_priv::pow10(18)) + 1,
326            ),
327            fixnum_const!(0, 18),
328            fixnum_const!(5, 18),
329            fixnum_const!(0.01, 18),
330        ),
331        // -5        0 1       5
332        // |         | |       |
333        // <=========.=========>
334        //           ^right
335        //          ^~left
336        // abs tolerance: +-5
337        // rel tolerance: +-0.05
338        ApproxEqTestCase::new(
339            FixedPoint::<i128, CustomPrecision>::from_bits(
340                -fixnum::_priv::parse_fixed(stringify!(0.05), fixnum::_priv::pow10(18)) - 1,
341            ),
342            fixnum_const!(0, 18),
343            fixnum_const!(5, 18),
344            fixnum_const!(0.01, 18),
345        ),
346        // 47        52        57
347        // |         |         |
348        // <=========.=========>
349        // ^left     ^right
350        // abs tolerance: +-5
351        // rel tolerance: +-4.95
352        ApproxEqTestCase::new(
353            fixnum_const!(47, 18),
354            fixnum_const!(52, 18),
355            fixnum_const!(5, 18),
356            fixnum_const!(0.05, 18),
357        ),
358        // closer to rel tolerance:
359        // 47.02        51.98
360        // |            |
361        // <============.============>
362        // ^left        ^right
363        // abs tolerance: +-5
364        // rel tolerance: +-4.95
365        ApproxEqTestCase::new(
366            fixnum_const!(47.02, 18),
367            fixnum_const!(51.98, 18),
368            fixnum_const!(5, 18),
369            fixnum_const!(0.05, 18),
370        ),
371    ];
372
373    #[test]
374    fn should_approx_eq_match_abs_tolerance() {
375        for &ApproxEqTestCase {
376            left,
377            right,
378            absolute_tolerance,
379            relative_percentage,
380        } in APPROX_EQ_ABS_MATCH_CASES
381        {
382            assert!(
383                are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
384                "Expected {} = {} with absolute tolerance {} and relative tolerance (%) {}, but got '!='",
385                left, right, absolute_tolerance, relative_percentage
386            );
387            assert!(
388                are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
389                "Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {} rel tolerance (%) {}",
390                left, right, right, left, absolute_tolerance, relative_percentage
391            );
392        }
393    }
394
395    #[test]
396    fn should_approx_eq_abs_match_abs_tolerance() {
397        for &ApproxEqTestCase {
398            left,
399            right,
400            absolute_tolerance,
401            relative_percentage: _,
402        } in APPROX_EQ_ABS_MATCH_CASES
403        {
404            assert!(
405                are_approx_eq_abs(left, right, absolute_tolerance).unwrap(),
406                "Expected {} = {} with absolute tolerance {}, but got '!='",
407                left,
408                right,
409                absolute_tolerance
410            );
411            assert!(
412                are_approx_eq_abs(right, left, absolute_tolerance).unwrap(),
413                "Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {}",
414                left,
415                right,
416                right,
417                left,
418                absolute_tolerance
419            );
420        }
421    }
422
423    #[test]
424    fn should_approx_eq_rel_not_match_abs_tolerance() {
425        for &ApproxEqTestCase {
426            left,
427            right,
428            absolute_tolerance: _,
429            relative_percentage,
430        } in APPROX_EQ_ABS_MATCH_CASES
431        {
432            assert!(
433                !are_approx_eq_rel(left, right, relative_percentage).unwrap(),
434                "Expected {} != {} with relative tolerance (%) {}, but got '='",
435                left,
436                right,
437                relative_percentage
438            );
439            assert!(
440                !are_approx_eq_rel(right, left, relative_percentage).unwrap(),
441                "Expected approx eq to be symmetrical; {} != {}, but {} = {} for rel tolerance (%) {}",
442                left, right, right, left, relative_percentage
443            );
444        }
445    }
446    // Test cases where the numbers are approx. equal only by relative tolerance
447    const APPROX_EQ_REL_MATCH_CASES: &[ApproxEqTestCase] = &[
448        // 0       5 6
449        // |       | |
450        //       {#.#}
451        //         ^right
452        //           ^left
453        // abs tolerance: 0
454        // rel tolerance: +-1.1
455        ApproxEqTestCase::new(
456            fixnum_const!(6, 18),
457            fixnum_const!(5, 18),
458            fixnum_const!(0, 18),
459            fixnum_const!(0.1, 18),
460        ),
461        //   9   11
462        //   |   |
463        // ##.###}
464        //   ^right
465        //       ^left
466        // abs tolerance: 0
467        // rel tolerance: +-2
468        ApproxEqTestCase::new(
469            fixnum_const!(11, 18),
470            fixnum_const!(9, 18),
471            fixnum_const!(0, 18),
472            fixnum_const!(0.1, 18),
473        ),
474        //   9   11
475        //   |   |
476        // ##.###}
477        //   ^right
478        //       ^left
479        // abs tolerance: +-1.9999
480        // rel tolerance: +-2
481        ApproxEqTestCase::new(
482            fixnum_const!(11, 18),
483            fixnum_const!(9, 18),
484            fixnum_const!(1.9999, 18),
485            fixnum_const!(0.1, 18),
486        ),
487        //   9   10.1
488        //   |   |
489        // ##.###}
490        //   ^left
491        //       ^right
492        // abs tolerance: +-1
493        // rel tolerance: +-1.91
494        ApproxEqTestCase::new(
495            fixnum_const!(9, 18),
496            fixnum_const!(10.1, 18),
497            fixnum_const!(1, 18),
498            fixnum_const!(0.1, 18),
499        ),
500    ];
501
502    #[test]
503    fn should_approx_eq_match_rel_tolerance() {
504        for &ApproxEqTestCase {
505            left,
506            right,
507            absolute_tolerance,
508            relative_percentage,
509        } in APPROX_EQ_REL_MATCH_CASES
510        {
511            assert!(
512                are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
513                "Expected {} = {} with absolute tolerance {} and relative tolerance (%) {}, but got '!='",
514                left, right, absolute_tolerance, relative_percentage
515            );
516            assert!(
517                are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
518                "Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {} rel tolerance (%) {}",
519                left, right, right, left, absolute_tolerance, relative_percentage
520            );
521        }
522    }
523
524    #[test]
525    fn should_approx_eq_abs_not_match_rel_tolerance() {
526        for &ApproxEqTestCase {
527            left,
528            right,
529            absolute_tolerance,
530            relative_percentage: _,
531        } in APPROX_EQ_REL_MATCH_CASES
532        {
533            assert!(
534                !are_approx_eq_abs(left, right, absolute_tolerance).unwrap(),
535                "Expected {} != {} with absolute tolerance {}, but got '='",
536                left,
537                right,
538                absolute_tolerance
539            );
540            assert!(
541                !are_approx_eq_abs(right, left, absolute_tolerance).unwrap(),
542                "Expected approx eq to be symmetrical; {} != {}, but {} = {} for abs tolerance {}",
543                left,
544                right,
545                right,
546                left,
547                absolute_tolerance
548            );
549        }
550    }
551
552    #[test]
553    fn should_approx_eq_rel_match_rel_tolerance() {
554        for &ApproxEqTestCase {
555            left,
556            right,
557            absolute_tolerance: _,
558            relative_percentage,
559        } in APPROX_EQ_REL_MATCH_CASES
560        {
561            assert!(
562                are_approx_eq_rel(left, right, relative_percentage).unwrap(),
563                "Expected {} = {} with relative tolerance (%) {}, but got '!='",
564                left,
565                right,
566                relative_percentage
567            );
568            assert!(
569                are_approx_eq_rel(right, left, relative_percentage).unwrap(),
570                "Expected approx eq to be symmetrical; {} = {}, but {} != {} for rel tolerance (%) {}",
571                left, right, right, left, relative_percentage
572            );
573        }
574    }
575
576    // Test cases where the numbers are not approx. equal
577    const APPROX_EQ_BOTH_MATCH_CASES: &[ApproxEqTestCase] = &[
578        // 0       5 6
579        // |       | |
580        //       {#.#}
581        //         ^right
582        //           ^left
583        // abs tolerance: +-1.1
584        // rel tolerance: +-1.1
585        ApproxEqTestCase::new(
586            fixnum_const!(6, 18),
587            fixnum_const!(5, 18),
588            fixnum_const!(1.1, 18),
589            fixnum_const!(0.1, 18),
590        ),
591        //   9   11
592        //   |   |
593        // ##.###}
594        //   ^left
595        //       ^right
596        // abs tolerance: +-2
597        // rel tolerance: +-2
598        ApproxEqTestCase::new(
599            fixnum_const!(9, 18),
600            fixnum_const!(11, 18),
601            fixnum_const!(2, 18),
602            fixnum_const!(0.1, 18),
603        ),
604        //   9      11
605        //   |      |
606        // ##.###}
607        //    ^right
608        //   ^left
609        // abs tolerance: +-2
610        // rel tolerance: +-2
611        ApproxEqTestCase::new(
612            fixnum_const!(9, 18),
613            FixedPoint::<i128, CustomPrecision>::from_bits(
614                fixnum::_priv::parse_fixed(stringify!(9), fixnum::_priv::pow10(18)) + 1,
615            ),
616            fixnum_const!(2, 18),
617            fixnum_const!(0.1, 18),
618        ),
619        //   9   10.1
620        //   |   |
621        // ##.###}
622        //   ^left
623        //       ^right
624        // abs tolerance: +-1.11
625        // rel tolerance: +-1.91
626        ApproxEqTestCase::new(
627            fixnum_const!(9, 18),
628            fixnum_const!(10.1, 18),
629            fixnum_const!(1.11, 18),
630            fixnum_const!(0.1, 18),
631        ),
632    ];
633
634    #[test]
635    fn should_approx_eq_match_both_tolerance() {
636        for &ApproxEqTestCase {
637            left,
638            right,
639            absolute_tolerance,
640            relative_percentage,
641        } in APPROX_EQ_BOTH_MATCH_CASES
642        {
643            assert!(
644                are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
645                "Expected {} = {} with absolute tolerance {} and relative tolerance (%) {}, but got '!='",
646                left, right, absolute_tolerance, relative_percentage
647            );
648            assert!(
649                are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
650                "Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {} rel tolerance (%) {}",
651                left, right, right, left, absolute_tolerance, relative_percentage
652            );
653        }
654    }
655
656    #[test]
657    fn should_approx_eq_abs_match_both_tolerance() {
658        for &ApproxEqTestCase {
659            left,
660            right,
661            absolute_tolerance,
662            relative_percentage: _,
663        } in APPROX_EQ_BOTH_MATCH_CASES
664        {
665            assert!(
666                are_approx_eq_abs(left, right, absolute_tolerance).unwrap(),
667                "Expected {} = {} with absolute tolerance {}, but got '!='",
668                left,
669                right,
670                absolute_tolerance
671            );
672            assert!(
673                are_approx_eq_abs(right, left, absolute_tolerance).unwrap(),
674                "Expected approx eq to be symmetrical; {} = {}, but {} != {} for abs tolerance {}",
675                left,
676                right,
677                right,
678                left,
679                absolute_tolerance
680            );
681        }
682    }
683
684    #[test]
685    fn should_approx_eq_rel_match_both_tolerance() {
686        for &ApproxEqTestCase {
687            left,
688            right,
689            absolute_tolerance: _,
690            relative_percentage,
691        } in APPROX_EQ_BOTH_MATCH_CASES
692        {
693            assert!(
694                are_approx_eq_rel(left, right, relative_percentage).unwrap(),
695                "Expected {} = {} with relative tolerance (%) {}, but got '!='",
696                left,
697                right,
698                relative_percentage
699            );
700            assert!(
701                are_approx_eq_rel(right, left, relative_percentage).unwrap(),
702                "Expected approx eq to be symmetrical; {} = {}, but {} != {} for rel tolerance (%) {}",
703                left, right, right, left, relative_percentage
704            );
705        }
706    }
707
708    // Test cases where the numbers are not approx. equal
709    const APPROX_EQ_NOT_MATCH_CASES: &[ApproxEqTestCase] = &[
710        // -5        0 1       5
711        // |         | |       |
712        // <=========.=========>
713        //           ^right     ^left
714        // abs tolerance: +-5
715        // rel tolerance: +-0.05
716        ApproxEqTestCase::new(
717            FixedPoint::<i128, CustomPrecision>::from_bits(
718                fixnum::_priv::parse_fixed(stringify!(5), fixnum::_priv::pow10(18)) + 1,
719            ),
720            fixnum_const!(0, 18),
721            fixnum_const!(5, 18),
722            fixnum_const!(0.01, 18),
723        ),
724        //  -5        0 1       5
725        //  |         | |       |
726        //  <=========.=========>
727        // ^left      ^right
728        // abs tolerance: +-5
729        // rel tolerance: +-0.05
730        ApproxEqTestCase::new(
731            FixedPoint::<i128, CustomPrecision>::from_bits(
732                -fixnum::_priv::parse_fixed(stringify!(5), fixnum::_priv::pow10(18)) - 1,
733            ),
734            fixnum_const!(0, 18),
735            fixnum_const!(5, 18),
736            fixnum_const!(0.01, 18),
737        ),
738        // -5        0 1       5
739        // |         | |       |
740        // <=========.=========>
741        //           ^right
742        // abs tolerance: +-5
743        // rel tolerance: +-(0.01*FixedInner::MAX)
744        ApproxEqTestCase::new(
745            FixedPoint::<i128, CustomPrecision>::MAX,
746            fixnum_const!(0, 18),
747            fixnum_const!(5, 18),
748            fixnum_const!(0.01, 18),
749        ),
750        // -5        0 1       5
751        // |         | |       |
752        // <=========.=========>
753        //           ^right
754        // abs tolerance: +-5
755        // rel tolerance: +-(0.01*FixedInner::MIN.abs())
756        ApproxEqTestCase::new(
757            FixedPoint::<i128, CustomPrecision>::MIN,
758            fixnum_const!(0, 18),
759            fixnum_const!(5, 18),
760            fixnum_const!(0.01, 18),
761        ),
762        //  47        52        57
763        //  |         |         |
764        //   <=========.=========>
765        // ^left       ^right
766        // abs tolerance: +-5
767        // rel tolerance: +-4.95
768        ApproxEqTestCase::new(
769            FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
770                stringify!(47),
771                fixnum::_priv::pow10(18) - 1,
772            )),
773            FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
774                stringify!(52),
775                fixnum::_priv::pow10(18) + 1,
776            )),
777            fixnum_const!(5, 18),
778            fixnum_const!(0.05, 18),
779        ),
780        //  47        53        57
781        //  |         |         |
782        //   <=========.=========>
783        // ^left       ^right
784        // abs tolerance: +-5
785        // rel tolerance: +-5
786        ApproxEqTestCase::new(
787            FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
788                stringify!(47),
789                fixnum::_priv::pow10(18) - 1,
790            )),
791            FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
792                stringify!(53),
793                fixnum::_priv::pow10(18) + 1,
794            )),
795            fixnum_const!(5, 18),
796            fixnum_const!(0.05, 18),
797        ),
798        //   9   11
799        //   |   |
800        // ##.###}
801        //   ^left
802        //        ^right
803        // abs tolerance: 0
804        // rel tolerance: +-2
805        ApproxEqTestCase::new(
806            fixnum_const!(9, 18),
807            FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
808                stringify!(11),
809                fixnum::_priv::pow10(18) + 10,
810            )),
811            fixnum_const!(0, 18),
812            fixnum_const!(0.1, 18),
813        ),
814        //   9   11
815        //   |   |
816        // ##.###}
817        //   ^left
818        //        ^right
819        // abs tolerance: +-1.9999
820        // rel tolerance: +-2
821        ApproxEqTestCase::new(
822            fixnum_const!(9, 18),
823            FixedPoint::<i128, CustomPrecision>::from_bits(fixnum::_priv::parse_fixed(
824                stringify!(11),
825                fixnum::_priv::pow10(18) + 10,
826            )),
827            fixnum_const!(1.9999, 18),
828            fixnum_const!(0.1, 18),
829        ),
830    ];
831
832    #[test]
833    fn should_approx_eq_not_match() {
834        for &ApproxEqTestCase {
835            left,
836            right,
837            absolute_tolerance,
838            relative_percentage,
839        } in APPROX_EQ_NOT_MATCH_CASES
840        {
841            assert!(
842                !are_approx_eq(left, right, absolute_tolerance, relative_percentage).unwrap(),
843                "Expected {} != {} with absolute tolerance {} and relative tolerance (%) {}, but got '=='",
844                left, right, absolute_tolerance, relative_percentage
845            );
846            assert!(
847                !are_approx_eq(right, left, absolute_tolerance, relative_percentage).unwrap(),
848                "Expected approx eq to be symmetrical; {} != {}, but {} = {} for abs tolerance {} rel tolerance (%) {}",
849                left, right, right, left, absolute_tolerance, relative_percentage
850            );
851        }
852    }
853
854    #[test]
855    fn should_fail_incorrect_relative_percentage() {
856        let percentage = FixedPoint::<i128, CustomPrecision>::from_bits(-1234);
857        assert_eq!(
858            are_approx_eq(
859                FixedPoint::<i128, CustomPrecision>::ZERO,
860                FixedPoint::<i128, CustomPrecision>::ZERO,
861                FixedPoint::<i128, CustomPrecision>::ZERO,
862                percentage,
863            ),
864            Err(ApproxEqError::IncorrectRelativePercentage(percentage))
865        );
866        let percentage = FixedPoint::<i128, CustomPrecision>::ONE;
867        assert_eq!(
868            are_approx_eq(
869                FixedPoint::<i128, CustomPrecision>::ZERO,
870                FixedPoint::<i128, CustomPrecision>::ZERO,
871                FixedPoint::<i128, CustomPrecision>::ZERO,
872                percentage,
873            ),
874            Err(ApproxEqError::IncorrectRelativePercentage(percentage))
875        );
876    }
877
878    #[test]
879    fn should_fail_incorrect_absolute_percentage() {
880        let abs_tolerance = FixedPoint::<i128, CustomPrecision>::from_bits(-1);
881        assert_eq!(
882            are_approx_eq(
883                FixedPoint::<i128, CustomPrecision>::ZERO,
884                FixedPoint::<i128, CustomPrecision>::ZERO,
885                abs_tolerance,
886                FixedPoint::<i128, CustomPrecision>::ZERO,
887            ),
888            Err(ApproxEqError::NegativeAbsoluteTolerance(abs_tolerance))
889        );
890        let abs_tolerance = FixedPoint::<i128, CustomPrecision>::from_bits(i128::MIN);
891        assert_eq!(
892            are_approx_eq(
893                FixedPoint::<i128, CustomPrecision>::ZERO,
894                FixedPoint::<i128, CustomPrecision>::ZERO,
895                abs_tolerance,
896                FixedPoint::<i128, CustomPrecision>::ZERO,
897            ),
898            Err(ApproxEqError::NegativeAbsoluteTolerance(abs_tolerance))
899        );
900    }
901}