diff --git a/TEST_README.md b/TEST_README.md new file mode 100644 index 0000000..8dfc635 --- /dev/null +++ b/TEST_README.md @@ -0,0 +1,61 @@ +# Tests for findClosestRationalAngle + +This directory contains comprehensive tests for the `findClosestRationalAngle` function. + +## Running the Tests + +To run all tests: +```bash +deno test --allow-read +``` + +Or using the task defined in `deno.json`: +```bash +deno task test +``` + +To run just the lib tests: +```bash +deno test src/lib_test.ts --allow-read +``` + +## Test Coverage + +The test suite for `findClosestRationalAngle` covers: + +### Exact Matches +- Tests for angles that exactly match rational angles in the RATIONAL_ANGLES array +- Examples: 0°, 45°, 90°, -45°, -90°, 26.565° + +### Closest Angle Finding +- Tests for angles that are close to but not exactly matching rational angles +- Examples: 44° → 45°, 46° → 45°, 25° → 26.565°, 30° → 26.565° + +### Angle Normalization +- Tests for angles > 360° (e.g., 405° → 45°, 720° → 0°) +- Tests for negative angles (e.g., -45°, -90°, -30°) +- Tests for very large angles (e.g., 1440°, -1440°) +- Tests for edge cases around 0° (e.g., 359°, 1°, -1°, -359°) + +### Fractional Angles +- Tests for non-integer angles (e.g., 44.5° → 45°) + +### Edge Cases +- Tests for angles near boundaries (e.g., 89° → 90°, 91° → 90°) +- Tests that verify the result always comes from RATIONAL_ANGLES array +- Tests that verify all required fields are present in the returned angleEntry object + +## Test Count + +The test suite includes **27 comprehensive test cases** covering all aspects of the `findClosestRationalAngle` function. + +## Function Behavior + +The `findClosestRationalAngle` function: +1. Normalizes input angles to the 0-360° range +2. Finds the rational angle with the smallest absolute difference from the normalized angle +3. Returns an `angleEntry` object containing: + - `degrees`: The angle in degrees + - `m`: The numerator of the rational angle (tan(θ) = m/n) + - `n`: The denominator of the rational angle (tan(θ) = m/n) + - `label`: A human-readable label for the angle diff --git a/deno.json b/deno.json index b61140f..2d64b1b 100644 --- a/deno.json +++ b/deno.json @@ -6,7 +6,8 @@ "sharp": "npm:sharp@^0.34.5" }, "tasks": { - "compile": "deno compile --allow-all main.ts" + "compile": "deno compile --allow-all main.ts", + "test": "deno test --allow-read" }, "version": "0.0.1" } diff --git a/src/lib_test.ts b/src/lib_test.ts new file mode 100644 index 0000000..963b9ed --- /dev/null +++ b/src/lib_test.ts @@ -0,0 +1,190 @@ +import { findClosestRationalAngle, RATIONAL_ANGLES, type angleEntry } from "./lib.ts"; + +Deno.test("findClosestRationalAngle - exact match for 0 degrees", () => { + const result = findClosestRationalAngle(0); + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); + if (result.m !== 0) throw new Error(`Expected m=0, got ${result.m}`); + if (result.n !== 1) throw new Error(`Expected n=1, got ${result.n}`); + if (result.label !== "0°") throw new Error(`Expected label "0°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - exact match for 45 degrees", () => { + const result = findClosestRationalAngle(45); + if (result.degrees !== 45) throw new Error(`Expected 45, got ${result.degrees}`); + if (result.m !== 1) throw new Error(`Expected m=1, got ${result.m}`); + if (result.n !== 1) throw new Error(`Expected n=1, got ${result.n}`); + if (result.label !== "45°") throw new Error(`Expected label "45°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - exact match for 90 degrees", () => { + const result = findClosestRationalAngle(90); + if (result.degrees !== 90) throw new Error(`Expected 90, got ${result.degrees}`); + if (result.m !== 1) throw new Error(`Expected m=1, got ${result.m}`); + if (result.n !== 0) throw new Error(`Expected n=0, got ${result.n}`); + if (result.label !== "90°") throw new Error(`Expected label "90°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - exact match for -45 degrees", () => { + const result = findClosestRationalAngle(-45); + if (result.degrees !== -45) throw new Error(`Expected -45, got ${result.degrees}`); + if (result.m !== -1) throw new Error(`Expected m=-1, got ${result.m}`); + if (result.n !== 1) throw new Error(`Expected n=1, got ${result.n}`); + if (result.label !== "-45°") throw new Error(`Expected label "-45°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - exact match for -90 degrees", () => { + const result = findClosestRationalAngle(-90); + if (result.degrees !== -90) throw new Error(`Expected -90, got ${result.degrees}`); + if (result.m !== -1) throw new Error(`Expected m=-1, got ${result.m}`); + if (result.n !== 0) throw new Error(`Expected n=0, got ${result.n}`); + if (result.label !== "-90°") throw new Error(`Expected label "-90°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - exact match for 26.565 degrees", () => { + const result = findClosestRationalAngle(26.565); + if (result.degrees !== 26.565) throw new Error(`Expected 26.565, got ${result.degrees}`); + if (result.m !== 1) throw new Error(`Expected m=1, got ${result.m}`); + if (result.n !== 2) throw new Error(`Expected n=2, got ${result.n}`); + if (result.label !== "26.565° (arctan 1/2)") throw new Error(`Expected label "26.565° (arctan 1/2)", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - finds closest angle for 44 degrees", () => { + const result = findClosestRationalAngle(44); + if (result.degrees !== 45) throw new Error(`Expected 45, got ${result.degrees}`); + if (result.label !== "45°") throw new Error(`Expected label "45°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - finds closest angle for 46 degrees", () => { + const result = findClosestRationalAngle(46); + if (result.degrees !== 45) throw new Error(`Expected 45, got ${result.degrees}`); + if (result.label !== "45°") throw new Error(`Expected label "45°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - finds closest angle for 25 degrees", () => { + const result = findClosestRationalAngle(25); + if (result.degrees !== 26.565) throw new Error(`Expected 26.565, got ${result.degrees}`); + if (result.label !== "26.565° (arctan 1/2)") throw new Error(`Expected label "26.565° (arctan 1/2)", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - finds closest angle for 30 degrees", () => { + const result = findClosestRationalAngle(30); + // 30 is closer to 26.565 (diff: 3.435) than to 33.69 (diff: 3.69) + if (result.degrees !== 26.565) throw new Error(`Expected 26.565, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles angle > 360 degrees (405)", () => { + const result = findClosestRationalAngle(405); + // 405 % 360 = 45 + if (result.degrees !== 45) throw new Error(`Expected 45, got ${result.degrees}`); + if (result.label !== "45°") throw new Error(`Expected label "45°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - handles angle > 360 degrees (370)", () => { + const result = findClosestRationalAngle(370); + // 370 % 360 = 10, closest to 11.31° (arctan 1/5) + if (result.degrees !== 11.31) throw new Error(`Expected 11.31, got ${result.degrees}`); + if (result.label !== "11.310° (arctan 1/5)") throw new Error(`Expected label "11.310° (arctan 1/5)", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - handles large positive angle (720)", () => { + const result = findClosestRationalAngle(720); + // 720 % 360 = 0 + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); + if (result.label !== "0°") throw new Error(`Expected label "0°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - handles negative angle (-45)", () => { + const result = findClosestRationalAngle(-45); + if (result.degrees !== -45) throw new Error(`Expected -45, got ${result.degrees}`); + if (result.label !== "-45°") throw new Error(`Expected label "-45°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - handles negative angle (-90)", () => { + const result = findClosestRationalAngle(-90); + if (result.degrees !== -90) throw new Error(`Expected -90, got ${result.degrees}`); + if (result.label !== "-90°") throw new Error(`Expected label "-90°", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - handles negative angle (-30)", () => { + const result = findClosestRationalAngle(-30); + // -30 % 360 = -30, then +360 = 330 + // 330 is closest to -26.565° (which normalizes to 333.435°, diff: 3.435) + if (result.degrees !== -26.565) throw new Error(`Expected -26.565, got ${result.degrees}`); + if (result.label !== "-26.565° (arctan -1/2)") throw new Error(`Expected "-26.565° (arctan -1/2)", got ${result.label}`); +}); + +Deno.test("findClosestRationalAngle - handles angle close to 0 from negative side (-1)", () => { + const result = findClosestRationalAngle(-1); + // -1 % 360 = -1, then +360 = 359, closest to 0° + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles angle 359 degrees", () => { + const result = findClosestRationalAngle(359); + // 359 is closest to 0° (1 degree away) + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles angle 1 degree", () => { + const result = findClosestRationalAngle(1); + // 1 is closest to 0° + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles negative angle close to -360 (-359)", () => { + const result = findClosestRationalAngle(-359); + // -359 % 360 = -359, then +360 = 1, closest to 0° + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles very large positive angle (1440)", () => { + const result = findClosestRationalAngle(1440); // 4 full rotations + // 1440 % 360 = 0 + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles very large negative angle (-1440)", () => { + const result = findClosestRationalAngle(-1440); // -4 full rotations + // -1440 % 360 = 0 + if (result.degrees !== 0) throw new Error(`Expected 0, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles fractional angle 44.5", () => { + const result = findClosestRationalAngle(44.5); + // 44.5 is closest to 45° (0.5 away) + if (result.degrees !== 45) throw new Error(`Expected 45, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles angle 89", () => { + const result = findClosestRationalAngle(89); + // 89 is closest to 90° (1 away) vs 78.69° (10.31 away) + if (result.degrees !== 90) throw new Error(`Expected 90, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - handles angle 91", () => { + const result = findClosestRationalAngle(91); + // 91 is closest to 90° (1 away) + if (result.degrees !== 90) throw new Error(`Expected 90, got ${result.degrees}`); +}); + +Deno.test("findClosestRationalAngle - returns an angleEntry object with all required fields", () => { + const result = findClosestRationalAngle(45); + if (typeof result.degrees !== "number") throw new Error("degrees should be number"); + if (typeof result.m !== "number") throw new Error("m should be number"); + if (typeof result.n !== "number") throw new Error("n should be number"); + if (typeof result.label !== "string") throw new Error("label should be string"); +}); + +Deno.test("findClosestRationalAngle - always returns one of the predefined RATIONAL_ANGLES", () => { + const testAngles = [0, 15, 30, 45, 60, 75, 90, 180, 270, -45, -90, 360, 400]; + + for (const angle of testAngles) { + const result = findClosestRationalAngle(angle); + const isInArray = RATIONAL_ANGLES.some( + (ra) => ra.degrees === result.degrees && ra.label === result.label + ); + if (!isInArray) { + throw new Error(`Result for angle ${angle} should be in RATIONAL_ANGLES`); + } + } +});