Skip to content

JustDummies: document how users deal with NaN and the infinities #309

Description

@Reefact

Problem

Anyone testing code that consumes double, float or Half will meet NaN and the infinities eventually — a division, a parse, a sensor reading, an external payload. JustDummies has a clear and deliberate position on them, but from a user's seat that position is invisible until it throws, and it offers no exit.

What a user currently finds:

  • Any.Double() never draws a non-finite value — and every argument-taking method rejects one too. ContinuousIntervalSpec.EnsureFinite (ContinuousIntervalSpec.cs:35) is called from 27 sites across AnyDouble, AnySingle and AnyHalf: bounds, OneOf, Except, DifferentFrom. So Any.Double().Except(double.NaN) — a perfectly natural first attempt — throws an ArgumentException.
  • The message, "The value must be finite: NaN and infinities are never generated.", states the rule and stops. It does not say what to do instead.
  • The only user-facing mention is one clause in JustDummies/README.nuget.md"finite values only — never NaN or infinities" — which documents the restriction, not the workaround.

So the library looks like it is missing a feature, when in fact the need is already served through another door.

The questions a user actually has

  1. "My code must handle NaN — how do I get one in a test?"
  2. "Why does Any.Double().Except(double.NaN) throw?" (the rule is on arguments too, not only on draws)
  3. "Is decimal the same?" — no, and the answer is not a design choice: System.Decimal has no NaN or infinity representation at all. AnyDecimal runs on DecimalIntervalSpec, which carries no finiteness guard because there is nothing to guard. A user looking for the symmetry with AnyDouble will not find it and may file it as a gap.
  4. "If I do get a NaN into a pool or a collection, does it behave?" — not entirely, and the trap is worth naming (see below).

Direction

Write a short, focused recipe — user-facing, not architectural.

The rule and its reason. Non-finite values are never drawn and never accepted as arguments, because "arbitrary test values should cross invariants, not sabotage arithmetic" (ContinuousIntervalSpec.cs:8). A NaN silently drawn into an unrelated arrangement would fail an assertion the test never meant to make.

The exit. The generic entry points carry no finiteness guard — AnyOneOf<T>.FromPool (AnyOneOf.cs:39-40) validates only non-empty pool and no null element, and As (AnyExtensions.cs:36) projects freely. So Any.OneOf(double.NaN, double.PositiveInfinity) and Any.Double().As(...) are the supported routes. This is by construction, not an oversight: the library cannot judge the semantics of an opaque T.

Which shape to use, and when — the part that actually helps:

  • The test asserts on the NaN path (Should_reject_a_non_finite_amount): write double.NaN as a literal. It is the subject of the test, so it belongs at the call site in plain sight. No generator needed.
  • A non-finite value is a legitimate value of the domain (NaN meaning "missing measurement" in a scientific pipeline): Any.OneOf(double.NaN, 1.0, 2.0) is the right expression — here it really is a dummy drawn from the domain's real value set.
  • Warn against the middle ground: a mixed pool used to "also cover NaN" exercises the NaN path only on some seeds. The test passes or fails depending on the draw, and it does not report which branch it took.

The composition trap, worth one paragraph. EqualityComparer<double>.Default.Equals(NaN, NaN) is true while NaN == NaN is false. A NaN flowing into Distinct() (or into any comparer-based collection) therefore dedupes, while user code comparing with == sees two different values. Anyone deliberately putting NaN in a pool should know this before it surprises them.

Same posture across the family: AnySingle and AnyHalf behave exactly like AnyDouble; AnyDecimal is out of scope for the whole subject (see above).

Where it goes:

  • A recipe section in JustDummies/README.nuget.md.
  • The same in the JustDummies user guide when it lands (audit recommendation §11.6).
  • Make the wall point at its own exit: a <remarks> on the floating-point builders' XML docs naming Any.OneOf / As, so the answer is reachable from IntelliSense at the moment the user is blocked.

Acceptance criteria

  • README.nuget.md carries the recipe: the rule, the reason, the exit, and the choice between a literal and a pool.
  • The decimal clarification is stated (no NaN/infinity representation exists — nothing to generate, nothing to guard).
  • The Equals/== asymmetry is documented for users who deliberately pool a NaN.
  • The floating-point builders' XML docs name the escape hatch.
  • The documented recipe is locked in by a test (JustDummies.UnitTests) so the doc cannot rot silently: the guarded entry points still reject a non-finite argument, and the generic pool still yields one.
  • The French user documentation is updated in step, per the repository's EN-canonical / FR-translation rule.
  • No behavioural change and no public API change is implied by this issue.

Context

Surfaced while assessing whether JustDummies should offer an explicit non-finite or out-of-domain generator (WithNaN(), Undeclared()). The conclusion was that it should not — but that the need behind the question is real, already served, and completely undocumented.

The governance half of the same subject — recording the arbitrary-yet-valid boundary as an ADR — is tracked separately in #308. This issue is the user-facing half and stands on its own: it is worth doing whether or not that ADR is ever written.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions