diff --git a/app/api/routes-f/iqr-outlier/route.test.ts b/app/api/routes-f/iqr-outlier/route.test.ts new file mode 100644 index 00000000..144bbb3f --- /dev/null +++ b/app/api/routes-f/iqr-outlier/route.test.ts @@ -0,0 +1,124 @@ +import { POST } from "./route"; + +describe("IQR Outlier Detection API", () => { + it("should return 400 for invalid JSON", async () => { + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: "bad json", + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("should return 400 when data is missing", async () => { + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({}), + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("should return 400 when data is empty", async () => { + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: [] }), + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("should return 400 when data contains non-numbers", async () => { + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: [1, "two", 3] }), + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("should return 400 for a negative multiplier", async () => { + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: [1, 2, 3, 4, 5], multiplier: -1 }), + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("detects dataset with no outliers", async () => { + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }), + }); + const res = await POST(req as any); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.outliers).toEqual([]); + expect(typeof data.q1).toBe("number"); + expect(typeof data.q3).toBe("number"); + expect(typeof data.iqr).toBe("number"); + expect(typeof data.lower_bound).toBe("number"); + expect(typeof data.upper_bound).toBe("number"); + }); + + it("detects obvious outliers", async () => { + // Dataset: 1-10 with 100 as an outlier + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100] }), + }); + const res = await POST(req as any); + const data = await res.json(); + expect(data.outliers).toContain(100); + }); + + it("uses default multiplier of 1.5", async () => { + const dataset = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100]; + const req1 = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: dataset }), + }); + const req2 = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: dataset, multiplier: 1.5 }), + }); + const [res1, res2] = await Promise.all([POST(req1 as any), POST(req2 as any)]); + const [d1, d2] = await Promise.all([res1.json(), res2.json()]); + expect(d1).toEqual(d2); + }); + + it("custom multiplier changes bounds", async () => { + const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15]; + const reqDefault = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data, multiplier: 1.5 }), + }); + const reqStrict = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data, multiplier: 0.5 }), + }); + const [resDefault, resStrict] = await Promise.all([ + POST(reqDefault as any), + POST(reqStrict as any), + ]); + const [dDefault, dStrict] = await Promise.all([resDefault.json(), resStrict.json()]); + // Stricter multiplier yields narrower bounds, more outliers + expect(dStrict.upper_bound).toBeLessThan(dDefault.upper_bound); + }); + + it("computes correct IQR values for a known dataset", async () => { + // sorted: [2, 4, 6, 8, 10] → Q1=4, Q3=8, IQR=4 + const req = new Request("http://localhost/api/routes-f/iqr-outlier", { + method: "POST", + body: JSON.stringify({ data: [10, 2, 6, 8, 4] }), + }); + const res = await POST(req as any); + const data = await res.json(); + expect(data.q1).toBeCloseTo(4); + expect(data.q3).toBeCloseTo(8); + expect(data.iqr).toBeCloseTo(4); + expect(data.lower_bound).toBeCloseTo(4 - 1.5 * 4); // -2 + expect(data.upper_bound).toBeCloseTo(8 + 1.5 * 4); // 14 + }); +}); diff --git a/app/api/routes-f/iqr-outlier/route.ts b/app/api/routes-f/iqr-outlier/route.ts new file mode 100644 index 00000000..22136d60 --- /dev/null +++ b/app/api/routes-f/iqr-outlier/route.ts @@ -0,0 +1,54 @@ +import { type NextRequest, NextResponse } from "next/server"; + +/** + * Calculates the quartile value for a sorted dataset using linear interpolation. + * Uses the inclusive method (same as Excel's QUARTILE.INC / NumPy default). + */ +function quartile(sorted: number[], q: 0.25 | 0.75): number { + const pos = q * (sorted.length - 1); + const lower = Math.floor(pos); + const upper = Math.ceil(pos); + const frac = pos - lower; + return sorted[lower] + frac * (sorted[upper] - sorted[lower]); +} + +export async function POST(req: NextRequest) { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 }); + } + + if (typeof body !== "object" || body === null || Array.isArray(body)) { + return NextResponse.json({ error: "Request body must be an object." }, { status: 400 }); + } + + const { data, multiplier } = body as Record; + + if (!Array.isArray(data) || data.length === 0 || data.some((v) => typeof v !== "number")) { + return NextResponse.json( + { error: "data must be a non-empty array of numbers." }, + { status: 400 } + ); + } + + const m = multiplier !== undefined ? multiplier : 1.5; + if (typeof m !== "number" || m < 0) { + return NextResponse.json( + { error: "multiplier must be a non-negative number." }, + { status: 400 } + ); + } + + const sorted = [...(data as number[])].sort((a, b) => a - b); + + const q1 = quartile(sorted, 0.25); + const q3 = quartile(sorted, 0.75); + const iqr = q3 - q1; + const lower_bound = q1 - m * iqr; + const upper_bound = q3 + m * iqr; + const outliers = sorted.filter((v) => v < lower_bound || v > upper_bound); + + return NextResponse.json({ q1, q3, iqr, lower_bound, upper_bound, outliers }); +} diff --git a/app/api/routes-f/polynomial-eval/route.test.ts b/app/api/routes-f/polynomial-eval/route.test.ts new file mode 100644 index 00000000..be33cb4c --- /dev/null +++ b/app/api/routes-f/polynomial-eval/route.test.ts @@ -0,0 +1,112 @@ +import { POST } from "./route"; + +describe("Polynomial Evaluator API", () => { + it("should return 400 for invalid JSON", async () => { + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: "not-json", + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBeDefined(); + }); + + it("should return 400 when coefficients is missing", async () => { + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ x: 2 }), + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("should return 400 when coefficients is empty", async () => { + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients: [], x: 2 }), + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("should return 400 when x is not a number or array", async () => { + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients: [1, 2, 3], x: "hello" }), + }); + const res = await POST(req as any); + expect(res.status).toBe(400); + }); + + it("evaluates a constant polynomial f(x) = 5", async () => { + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients: [5], x: 10 }), + }); + const res = await POST(req as any); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.results).toEqual([5]); + }); + + it("evaluates f(x) = 2x + 1 at x=3 via Horner's method", async () => { + // Horner: coefficients = [2, 1], x=3 → 2*3+1 = 7 + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients: [2, 1], x: 3 }), + }); + const res = await POST(req as any); + const data = await res.json(); + expect(data.results).toEqual([7]); + }); + + it("evaluates f(x) = 3x² + 2x + 1 at x=2 via Horner (matches expanded form)", async () => { + // Expanded: 3*4 + 2*2 + 1 = 17. Horner: [3,2,1], x=2 → ((3*2)+2)*2+1=17 + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients: [3, 2, 1], x: 2 }), + }); + const res = await POST(req as any); + const data = await res.json(); + expect(data.results).toEqual([17]); + }); + + it("evaluates a polynomial at multiple x values", async () => { + // f(x) = x^2 = [1, 0, 0], x=[0,2,3] → [0,4,9] + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients: [1, 0, 0], x: [0, 2, 3] }), + }); + const res = await POST(req as any); + const data = await res.json(); + expect(data.results).toEqual([0, 4, 9]); + }); + + it("Horner result matches expanded form for degree-3 polynomial", async () => { + // f(x) = 2x^3 - 3x^2 + x - 5 at x=4 + // Expanded: 2*64 - 3*16 + 4 - 5 = 128 - 48 + 4 - 5 = 79 + const x = 4; + const coefficients = [2, -3, 1, -5]; + const expanded = 2 * x ** 3 - 3 * x ** 2 + 1 * x - 5; + + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients, x }), + }); + const res = await POST(req as any); + const data = await res.json(); + expect(data.results[0]).toBeCloseTo(expanded); + }); + + it("handles x=0", async () => { + // f(0) = constant term (last coef) + const req = new Request("http://localhost/api/routes-f/polynomial-eval", { + method: "POST", + body: JSON.stringify({ coefficients: [5, 3, 7], x: 0 }), + }); + const res = await POST(req as any); + const data = await res.json(); + expect(data.results).toEqual([7]); + }); +}); diff --git a/app/api/routes-f/polynomial-eval/route.ts b/app/api/routes-f/polynomial-eval/route.ts new file mode 100644 index 00000000..6a06e226 --- /dev/null +++ b/app/api/routes-f/polynomial-eval/route.ts @@ -0,0 +1,50 @@ +import { type NextRequest, NextResponse } from "next/server"; + +/** + * Evaluates a polynomial at a given x value using Horner's method. + * Coefficients are ordered highest-degree first. + * e.g. [3, 2, 1] represents 3x² + 2x + 1 + */ +function horner(coefficients: number[], x: number): number { + return coefficients.reduce((acc, coef) => acc * x + coef, 0); +} + +export async function POST(req: NextRequest) { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 }); + } + + if (typeof body !== "object" || body === null || Array.isArray(body)) { + return NextResponse.json({ error: "Request body must be an object." }, { status: 400 }); + } + + const { coefficients, x } = body as Record; + + if ( + !Array.isArray(coefficients) || + coefficients.length === 0 || + coefficients.some((c) => typeof c !== "number") + ) { + return NextResponse.json( + { error: "coefficients must be a non-empty array of numbers." }, + { status: 400 } + ); + } + + const isArrayOfX = Array.isArray(x); + const xValues: unknown[] = isArrayOfX ? x : [x]; + + if (xValues.some((v) => typeof v !== "number")) { + return NextResponse.json( + { error: "x must be a number or an array of numbers." }, + { status: 400 } + ); + } + + const results = (xValues as number[]).map((xVal) => horner(coefficients as number[], xVal)); + + return NextResponse.json({ results }); +} diff --git a/app/api/routesF/reverse-word-order/route.test.ts b/app/api/routesF/reverse-word-order/route.test.ts new file mode 100644 index 00000000..945ca9cb --- /dev/null +++ b/app/api/routesF/reverse-word-order/route.test.ts @@ -0,0 +1,110 @@ +import { POST } from "./route"; + +describe("Reverse Word Order API", () => { + it("should return 400 for invalid JSON", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: "not-json", + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it("should return 400 when text is missing", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({}), + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it("should return 400 when text is not a string", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: 42 }), + }); + const res = await POST(req); + expect(res.status).toBe(400); + }); + + it("reverses words in a simple sentence", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: "hello world" }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe("world hello"); + }); + + it("collapses multiple internal spaces to a single space", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: "hello world foo" }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe("foo world hello"); + }); + + it("trims leading and trailing whitespace", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: " hello world " }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe("world hello"); + }); + + it("handles a single word", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: "hello" }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe("hello"); + }); + + it("handles an empty string", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: "" }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe(""); + }); + + it("handles a string with only whitespace", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: " " }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe(""); + }); + + it("handles mixed tabs and spaces", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: "foo\t\tbar baz" }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe("baz bar foo"); + }); + + it("reverses a multi-word sentence correctly", async () => { + const req = new Request("http://localhost/api/routesF/reverse-word-order", { + method: "POST", + body: JSON.stringify({ text: "the quick brown fox" }), + }); + const res = await POST(req); + const data = await res.json(); + expect(data.result).toBe("fox brown quick the"); + }); +}); diff --git a/app/api/routesF/reverse-word-order/route.ts b/app/api/routesF/reverse-word-order/route.ts new file mode 100644 index 00000000..e851a847 --- /dev/null +++ b/app/api/routesF/reverse-word-order/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from "next/server"; + +/** + * Reverses the order of words in the given text. + * Collapses internal whitespace to single spaces and trims leading/trailing whitespace. + */ +function reverseWordOrder(text: string): string { + return text + .trim() + .split(/\s+/) + .reverse() + .join(" "); +} + +export async function POST(request: Request) { + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 }); + } + + if (typeof body !== "object" || body === null || Array.isArray(body)) { + return NextResponse.json({ error: "Request body must be an object." }, { status: 400 }); + } + + const { text } = body as Record; + + if (typeof text !== "string") { + return NextResponse.json({ error: "text must be a string." }, { status: 400 }); + } + + const result = reverseWordOrder(text); + + return NextResponse.json({ result }); +}