Numeric::Nearest
WorksBinary-search a sorted list for the closest element — and the plural form crashes on any list of two, three or four.
- Version
0.5.1cpan:DRCLAW- Depends
none beyond the core- License
- Artistic-2.0
- Its own test suite
- 1 file, green
- Checked
- 2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Install it #
$ rakupp install Numeric::Nearestzef install Numeric::Nearest writes the same store; either installer leaves the module usable by both engines.
What it is for #
Snapping a measurement to a calibration table, a timestamp to a sample grid, a price to a tick size. The list is sorted and long, the key is arbitrary, and you want the closest entry plus its index.
Finding #
use Numeric::Nearest;
my @grid = 0, 10, 20, 30, 40, 50;
for -5, 4, 5, 14, 15, 99 -> $key {
say sprintf(' key %3d -> %s', $key, nearestPair($key, @grid).raku);
}
say '';
say 'the result is an index => value Pair, and it clamps at both ends.';
say 'an exact tie goes to the HIGHER index:';
say ' nearestPair(5, (0, 10)) = ', nearestPair(5, (0, 10)).raku;
say '';
my $r = try nearestPair(1, ());
say ' an empty list -> ', $! ?? 'throws X::OutOfRange' !! $r.raku; key -5 -> 0 => 0
key 4 -> 0 => 0
key 5 -> 1 => 10
key 14 -> 1 => 10
key 15 -> 2 => 20
key 99 -> 5 => 50
the result is an index => value Pair, and it clamps at both ends.
an exact tie goes to the HIGHER index:
nearestPair(5, (0, 10)) = 1 => 10
an empty list -> throws X::OutOfRangeMany keys at once #
use Numeric::Nearest;
my @grid = 0, 10, 20, 30, 40;
say 'nearestPairs over ascending keys:';
say ' ', nearestPairs((0, 12, 19, 41), @grid).map(*.raku).join(' ');
say '';
say 'and over unsorted ones — it feeds each search the previous answer`s';
say 'index as a starting hint, so order affects the work but not the';
say 'answer:';
my @keys = 41, 0, 19, 12;
say ' ', nearestPairs(@keys, @grid).map(*.raku).join(' ');
say '';
say 'return type : ', nearestPairs((1,), @grid).WHAT.^name;nearestPairs over ascending keys:
0 => 0 1 => 10 2 => 20 4 => 40
and over unsorted ones — it feeds each search the previous answer`s
index as a starting hint, so order affects the work but not the
answer:
4 => 40 0 => 0 2 => 20 1 => 10
return type : SeqThe one thing to know #
nearestPairs — the plural entry point — crashes on any list of two, three or four elements.
use Numeric::Nearest;
for 2, 3, 4, 5, 6 -> $n {
my @grid = (^$n).map(* * 10);
my $r = try nearestPairs((5, 15), @grid).map(*.raku).join(' ');
say sprintf(' a %d-element list -> %s', $n, $! ?? 'DIES' !! $r);
}
say '';
say 'it threads the previous key`s index in as :start, and the search';
say 'loop is bounded by $list.elems iterations — one probe short once a';
say 'start hint has been consumed. The search then falls off the end';
say 'without assigning a result, and nearestPairs calls .key on it.';
say '';
say 'nearestPair on its own is correct for EVERY list length; only the';
say 'plural form is affected, and only below five elements — which is';
say 'exactly the size you reach for when writing a first test.';
say '';
say 'map the singular form yourself:';
my @grid = 0, 10, 20;
say ' ', (5, 15).map({ nearestPair($_, @grid) }).map(*.raku).join(' '); a 2-element list -> 1 => 10 1 => 10
a 3-element list -> DIES
a 4-element list -> 1 => 10 2 => 20
a 5-element list -> 1 => 10 2 => 20
a 6-element list -> 1 => 10 2 => 20
it threads the previous key`s index in as :start, and the search
loop is bounded by $list.elems iterations — one probe short once a
start hint has been consumed. The search then falls off the end
without assigning a result, and nearestPairs calls .key on it.
nearestPair on its own is correct for EVERY list length; only the
plural form is affected, and only below five elements — which is
exactly the size you reach for when writing a first test.
map the singular form yourself:
1 => 10 2 => 20Two shapes to know #
use Numeric::Nearest;
say 'the list must be SORTED — nothing checks it, and an unsorted list';
say 'gives a silently wrong answer:';
say ' sorted : ', nearestPair(15, (0, 10, 20, 30)).raku;
say ' unsorted : ', nearestPair(15, (30, 0, 20, 10)).raku;
say '';
say 'and the unit inside lib/Numeric/Nearest.pm6 is declared';
say '`unit module Nearest;` — so after `use Numeric::Nearest` the package';
say 'Numeric::Nearest does NOT exist and `Nearest` does:';
say ' ::("Numeric::Nearest").defined : ', ::('Numeric::Nearest').defined;
say ' ::("Nearest").defined : ', ::('Nearest').defined;
say '';
say 'the two subs are exported, so you rarely need either name.';the list must be SORTED — nothing checks it, and an unsorted list
gives a silently wrong answer:
sorted : 2 => 20
unsorted : 0 => 30
and the unit inside lib/Numeric/Nearest.pm6 is declared
`unit module Nearest;` — so after `use Numeric::Nearest` the package
Numeric::Nearest does NOT exist and `Nearest` does:
::("Numeric::Nearest").defined : False
::("Nearest").defined : False
the two subs are exported, so you rarely need either name.Where the two engines differ #
Nothing in the search: an exhaustive sweep over every integer key against lists of 2 to 12 elements gives identical results on both engines, including the small-list crash and the tie convention.
Only the empty-list failure differs in wording — Raku++ says Index out of range. Is: -1, should be in 0..^Inf and Rakudo adds a Use of uninitialized value warning of its own first.
use Numeric::Nearest;
# the wrapper worth having
sub nearest($key, @sorted) {
die 'nearest: empty list' unless @sorted;
nearestPair($key, @sorted)
}
sub nearest-all(@keys, @sorted) {
@keys.map({ nearest($_, @sorted) })
}
my @grid = 0, 10, 20;
say 'nearest-all over a 3-element grid : ',
nearest-all((5, 15, 99), @grid).map(*.raku).join(' ');
my $r = try nearest(1, ());
say 'nearest(1, ()) : ', $! ?? $!.message !! $r.raku;
say '';
say 'that avoids both the small-list crash and the empty-list wording,';
say 'and costs one .map.';nearest-all over a 3-element grid : 1 => 10 2 => 20 2 => 20
nearest(1, ()) : nearest: empty list
that avoids both the small-list crash and the empty-list wording,
and costs one .map.