Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-283 · Case file with fixtures · Case 255 of 694 · Runtime evidence

Why a Floating Range with NaN Is Empty in Rust

Range::is_empty works with PartialOrd and treats incomparable endpoints as empty. Since NaN is incomparable, validate finite numeric bounds separately when NaN should be an input error rather than an empty interval.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Floating-point values have a partial order, and the range method defines emptiness when the start is not strictly less than the end.
First discriminating check
Validate endpoint finiteness separately before using is_empty to decide whether an application range is acceptable.

I used range.is_empty() as a compact check for reversed floating bounds. A NaN endpoint returned true, and my code treated corrupted input as an ordinary empty selection.

The failing program constructs 0.0..NaN and expects it not to be empty. Range::is_empty deliberately says it is empty because the endpoints are incomparable.

Floating-point order is partial

f64 implements PartialOrd, not Ord, because NaN does not participate in ordinary numerical ordering. Comparisons such as these are false:

NaN < 5.0
NaN >= 5.0
NaN == NaN

This breaks the shortcut “a range is empty exactly when start >= end.” For partially ordered values, neither increasing nor reversed may be true.

The range API resolves that third state by treating incomparable endpoints as empty. Its documentation shows NaN at either side.

Empty and invalid may be different product outcomes

For a search filter, 10.0..5.0 might be an empty interval or a validation mistake. 0.0..NaN is more likely malformed measurement data. Returning the same empty result for both can hide an upstream failure.

The repaired program defines a stricter application boundary. It rejects non-finite endpoints first, then rejects a range that does not increase.

This policy also excludes infinity. A different domain may allow infinite bounds but still reject NaN. The important point is that is_empty answers the collection question, while validation answers the product question.

Checking only comparisons misses NaN

A common validator is:

if start >= end { reject }

With NaN, the condition is false, so invalid bounds pass. Reversing the condition to if !(start < end) catches both reversed and incomparable values, but produces one combined error.

I prefer explicit finiteness or is_nan checks when diagnostics and metrics need to distinguish them. A named finite-value newtype can move this proof to construction time.

A Range<f64> is not an Iterator

Integer ranges can commonly be iterated because the standard library knows how to step through their values. A floating range describes bounds and implements range operations such as contains and is_empty, but ordinary for value in 0.0..1.0 is not a floating step generator.

Sampling needs a step count or increment policy, along with rounding and termination rules. Adding 0.1 repeatedly can accumulate floating error.

I keep interval validation separate from sample generation. The existence of range syntax does not define a numerical grid.

total_cmp answers another question

f64::total_cmp can impose a deterministic total order including NaNs and signed zero. That is useful for sorting representations.

It does not automatically make NaN a valid measurement bound. Using a total storage order to admit invalid domain values confuses two layers.

If a domain genuinely includes multiple NaN representations, it can define interval semantics around that total order explicitly. Most numerical filters instead reject NaN before range construction.

Contains follows partial comparisons too

A range containing ordinary finite bounds will not contain NaN under normal partial comparisons. A range with NaN bounds also does not become a useful catch-all.

I test contains together with is_empty because code often validates one and queries the other. Both must reflect the same product policy after external values are decoded.

Serialized floating values deserve special attention. JSON implementations differ around NaN and infinity, while binary formats may preserve them. Validation should run after decoding and before indexing or authorization logic.

At an API boundary, an empty range can be a valid request meaning “select nothing,” while NaN is malformed input. If both collapse into one boolean, logs and client errors lose the reason. A validated wrapper type keeps this distinction after parsing instead of making every caller remember it.

What I test

My table contains increasing, equal, and decreasing finite endpoints; NaN on each side; positive and negative infinity if the input format permits them; and signed zero.

I assert the validator result separately from Rust's range result. This preserves evidence that the standard behavior is understood even when the product deliberately chooses a stricter rule.

For a database query I also test how bounds translate to the database type. Rust validation cannot guarantee another system has identical NaN ordering.

The core principle is that partial order has a third state: incomparable. Range::is_empty classifies that state as empty, which makes sense for range membership. If NaN means corrupt or forbidden input in my system, I validate it explicitly instead of using emptiness as a complete numeric health check.