158 lines
4.1 KiB
TypeScript
158 lines
4.1 KiB
TypeScript
|
|
import { create, all, type EvalFunction } from 'mathjs';
|
||
|
|
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
const math = create(all, { number: 'number', precision: 14 } as any);
|
||
|
|
|
||
|
|
function buildScope(variables: Record<string, string>): Record<string, unknown> {
|
||
|
|
const scope: Record<string, unknown> = {};
|
||
|
|
for (const [name, expr] of Object.entries(variables)) {
|
||
|
|
if (!expr.trim()) continue;
|
||
|
|
try {
|
||
|
|
scope[name] = math.evaluate(expr);
|
||
|
|
} catch {
|
||
|
|
// skip invalid variables
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return scope;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface EvalResult {
|
||
|
|
result: string;
|
||
|
|
error: boolean;
|
||
|
|
assignedName?: string;
|
||
|
|
assignedValue?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function evaluateExpression(
|
||
|
|
expression: string,
|
||
|
|
variables: Record<string, string> = {}
|
||
|
|
): EvalResult {
|
||
|
|
const trimmed = expression.trim();
|
||
|
|
if (!trimmed) return { result: '', error: false };
|
||
|
|
|
||
|
|
try {
|
||
|
|
const scope = buildScope(variables);
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
const raw = math.evaluate(trimmed, scope as any);
|
||
|
|
const formatted = formatValue(raw);
|
||
|
|
|
||
|
|
// Detect assignment: "name = expr" or "name(args) = expr"
|
||
|
|
const assignMatch = trimmed.match(/^([a-zA-Z_]\w*)\s*(?:\([^)]*\))?\s*=/);
|
||
|
|
if (assignMatch) {
|
||
|
|
return {
|
||
|
|
result: formatted,
|
||
|
|
error: false,
|
||
|
|
assignedName: assignMatch[1],
|
||
|
|
assignedValue: formatted,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return { result: formatted, error: false };
|
||
|
|
} catch (err) {
|
||
|
|
const msg = err instanceof Error ? err.message : String(err);
|
||
|
|
return { result: msg.replace(/^Error: /, ''), error: true };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatValue(value: unknown): string {
|
||
|
|
if (value === null || value === undefined) return 'null';
|
||
|
|
if (typeof value === 'boolean') return String(value);
|
||
|
|
if (typeof value === 'number') {
|
||
|
|
if (!isFinite(value)) return value > 0 ? 'Infinity' : '-Infinity';
|
||
|
|
if (value === 0) return '0';
|
||
|
|
const abs = Math.abs(value);
|
||
|
|
if (abs >= 1e13 || (abs < 1e-7 && abs > 0)) {
|
||
|
|
return value.toExponential(6).replace(/\.?0+(e)/, '$1');
|
||
|
|
}
|
||
|
|
return parseFloat(value.toPrecision(12)).toString();
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return math.format(value as never, { precision: 10 });
|
||
|
|
} catch {
|
||
|
|
return String(value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Compilation cache for fast repeated graph evaluation
|
||
|
|
const compileCache = new Map<string, EvalFunction>();
|
||
|
|
|
||
|
|
function getCompiled(expr: string): EvalFunction | null {
|
||
|
|
if (!compileCache.has(expr)) {
|
||
|
|
try {
|
||
|
|
compileCache.set(expr, math.compile(expr) as EvalFunction);
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (compileCache.size > 200) {
|
||
|
|
compileCache.delete(compileCache.keys().next().value!);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return compileCache.get(expr) ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function evaluateAt(
|
||
|
|
expression: string,
|
||
|
|
x: number,
|
||
|
|
variables: Record<string, string>
|
||
|
|
): number {
|
||
|
|
const compiled = getCompiled(expression);
|
||
|
|
if (!compiled) return NaN;
|
||
|
|
try {
|
||
|
|
const scope = buildScope(variables);
|
||
|
|
const result = compiled.evaluate({ ...scope, x });
|
||
|
|
return typeof result === 'number' && isFinite(result) ? result : NaN;
|
||
|
|
} catch {
|
||
|
|
return NaN;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface GraphPoint {
|
||
|
|
x: number;
|
||
|
|
y: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function sampleFunction(
|
||
|
|
expression: string,
|
||
|
|
xMin: number,
|
||
|
|
xMax: number,
|
||
|
|
numPoints: number,
|
||
|
|
variables: Record<string, string> = {}
|
||
|
|
): Array<GraphPoint | null> {
|
||
|
|
if (!expression.trim()) return [];
|
||
|
|
|
||
|
|
const compiled = getCompiled(expression);
|
||
|
|
if (!compiled) return [];
|
||
|
|
|
||
|
|
const scope = buildScope(variables);
|
||
|
|
const points: Array<GraphPoint | null> = [];
|
||
|
|
const step = (xMax - xMin) / numPoints;
|
||
|
|
let prevY: number | null = null;
|
||
|
|
const jumpThreshold = Math.abs(xMax - xMin) * 4;
|
||
|
|
|
||
|
|
for (let i = 0; i <= numPoints; i++) {
|
||
|
|
const x = xMin + i * step;
|
||
|
|
let y: number;
|
||
|
|
try {
|
||
|
|
const r = compiled.evaluate({ ...scope, x });
|
||
|
|
y = typeof r === 'number' ? r : NaN;
|
||
|
|
} catch {
|
||
|
|
y = NaN;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!isFinite(y) || isNaN(y)) {
|
||
|
|
if (points.length > 0 && points[points.length - 1] !== null) {
|
||
|
|
points.push(null);
|
||
|
|
}
|
||
|
|
prevY = null;
|
||
|
|
} else {
|
||
|
|
if (prevY !== null && Math.abs(y - prevY) > jumpThreshold) {
|
||
|
|
points.push(null);
|
||
|
|
}
|
||
|
|
points.push({ x, y });
|
||
|
|
prevY = y;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return points;
|
||
|
|
}
|