Nearest[list, x]: the closest element(s), with all ties returned - #53
Open
msollami wants to merge 1 commit into
Open
Nearest[list, x]: the closest element(s), with all ties returned#53msollami wants to merge 1 commit into
msollami wants to merge 1 commit into
Conversation
Adds the two-argument Nearest: the element(s) of list at minimum
Abs[element - x], as a List in original order.
All tied elements are returned, which is what fixes the algorithm.
Nearest[{1, 5, 10}, 3] is {1, 5}, not {1}. The shape is MinimalBy's
(src/sort.c:663-716) — find the minimum, then collect every distance
equal to it — rather than the RankedMin quickselect, whose comparator
carries an original-index tiebreak (src/sort.c:932) that exists to make
ties impossible and would return a single element. Input order among
ties falls out of the ascending collect pass, so there is no tie logic
in the file.
Distance composes the existing internal_subtract and internal_abs the
way comparisons.c:313-314 already does; no distance helper was added.
Abs of a complex difference is its modulus, so Nearest[{3 + 4 I, 1}, 0]
is {1} with no extra code.
Distances are ordered by numeric value, not by expr_compare. Canonical
order is wrong here in both directions, and each direction breaks the
all-ties guarantee. It settles a value tie between different ExprTypes
on the type enum (src/sort.c:376), so Nearest[{0, 2.0}, 1] would answer
{0} — distances 1 and 1.0 are equal but Integer sorts before Real —
dropping a tied element from the one function whose contract is to
return them all. And for atoms that are not both integer-like it
compares get_numeric_value() doubles (src/sort.c:372-377), so
Nearest[{1/3, 1/3 + 1/10^18}, 0] would report a tie between distances
that differ exactly. Subtracting is exact where it must be and inexact
only where the input already was: 1 - 1.0 is 0.0, a genuine tie, while
1/3 - (1/3 + 1/10^18) is the exact Rational[-1, 10^18]. nearest_sign
reports undecidable separately from zero, which expr_numeric_sign
cannot — it returns a bare 0 for both and recognises neither MPFR nor a
bigint-component Rational.
Nearest diverges from MinimalBy in one respect, deliberately: every
distance must be a real number or the call stays unevaluated.
MinimalBy[{1, a, 3}, Abs[# - 2] &] answers {1, 3}, dropping the symbolic
element because expr_compare orders symbols after all numbers — a
plausible wrong answer. Gating on the distance rather than the element
covers a symbolic element, a symbolic target and a non-real complex in
one check.
Two limitations are pinned by test rows rather than left silent. A
symbolic real such as Pi declines instead of being numericalized. A
rational with a bigint component declines for a reason outside this
file: builtin_abs does not evaluate one (Abs[1/1000] is 1/1000,
Abs[1/10^25] is Abs[1/10^25], and Sign has the same gap), so the
distance arrives unevaluated and the gate rejects it. Both rows flip the
day the underlying behaviour changes.
Only the two-argument form lands here; the n-nearest, radius, rule,
all-pairs and NearestTo forms and the DistanceFunction option are
separate. A packed list is materialised on the way in since Nearest is
not on pack.c's AWARE list; a visible NDArray is not a List and stays
unevaluated rather than being silently truncated.
New: src/list/nearest.{c,h}, registered in list_init.c with Protected.
30 acceptance rows in tests/test_list.c::test_nearest. make check-c99,
check-packed-aware and check-array-exactness pass; leak-free under a
differential leaks run (0 bytes at 200 and 20000 iterations across the
success, gate-bail, mixed-type-tie and decline paths).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the two-argument
Nearest[list, x]— the element(s) oflistat minimumAbs[element - x], returned as aListin original order, with all tied elements included.Nearest[{1, 5, 10}, 3]is{1, 5}, not{1}.Changes
New:
src/list/nearest.{c,h}, registered insrc/list/list_init.cwithProtectedand a docstring.Algorithm —
MinimalBy's two-pass shape (src/sort.c:663-716): find the minimum, then collect every distance equal to it. Deliberately not theRankedMinquickselect, whose comparator carries an original-index tiebreak (src/sort.c:932) that exists to make ties impossible and would return a single element. Input order among ties falls out of the ascending collect pass, so there is no tie logic in the file.Distance composes the existing
internal_subtractandinternal_absthe waycomparisons.c:313-314already does; no distance helper was added. SinceAbsof a complex difference is its modulus,Nearest[{3 + 4 I, 1}, 0]is{1}with no extra code.Distances are ordered by numeric value, not by
expr_compare. Canonical order is wrong here in both directions, and each direction breaks the all-ties guarantee:ExprTypes on the type enum (src/sort.c:376), soNearest[{0, 2.0}, 1]would answer{0}— distances1and1.0are equal butIntegersorts beforeReal— dropping a tied element from the one function whose contract is to return them all.get_numeric_value()doubles (src/sort.c:372-377), soNearest[{1/3, 1/3 + 1/10^18}, 0]would report a tie between distances that differ exactly.Subtracting instead is exact where it must be and inexact only where the input already was:
1 - 1.0is0.0, a genuine tie, while1/3 - (1/3 + 1/10^18)is the exactRational[-1, 10^18].nearest_signreports undecidable separately from zero, whichexpr_numeric_signcannot — it returns a bare0for both and recognises neitherMPFRnor a bigint-componentRational.One deliberate divergence from
MinimalBy: every distance must be a real number, or the call stays unevaluated.MinimalBy[{1, a, 3}, Abs[# - 2] &]answers{1, 3}, dropping the symbolic element becauseexpr_compareorders symbols after all numbers — a plausible wrong answer. Gating on the distance rather than the element covers a symbolic element, a symbolic target and a non-real complex in one check.Known limitations, pinned by test rows rather than left silent
Both flip the day the underlying behaviour changes, so they surface as a table diff:
Pideclines rather than being numericalized (ranked_numeric_keywould).builtin_absdoes not evaluate one.Abs[1/1000]is1/1000butAbs[1/10^25]isAbs[1/10^25], andSignhas the same gap — so the distance arrives unevaluated and the gate rejects it. Worth fixing inbuiltin_abs, where every caller benefits; not done here to keep this diff to one builtin.Scope
Two-argument form only. The
n-nearest, radius, rule, all-pairs andNearestTooperator forms and theDistanceFunctionoption are deliberately excluded.A packed list is materialised on the way in, since
Nearestis not onpack.c'sAWARElist. A visibleNDArrayis not aListand stays unevaluated rather than being silently truncated.Testing
29 acceptance rows in
tests/test_list.c::test_nearest, covering ties (input order, mixed exact/inexact, exact rational, duplicates, symmetric), unique-nearest, empty lists, the eight unevaluated-gate cases, packed input and attributes.list_testspasses; 17 list/array/sort/packing binaries pass with no regressionmake check-c99,make check-packed-aware,make check-array-exactnesspassleaksrun: 0 bytes at both 200 and 20 000 iterations across the success, gate-bail, mixed-type-tie and decline pathsDocs
docs/spec/builtins/lists-and-iteration.mdgains a## Nearestentry;docs/spec/changelog/2026-08-03.mdrecords the rationale.