import {
MathExpression,
MathNode
} from '../../types-advanced-math.js';
/**
* Expression evaluator for mathematical expressions
* Handles evaluation of parsed expressions with variable substitution
*/
export class ExpressionEvaluator {
/**
* Evaluates a parsed expression with given variable values
* @param expr The parsed expression
* @param variables Optional map of variable values
* @returns The evaluated result
*/
evaluateExpression(expr: MathExpression, variables?: Record<string, number>): number {
return this.evaluateNode(expr.parsed, variables);
}
/**
* Evaluates a single node in the expression tree
* @param node The node to evaluate
* @param variables Variable values
* @returns The evaluated result
*/
evaluateNode(node: MathNode, variables?: Record<string, number>): number {
switch (node.type) {
case 'number':
return node.value;
case 'variable':
if (!variables || variables[node.name] === undefined) {
throw new Error(`Variable ${node.name} not defined`);
}
return variables[node.name];
case 'operator':
return this.evaluateOperator(node, variables);
case 'function':
return this.evaluateFunction(node, variables);
}
}
/**
* Evaluates an operator node
* @param node The operator node
* @param variables Variable values
* @returns The evaluated result
*/
private evaluateOperator(node: { type: 'operator'; operator: string; operands: MathNode[] }, variables?: Record<string, number>): number {
const operands = node.operands.map(op => this.evaluateNode(op, variables));
switch (node.operator) {
case '+':
return operands.reduce((a, b) => a + b, 0);
case '-':
if (operands.length === 1) {
return -operands[0];
}
return operands.reduce((a, b) => a - b);
case '*':
return operands.reduce((a, b) => a * b, 1);
case '/':
if (operands.length !== 2) {
throw new Error('Division requires exactly 2 operands');
}
if (Math.abs(operands[1]) < 1e-10) {
throw new Error('Division by zero');
}
return operands[0] / operands[1];
case '^':
if (operands.length !== 2) {
throw new Error('Exponentiation requires exactly 2 operands');
}
return Math.pow(operands[0], operands[1]);
case '%':
if (operands.length !== 2) {
throw new Error('Modulo requires exactly 2 operands');
}
return operands[0] % operands[1];
// Comparison operators
case '<':
return operands[0] < operands[1] ? 1 : 0;
case '>':
return operands[0] > operands[1] ? 1 : 0;
case '<=':
return operands[0] <= operands[1] ? 1 : 0;
case '>=':
return operands[0] >= operands[1] ? 1 : 0;
case '==':
return Math.abs(operands[0] - operands[1]) < 1e-10 ? 1 : 0;
case '!=':
return Math.abs(operands[0] - operands[1]) >= 1e-10 ? 1 : 0;
default:
throw new Error(`Unknown operator: ${node.operator}`);
}
}
/**
* Evaluates a function node
* @param node The function node
* @param variables Variable values
* @returns The evaluated result
*/
private evaluateFunction(node: { type: 'function'; name: string; arguments: MathNode[] }, variables?: Record<string, number>): number {
const args = node.arguments.map(arg => this.evaluateNode(arg, variables));
switch (node.name) {
// Trigonometric functions
case 'sin':
this.validateArgCount(node.name, args, 1);
return Math.sin(args[0]);
case 'cos':
this.validateArgCount(node.name, args, 1);
return Math.cos(args[0]);
case 'tan':
this.validateArgCount(node.name, args, 1);
return Math.tan(args[0]);
case 'asin':
this.validateArgCount(node.name, args, 1);
this.validateRange(args[0], -1, 1, 'asin');
return Math.asin(args[0]);
case 'acos':
this.validateArgCount(node.name, args, 1);
this.validateRange(args[0], -1, 1, 'acos');
return Math.acos(args[0]);
case 'atan':
this.validateArgCount(node.name, args, 1);
return Math.atan(args[0]);
case 'atan2':
this.validateArgCount(node.name, args, 2);
return Math.atan2(args[0], args[1]);
// Logarithmic functions
case 'log':
this.validateArgCount(node.name, args, 1);
this.validatePositive(args[0], 'log');
return Math.log10(args[0]);
case 'ln':
this.validateArgCount(node.name, args, 1);
this.validatePositive(args[0], 'ln');
return Math.log(args[0]);
case 'log2':
this.validateArgCount(node.name, args, 1);
this.validatePositive(args[0], 'log2');
return Math.log2(args[0]);
// Root functions
case 'sqrt':
this.validateArgCount(node.name, args, 1);
this.validateNonNegative(args[0], 'sqrt');
return Math.sqrt(args[0]);
case 'cbrt':
this.validateArgCount(node.name, args, 1);
return Math.cbrt(args[0]);
// Other functions
case 'abs':
this.validateArgCount(node.name, args, 1);
return Math.abs(args[0]);
case 'exp':
this.validateArgCount(node.name, args, 1);
return Math.exp(args[0]);
case 'floor':
this.validateArgCount(node.name, args, 1);
return Math.floor(args[0]);
case 'ceil':
this.validateArgCount(node.name, args, 1);
return Math.ceil(args[0]);
case 'round':
this.validateArgCount(node.name, args, 1);
return Math.round(args[0]);
case 'sign':
this.validateArgCount(node.name, args, 1);
return Math.sign(args[0]);
// Min/Max functions
case 'min':
if (args.length === 0) {
throw new Error('min requires at least one argument');
}
return Math.min(...args);
case 'max':
if (args.length === 0) {
throw new Error('max requires at least one argument');
}
return Math.max(...args);
// Hyperbolic functions
case 'sinh':
this.validateArgCount(node.name, args, 1);
return Math.sinh(args[0]);
case 'cosh':
this.validateArgCount(node.name, args, 1);
return Math.cosh(args[0]);
case 'tanh':
this.validateArgCount(node.name, args, 1);
return Math.tanh(args[0]);
default:
throw new Error(`Unknown function: ${node.name}`);
}
}
/**
* Validates the number of arguments for a function
* @param funcName Function name
* @param args Arguments array
* @param expected Expected count
*/
private validateArgCount(funcName: string, args: number[], expected: number): void {
if (args.length !== expected) {
throw new Error(`${funcName} requires exactly ${expected} argument(s), got ${args.length}`);
}
}
/**
* Validates that a value is positive
* @param value The value to check
* @param funcName Function name for error message
*/
private validatePositive(value: number, funcName: string): void {
if (value <= 0) {
throw new Error(`${funcName} requires positive argument, got ${value}`);
}
}
/**
* Validates that a value is non-negative
* @param value The value to check
* @param funcName Function name for error message
*/
private validateNonNegative(value: number, funcName: string): void {
if (value < 0) {
throw new Error(`${funcName} requires non-negative argument, got ${value}`);
}
}
/**
* Validates that a value is within a range
* @param value The value to check
* @param min Minimum value
* @param max Maximum value
* @param funcName Function name for error message
*/
private validateRange(value: number, min: number, max: number, funcName: string): void {
if (value < min || value > max) {
throw new Error(`${funcName} requires argument in range [${min}, ${max}], got ${value}`);
}
}
/**
* Evaluates an expression and returns both the result and any errors
* @param expr The expression to evaluate
* @param variables Variable values
* @returns Result or error information
*/
safeEvaluate(expr: MathExpression, variables?: Record<string, number>): {
success: boolean;
result?: number;
error?: string;
} {
try {
const result = this.evaluateExpression(expr, variables);
return { success: true, result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Partially evaluates an expression, substituting known variables
* @param node The expression node
* @param variables Known variable values
* @returns Partially evaluated node
*/
partialEvaluate(node: MathNode, variables: Record<string, number>): MathNode {
switch (node.type) {
case 'number':
return node;
case 'variable':
if (variables[node.name] !== undefined) {
return { type: 'number', value: variables[node.name] };
}
return node;
case 'operator':
const evaluatedOperands = node.operands.map(op =>
this.partialEvaluate(op, variables)
);
// If all operands are numbers, evaluate the operation
if (evaluatedOperands.every(op => op.type === 'number')) {
try {
const result = this.evaluateNode({
...node,
operands: evaluatedOperands
}, {});
return { type: 'number', value: result };
} catch {
// If evaluation fails, return partially evaluated node
}
}
return { ...node, operands: evaluatedOperands };
case 'function':
const evaluatedArgs = node.arguments.map(arg =>
this.partialEvaluate(arg, variables)
);
// If all arguments are numbers, evaluate the function
if (evaluatedArgs.every(arg => arg.type === 'number')) {
try {
const result = this.evaluateNode({
...node,
arguments: evaluatedArgs
}, {});
return { type: 'number', value: result };
} catch {
// If evaluation fails, return partially evaluated node
}
}
return { ...node, arguments: evaluatedArgs };
}
}
}