Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions app/api/routes-f/iqr-outlier/route.test.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
54 changes: 54 additions & 0 deletions app/api/routes-f/iqr-outlier/route.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

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 });
}
112 changes: 112 additions & 0 deletions app/api/routes-f/polynomial-eval/route.test.ts
Original file line number Diff line number Diff line change
@@ -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]);
});
});
50 changes: 50 additions & 0 deletions app/api/routes-f/polynomial-eval/route.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

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 });
}
Loading
Loading