0

My tests fail when using floating point numbers f64 due to precision errors.

Playground:

use std::ops::Sub;

#[derive(Debug, PartialEq, Clone, Copy)]
struct Audio {
    amp: f64,
}

impl Sub for Audio {
    type Output = Self;

    fn sub(self, other: Self) -> Self::Output {
        Self {
            amp: self.amp - other.amp,
        }
    }
}

#[test]
fn subtract_audio() {
    let audio1 = Audio { amp: 0.9 };
    let audio2 = Audio { amp: 0.3 };

    assert_eq!(audio1 - audio2, Audio { amp: 0.6 });
    assert_ne!(audio1 - audio2, Audio { amp: 1.2 });
    assert_ne!(audio1 - audio2, Audio { amp: 0.3 });
}

I get the following error:

---- subtract_audio stdout ----
thread 'subtract_audio' panicked at 'assertion failed: `(left == right)`
  left: `Audio { amp: 0.6000000000000001 }`,
 right: `Audio { amp: 0.6 }`', src/lib.rs:23:5

How to test for structs with floating numbers like f64 ?

Stargateur
  • 24,473
  • 8
  • 65
  • 91
Saikub
  • 37
  • 3
  • 2
    Does this answer your question? [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – eggyal Jun 12 '21 at 18:38
  • Your problem isn't so much with your implementation of `Sub` as it is with the derived implementation of `PartialEq`. Better to manually implement, testing that the value is within your desired tolerance. – eggyal Jun 12 '21 at 18:40
  • @eggyal I understand floating point, thank you. Would you say implementing `PartialEq` is better than the Answer I posted? thanks. – Saikub Jun 12 '21 at 18:47
  • 3
    The derived `PartialEq` implementation is pretty useless for a struct containing a float, and is likely to lead to unexpected and hard to track down bugs—so I'd definitely suggest removing it. If the struct nevertheless needs to implement `PartialEq` for other reasons, then you'll need to do it manually anyway... after which your original `assert_eq` will work as expected. If you don't have any other reason to implement `PartialEq` then I guess it's up to you which approach you use, but I think implementing the trait captures the intent more clearly. – eggyal Jun 12 '21 at 18:53
  • Of course, if your tolerances during comparisons depend on context, then implementing `PartialEq` is probably a bad idea. – eggyal Jun 12 '21 at 18:58
  • no they don't depend on context, I want to compare with same accuracy everywhere. thank you @eggyal, I would +1 if I could – Saikub Jun 12 '21 at 19:09

1 Answers1

2

If the comparing were to be done with numbers without struct,

let a: f64 = 0.9;
let b: f64 = 0.6;

assert!(a - b < f64:EPSILON);

But with structs we need to take extra measures. First need to derive with PartialOrd to allow comparing with other structs.

#[derive(Debug, PartialEq, PartialOrd)]
struct Audio {...}

next create a struct for comparison

let audio_epsilon = Audio { amp: f64:EPSILON };

now I can compare regularly (with assert! not assert_eq!)

assert!(c - d < audio_epsilon)

An other solution is to implement PartialEq manually:

impl PartialEq for Audio {
    fn eq(&self, other: &Self) -> bool {
        (self.amp - other.amp).abs() < f64::EPSILON
    }
}
eggyal
  • 122,705
  • 18
  • 212
  • 237
Saikub
  • 37
  • 3
  • 2
    Nb, unless you know that the LHS is always greater than the RHS (so the subtractions is always positive) you’ll need to take its *absolute value* before comparing against your desired tolerance. – eggyal Jun 12 '21 at 21:30
  • @eggyal `-5--5 == 0`, unless you talk about possible overflow here yeah it's possible but I choice to keep it simple for a case that should never happen for f64. But yeah production code may want to avoid any risk. – Stargateur Jun 12 '21 at 22:01
  • 1
    @Stargateur: If `c` (or `self.amp`) is less than `d` (or `other.amp`) then `c - d` (or `self.amp - other.amp`) will be negative and thus necessarily less than `f64::EPSILON` irrespective of by how much they differ. – eggyal Jun 12 '21 at 22:05
  • @Stargateur: FYI I rolled back your latest edit because `.abs()` was perfectly fine. – eggyal Jun 14 '21 at 08:27
  • note that `f64:EPSILON` might not be the right thing to use here. e.g. you will have less precision if the intermediate values were significantly larger than 1, or involved a large number of intermediate calculations. note that it's fine given the values in the question, but in general might not do what you expect – Sam Mason Jun 15 '21 at 07:47