import type { Point2D } from "./types"; /** * Giới hạn giá trị trong khoảng [min, max]. */ export function clamp(val: number, min: number, max: number): number { return Math.min(Math.max(val, min), max); } /** * Kiểm tra hai số xấp xỉ bằng nhau với độ lệch eps. */ export function approxEqual(a: number, b: number, eps: number = 1e-4): boolean { return Math.abs(a - b) <= eps; } /** * Làm tròn số thực đến số chữ số thập phân chỉ định. */ export function roundTo(val: number, decimals: number = 2): number { const factor = Math.pow(10, decimals); return Math.round((val + Number.EPSILON) * factor) / factor; } /** * Thuật toán chia đôi (Bisection) tìm nghiệm x sao cho fn(x) = 0 trên [low, high]. */ export function solveRootBisection( fn: (x: number) => number, low: number, high: number, tol: number = 1e-6, maxIter: number = 100 ): number { let a = low; let b = high; let fa = fn(a); let fb = fn(b); if (Math.abs(fa) <= tol) return a; if (Math.abs(fb) <= tol) return b; if (fa * fb > 0) { // Nếu hai đầu cùng dấu, thực hiện quét 20 điểm con để tìm khoảng đổi dấu const steps = 20; const stepSize = (b - a) / steps; let foundSubInterval = false; for (let i = 0; i < steps; i++) { const subA = a + i * stepSize; const subB = subA + stepSize; const subFa = fn(subA); const subFb = fn(subB); if (Math.abs(subFa) <= tol) return subA; if (Math.abs(subFb) <= tol) return subB; if (subFa * subFb <= 0) { a = subA; b = subB; fa = subFa; fb = subFb; foundSubInterval = true; break; } } if (!foundSubInterval) { // Nếu vẫn không đổi dấu, trả về điểm có trị tuyệt đối fn nhỏ hơn return Math.abs(fa) < Math.abs(fb) ? a : b; } } for (let i = 0; i < maxIter; i++) { const mid = (a + b) / 2; const fmid = fn(mid); if (Math.abs(fmid) <= tol || (b - a) / 2 < tol) { return mid; } if (fa * fmid <= 0) { b = mid; fb = fmid; } else { a = mid; fa = fmid; } } return (a + b) / 2; } /** * Lấy mẫu tọa độ đường cong y = fn(x) để render trên đồ thị. */ export function sampleCurve( fn: (x: number) => number | null, xMin: number, xMax: number, steps: number = 120, yMaxClip: number = 1e4 ): Point2D[] { if (xMax <= xMin || steps <= 0) { return []; } const points: Point2D[] = []; const dx = (xMax - xMin) / steps; for (let i = 0; i <= steps; i++) { const x = xMin + i * dx; const y = fn(x); if (y !== null && Number.isFinite(y) && y >= 0 && y <= yMaxClip) { points.push({ x, y }); } } return points; }