Created
October 4, 2024 19:20
-
-
Save selimb/dc126c1fee687ed8a8b0ee4ac545b67a to your computer and use it in GitHub Desktop.
Typescript tests for Convex Hull Algorithm
This file contains 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
/** Typescript tests for https://www.nayuki.io/page/convex-hull-algorithm */ | |
import { convexhull, Point } from "./convex-hull"; | |
describe("convexhull", () => { | |
/*---- Fixed test vectors ----*/ | |
test("empty", () => { | |
const points: Point[] = []; | |
const actual = convexhull.makeHull(points); | |
const expected: Point[] = []; | |
expect(actual).toEqual(expected); | |
}); | |
test("one", () => { | |
const points: Point[] = [{ x: 3, y: 1 }]; | |
const actual = convexhull.makeHull(points); | |
const expected = points; | |
expect(actual).toEqual(expected); | |
}); | |
test("two duplicate", () => { | |
const points: Point[] = [ | |
{ x: 0, y: 0 }, | |
{ x: 0, y: 0 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected: Point[] = [{ x: 0, y: 0 }]; | |
expect(actual).toEqual(expected); | |
}); | |
test("two horizontal 0", () => { | |
const points: Point[] = [ | |
{ x: 2, y: 0 }, | |
{ x: 5, y: 0 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected = points; | |
expect(actual).toEqual(expected); | |
}); | |
test("two horizontal 1", () => { | |
const points: Point[] = [ | |
{ x: -6, y: -3 }, | |
{ x: -8, y: -3 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected: Point[] = [ | |
{ x: -8, y: -3 }, | |
{ x: -6, y: -3 }, | |
]; | |
expect(actual).toEqual(expected); | |
}); | |
test("two vertical 0", () => { | |
const points: Point[] = [ | |
{ x: 1, y: -4 }, | |
{ x: 1, y: 4 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected = points; | |
expect(actual).toEqual(expected); | |
}); | |
test("two vertical 1", () => { | |
const points: Point[] = [ | |
{ x: -1, y: 2 }, | |
{ x: -1, y: -3 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected: Point[] = [ | |
{ x: -1, y: -3 }, | |
{ x: -1, y: 2 }, | |
]; | |
expect(actual).toEqual(expected); | |
}); | |
test("two diagonal 0", () => { | |
const points: Point[] = [ | |
{ x: -2, y: -3 }, | |
{ x: 2, y: 0 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected = points; | |
expect(actual).toEqual(expected); | |
}); | |
test("two diagonal 1", () => { | |
const points: Point[] = [ | |
{ x: -2, y: 3 }, | |
{ x: 2, y: 0 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected = points; | |
expect(actual).toEqual(expected); | |
}); | |
test("rectangle", () => { | |
const points: Point[] = [ | |
{ x: -3, y: 2 }, | |
{ x: 1, y: 2 }, | |
{ x: 1, y: -4 }, | |
{ x: -3, y: -4 }, | |
]; | |
const actual = convexhull.makeHull(points); | |
const expected: Point[] = [ | |
{ x: -3, y: -4 }, | |
{ x: -3, y: 2 }, | |
{ x: 1, y: 2 }, | |
{ x: 1, y: -4 }, | |
]; | |
expect(actual).toEqual(expected); | |
}); | |
/*---- Randomized testing ----*/ | |
test("horizontal randomly", () => { | |
const TRIALS = 10000; | |
for (let i = 0; i < TRIALS; i++) { | |
const len = randomInt(30) + 1; | |
const points: Point[] = []; | |
if (Math.random() < 0.5) { | |
const y = randomGaussian(); | |
for (let i = 0; i < len; i++) points.push({ x: randomGaussian(), y }); | |
} else { | |
const y = randomInt(20) - 10; | |
for (let i = 0; i < len; i++) points.push({ x: randomInt(30), y }); | |
} | |
const actual = convexhull.makeHull(points); | |
const expected: Point[] = []; | |
const min = pointsMin(points); | |
const max = pointsMax(points); | |
expected.push(min); | |
if (!pointsEqual(min, max)) { | |
expected.push(max); | |
} | |
expect(actual).toEqual(expected); | |
} | |
}); | |
test("vertical randomly", () => { | |
const TRIALS = 10000; | |
for (let i = 0; i < TRIALS; i++) { | |
const len = randomInt(30) + 1; | |
const points: Point[] = []; | |
if (Math.random() < 0.5) { | |
const x = randomGaussian(); | |
for (let i = 0; i < len; i++) points.push({ x, y: randomGaussian() }); | |
} else { | |
const x = randomInt(20) - 10; | |
for (let i = 0; i < len; i++) points.push({ x, y: randomInt(30) }); | |
} | |
const actual = convexhull.makeHull(points); | |
const expected: Point[] = []; | |
const min = pointsMin(points); | |
const max = pointsMax(points); | |
expected.push(min); | |
if (!pointsEqual(min, max)) { | |
expected.push(max); | |
} | |
expect(actual).toEqual(expected); | |
} | |
}); | |
test("vs naive randomly", () => { | |
const TRIALS = 1000; | |
for (let i = 0; i < TRIALS; i++) { | |
const len = randomInt(100); | |
const points: Point[] = []; | |
if (Math.random() < 0.5) { | |
for (let i = 0; i < len; i++) points.push({ x: randomGaussian(), y: randomGaussian() }); | |
} else { | |
for (let i = 0; i < len; i++) points.push({ x: randomInt(10), y: randomInt(10) }); | |
} | |
const actual = convexhull.makeHull(points); | |
const expected = makeHullNaive(points); | |
expect(actual).toEqual(expected); | |
} | |
}); | |
test("hull properties randomly", () => { | |
const TRIALS = 1000; | |
for (let i = 0; i < TRIALS; i++) { | |
// Generate random points | |
const len = randomInt(100); | |
const points: Point[] = []; | |
if (Math.random() < 0.5) { | |
for (let i = 0; i < len; i++) points.push({ x: randomGaussian(), y: randomGaussian() }); | |
} else { | |
for (let i = 0; i < len; i++) points.push({ x: randomInt(10), y: randomInt(10) }); | |
} | |
// Compute hull and check properties | |
const hull = convexhull.makeHull(points); | |
expect(isPolygonConvex(hull)).toBe(true); | |
for (const p of points) expect(isPointInConvexPolygon(hull, p)).toBe(true); | |
// Add duplicate points and check new hull | |
if (points.length > 0) { | |
const dupe = randomInt(10) + 1; | |
for (let j = 0; j < dupe; j++) points.push(points[randomInt(points.length)]); | |
const nextHull = convexhull.makeHull(points); | |
expect(hull).toEqual(nextHull); | |
} | |
} | |
}); | |
function makeHullNaive(points: Point[]): Point[] { | |
if (points.length <= 1) { | |
return points.slice(); | |
} | |
const result: Point[] = []; | |
// Jarvis march / gift wrapping algorithm | |
let point = pointsMin(points); | |
do { | |
result.push(point); | |
let next = points[0]; | |
for (const p of points) { | |
const ax = next.x - point.x; | |
const ay = next.y - point.y; | |
const bx = p.x - point.x; | |
const by = p.y - point.y; | |
const cross = ax * by - ay * bx; | |
if (cross > 0 || (cross == 0 && bx * bx + by * by > ax * ax + ay * ay)) next = p; | |
} | |
point = next; | |
} while (!pointsEqual(point, result[0])); | |
return result; | |
} | |
function isPolygonConvex(points: Point[]): boolean { | |
let signum = 0; | |
for (let i = 0; i + 2 < points.length; i++) { | |
const p = points[i + 0]; | |
const q = points[i + 1]; | |
const r = points[i + 2]; | |
const sign = Math.sign((q.x - p.x) * (r.y - q.y) - (q.y - p.y) * (r.x - q.x)); | |
if (sign == 0) continue; | |
else if (signum == 0) signum = sign; | |
else if (sign != signum) return false; | |
} | |
return true; | |
} | |
function isPointInConvexPolygon(polygon: Point[], point: Point): boolean { | |
let signum = 0; | |
for (let i = 0; i < polygon.length; i++) { | |
const p = polygon[i]; | |
const q = polygon[(i + 1) % polygon.length]; | |
const sign = Math.sign((q.x - p.x) * (point.y - q.y) - (q.y - p.y) * (point.x - q.x)); | |
if (sign == 0) continue; | |
else if (signum == 0) signum = sign; | |
else if (sign != signum) return false; | |
} | |
return true; | |
} | |
function pointsEqual(a: Point, b: Point): boolean { | |
return a.x === b.x && a.y === b.y; | |
} | |
function pointsMin(points: Point[]): Point { | |
return points.reduce((a, b) => (a.x < b.x || (a.x === b.x && a.y < b.y) ? a : b)); | |
} | |
function pointsMax(points: Point[]): Point { | |
return points.reduce((a, b) => (a.x > b.x || (a.x === b.x && a.y > b.y) ? a : b)); | |
} | |
function randomGaussian(): number { | |
let u = 0; | |
let v = 0; | |
while (u === 0) u = Math.random(); | |
while (v === 0) v = Math.random(); | |
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); | |
} | |
function randomInt(maxValue: number): number { | |
return Math.floor(Math.random() * maxValue); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment