The boundaries — this is where it breaks
Create search_test.go (see the panel). We write this test; your job is to make
LowerBound pass it.
The happy path — "the item is somewhere in the middle" — is easy. Every case below is where a hand-rolled binary search actually breaks.
Ten boundary cases
$ go test -run TestLowerBoundBoundaries -v .
=== RUN TestLowerBoundBoundaries/exact_match,_first
=== RUN TestLowerBoundBoundaries/exact_match,_middle
=== RUN TestLowerBoundBoundaries/exact_match,_last
=== RUN TestLowerBoundBoundaries/between_two_items
=== RUN TestLowerBoundBoundaries/before_everything
=== RUN TestLowerBoundBoundaries/AFTER_everything
=== RUN TestLowerBoundBoundaries/empty_slice
=== RUN TestLowerBoundBoundaries/single_item,_before
=== RUN TestLowerBoundBoundaries/single_item,_exact
=== RUN TestLowerBoundBoundaries/single_item,_after
--- PASS: TestLowerBoundBoundaries (0.00s)
Three of them deserve their own paragraph.
"AFTER everything" returns 4 for a 4-item library. An index that does not
exist. That is exactly why hi starts at len(items) and not len(items)-1:
the answer "past the end" needs somewhere to live. Start at len-1 and this case
silently returns the last item instead.
An empty slice returns 0 and never enters the loop, because lo < hi is
false immediately. The zero case takes care of itself — if the invariant is
right.
A one-item library is checked three times, because mid is always 0 there
and a mistake with mid + 1 turns into an infinite loop.
Duplicates: which of the three?
lib := sorted("a", "b", "b", "b", "c")
LowerBound(lib, "b") // → 1
Three "b"s. The correct answer is the first, index 1. That is what lower bound means.
If your search returns 2 or 3 it looks right and will survive a casual check. But code that scans forward from the result to collect every match will miss some of them — and the bug will surface somewhere else entirely.
transit tests its search semantics rather than assuming them, for the same
reason.
And the most important test
$ go test -run TestUnsortedInputSilentlyLies -v .
search_test.go:93: unsorted input: linear says 0, binary says 2 —
binary is wrong, and silent
Unsorted input is not an error to a binary search. It reports nothing. It simply returns the wrong number.
Sorted order is a precondition, not a recommendation. Linear search always works; binary search works only when its condition holds, and it does not check — checking would cost O(n) and destroy the entire point. Preconditions are your program's job to protect, not the search's.