Skip to content

Commit 4275ba5

Browse files
committed
feat(collections/unstable): add non-exact binary search function
1 parent 096f0be commit 4275ba5

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

collections/binary_search.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
/**
4+
* A binary search that accounts for non-exact matches.
5+
*
6+
* @experimental **UNSTABLE**: New API, yet to be vetted.
7+
*
8+
* @typeParam T The type of `haystack`.
9+
*
10+
* @param haystack The array to search. This MUST be sorted in ascending order, otherwise results may be incorrect.
11+
* @param needle The value to search for.
12+
* @returns
13+
* - If `needle` is matched exactly, the index of `needle`. If multiple elements in `haystack` are equal to `needle`,
14+
* the index of the first match found (which may not be the first sequentially) is returned.
15+
* - Otherwise, the [bitwise complement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_NOT)
16+
* of `needle`'s insertion index if it were added to `haystack` in sorted order.
17+
*
18+
* Return value semantics are the same as C#'s [`Array.BinarySearch`](https://learn.microsoft.com/en-us/dotnet/api/system.array.binarysearch#system-array-binarysearch(system-array-system-object))
19+
* and Java's [`Arrays.binarySearch`](https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#binarySearch-int:A-int-).
20+
*
21+
* @example Usage
22+
* ```ts
23+
* import { binarySearch } from "@std/collections/binary-search";
24+
* import { assertEquals } from "@std/assert";
25+
*
26+
* assertEquals(binarySearch([0, 1], 0), 0);
27+
* assertEquals(binarySearch([0, 1], 1), 1);
28+
* assertEquals(binarySearch([0, 1], -0.5), -1); // (bitwise complement of 0)
29+
* assertEquals(binarySearch([0, 1], 0.5), -2); // (bitwise complement of 1)
30+
* assertEquals(binarySearch([0, 1], 1.5), -3); // (bitwise complement of 2)
31+
* ```
32+
*/
33+
export function binarySearch<
34+
T extends ArrayLike<number> | ArrayLike<bigint> | ArrayLike<string>,
35+
>(
36+
haystack: T,
37+
needle: T[number],
38+
): number {
39+
let start = 0;
40+
let mid: number;
41+
42+
for (
43+
let end = haystack.length - 1;
44+
start <= end;
45+
haystack[mid]! < needle ? start = mid + 1 : end = mid - 1
46+
) {
47+
mid = Math.floor((start + end) / 2);
48+
if (haystack[mid]! === needle) return mid;
49+
}
50+
51+
return ~start;
52+
}

