Skip to content

← Ground Rules: Values, Types, Control Flow step 8 of 24

Medium Primitives

Compare floats with an absolute tolerance

For each pair (a, b), return whether the computed f64 absolute difference is at most tol. Preserve the order of the pairs. An empty input returns an empty vector.

pub fn approx_equal(pairs: &[(f64, f64)], tol: f64) -> Vec<bool>

Use the rule (a - b).abs() <= tol, including its floating-point behavior. Do not add an exact-equality shortcut or reject unusual inputs. Keep the starter's #![warn(clippy::float_cmp)] attribute.

Pair Tolerance Result
0.1_f64 + 0.2, 0.3 1e-9 true
1.0, 1.0 0.0 true
1.0, 1.5 0.4 false
-2.0, -2.25 0.25 true
0.0, -0.0 0.0 true
NaN, any value any tolerance false

This is a small exercise in absolute difference, not a general-purpose numerical comparison library.

Why exact equality can miss a nearby result

Binary floating-point cannot represent 0.1 exactly. The stored values and the addition are rounded, so adding 0.1_f64 and 0.2 produces a different value from the stored 0.3:

fn main() {
    let sum = 0.1_f64 + 0.2;
    let target = 0.3_f64;
    let difference = (sum - target).abs();
    assert!(difference > 0.0);
    assert!(difference < 1e-9);
}

Whether that difference matters depends on the application. Exact comparisons are useful when exact stored values are what you intend to compare. Approximate comparisons are useful when some numerical error is acceptable.

An absolute tolerance uses the same units as the values. Applications working across very different scales may need relative tolerance as well. Neither 1e-9 nor f64::EPSILON is a universal threshold. EPSILON describes the spacing above 1.0, not the accumulated error of an arbitrary calculation.

NaN is unordered

For a comparison involving NaN, ==, <, <=, >, and >= return false. By contrast, != returns true.

fn main() {
    let value = f64::NAN;
    assert!(value.is_nan());
    assert!(value != 1.0);
    assert_eq!(value.partial_cmp(&1.0), None);
}

Use is_nan() to detect NaN. In this exercise, subtraction and abs() propagate NaN, and the final <= comparison returns false without a special branch.

Infinities and tolerance boundaries

The specified arithmetic has consequences that may differ from a library's notion of “close”:

  • Equal infinities return false: subtracting positive infinity from itself, or negative infinity from itself, produces NaN.
  • With a finite nonnegative tolerance, any pair containing an infinity returns false.
  • A negative tolerance or NaN tolerance returns false for every pair. Negative zero behaves like zero.
  • With positive infinity as the tolerance, any non-NaN absolute difference passes, including an infinite difference. NaN differences still fail.
  • Subtraction can overflow even when both inputs are finite. If its absolute result is infinity, it follows the same rules above.

For example, the largest positive finite value minus the most negative finite value overflows:

fn main() {
    let difference = (f64::MAX - f64::MIN).abs();
    assert!(difference.is_infinite());
    assert!(difference > f64::MAX);
    assert!(difference <= f64::INFINITY);
}

These are the exercise's defined results. A production API might instead validate its tolerance and give equal infinities special treatment; that would be a different contract.

The lint catches the starter's exact comparison

clippy::float_cmp is a pedantic lint, allowed by default. The starter explicitly enables it with a crate-level inner attribute, #![warn(clippy::float_cmp)]. Keep that attribute before the function items. Comments and other crate attributes can precede it; it does not have to be the first physical line.

Clippy with -D warnings turns this enabled warning into an error. Ordinary rustc does not run Clippy's analysis. The lint also has exceptions, such as comparisons against literal zero, so it is not a ban on every exact float comparison.

Related collection operations

f64 implements PartialEq and PartialOrd, but not Eq or Ord. Consequently, sorting floats with sort() or finding a maximum with the iterator's max() requires a different approach. sort_by(f64::total_cmp) provides a total ordering, including NaNs and signed zeros, rather than a tolerance-based comparison.

A HashMap<f64, V> type can be named, but ordinary key operations such as insert require Eq and Hash, which f64 does not implement. Choosing a wrapper or another representation requires deciding what equality should mean for your keys.

Test input encoding

The harness converts the strings "NaN", "inf", and "-inf" into the corresponding floats, since JSON numbers cannot encode those values. This applies to pair values and the tolerance. Your function receives f64 values, not strings. Other string spellings are outside the exercise's input format.

Further reading: f64 documentation and Clippy's float_cmp lint.