collections/binary_search_test.ts

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert";
3+
import { binarySearch } from "./binary_search.ts";
4+
import { assertSpyCalls, spy } from "@std/testing/mock";
5+
6+
Deno.test("binarySearch() gives exact or non-exact indexes", async (t) => {
7+
await t.step("examples", () => {
8+
assertEquals(binarySearch([0, 1], 0), 0);
9+
assertEquals(binarySearch([0, 1], 1), 1);
10+
assertEquals(binarySearch([0, 1], -0.5), -1); // -1 == ~0 (bitwise complement)
11+
assertEquals(binarySearch([0, 1], 0.5), -2); // -2 == ~1 (bitwise complement)
12+
assertEquals(binarySearch([0, 1], 1.5), -3); // -3 == ~2 (bitwise complement)
13+
});
14+
15+
await t.step("0 elements", () => {
16+
const arr: number[] = [];
17+
assertEquals(binarySearch(arr, -1), -1);
18+
assertEquals(binarySearch(arr, 0), -1);
19+
assertEquals(binarySearch(arr, 1), -1);
20+
});
21+
22+
await t.step("1 element", () => {
23+
const arr = [0];
24+
assertEquals(binarySearch(arr, -1), -1);
25+
assertEquals(binarySearch(arr, 0), 0);
26+
assertEquals(binarySearch(arr, 1), -2);
27+
});
28+
29+
await t.step("even number of elements", () => {
30+
const arr = [0, 1];
31+
assertEquals(binarySearch(arr, -1), -1);
32+
assertEquals(binarySearch(arr, -0.5), -1);
33+
assertEquals(binarySearch(arr, 0), 0);
34+
assertEquals(binarySearch(arr, 0.5), -2);
35+
assertEquals(binarySearch(arr, 1), 1);
36+
assertEquals(binarySearch(arr, 1.5), -3);
37+
assertEquals(binarySearch(arr, 2), -3);
38+
});
39+
40+
await t.step("odd number of elements", () => {
41+
const arr = [0, 1, 2];
42+
assertEquals(binarySearch(arr, -1), -1);
43+
assertEquals(binarySearch(arr, -0.5), -1);
44+
assertEquals(binarySearch(arr, 0), 0);
45+
assertEquals(binarySearch(arr, 0.5), -2);
46+
assertEquals(binarySearch(arr, 1), 1);
47+
assertEquals(binarySearch(arr, 1.5), -3);
48+
assertEquals(binarySearch(arr, 2), 2);
49+
assertEquals(binarySearch(arr, 2.5), -4);
50+
assertEquals(binarySearch(arr, 3), -4);
51+
});
52+
53+
await t.step("bigints", () => {
54+
const arr = [0n, 1n, 3n];
55+
assertEquals(binarySearch(arr, -1n), -1);
56+
assertEquals(binarySearch(arr, 0n), 0);
57+
assertEquals(binarySearch(arr, 1n), 1);
58+
assertEquals(binarySearch(arr, 2n), -3);
59+
assertEquals(binarySearch(arr, 3n), 2);
60+
});
61+
62+
await t.step("typed arrays", () => {
63+
const arr = new Int32Array([0, 1, 3]);
64+
assertEquals(binarySearch(arr, -1), -1);
65+
assertEquals(binarySearch(arr, 0), 0);
66+
assertEquals(binarySearch(arr, 1), 1);
67+
assertEquals(binarySearch(arr, 2), -3);
68+
assertEquals(binarySearch(arr, 3), 2);
69+
});
70+
71+
await t.step("typing", () => {
72+
void (() => {
73+
// @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'bigint'.
74+
binarySearch([0n], 0);
75+
// @ts-expect-error Argument of type 'bigint' is not assignable to parameter of type 'number'.
76+
binarySearch([0], 0n);
77+
78+
// @ts-expect-error Argument of type 'undefined' is not assignable to parameter of type 'number'.
79+
binarySearch([0], undefined);
80+
});
81+
});
82+
83+
await t.step("algorithm correctness - number of loop iterations", () => {
84+
/** `Math.floor` calls act as a proxy for the number of loop iterations, as it's called once per iteration */
85+
const spyLoopIterations = () => spy(Math, "floor");
86+
87+
const arr = Array.from({ length: 1_000_000 }, (_, i) => i);
88+
89+
{
90+
using iterations = spyLoopIterations();
91+
const searchVal = 499_999;
92+
const result = binarySearch(arr, searchVal);
93+
94+
assertEquals(result, 499_999);
95+
assertSpyCalls(iterations, 1);
96+
}
97+
98+
{
99+
using iterations = spyLoopIterations();
100+
const searchVal = 499_999.1;
101+
const result = binarySearch(arr, searchVal);
102+
103+
assertEquals(result, -500_001);
104+
assertSpyCalls(iterations, 19);
105+
}
106+
});
107+
});

collections/deno.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"./aggregate-groups": "./aggregate_groups.ts",
77
"./associate-by": "./associate_by.ts",
88
"./associate-with": "./associate_with.ts",
9+
"./binary-search": "./binary_search.ts",
910
"./chunk": "./chunk.ts",
1011
"./deep-merge": "./deep_merge.ts",
1112
"./distinct": "./distinct.ts",

collections/mod.ts

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
export * from "./aggregate_groups.ts";
3232
export * from "./associate_by.ts";
3333
export * from "./associate_with.ts";
34+
export * from "./binary_search.ts";
3435
export * from "./chunk.ts";
3536
export * from "./deep_merge.ts";
3637
export * from "./distinct.ts";

0 commit comments

Comments
 (0)