import {
MathNode,
NumberNode,
VariableNode,
OperatorNode,
FunctionNode,
MathExpression
} from '../../types-advanced-math.js';
/**
* Expression parser for advanced mathematical expressions
* Handles tokenization and parsing of mathematical expressions
*/
export class ExpressionParser {
/**
* Parses a mathematical expression into a structured format
* @param expression The expression string
* @returns Parsed expression
*/
parseExpression(expression: string): MathExpression {
// Tokenize the expression
const tokens = this.tokenizeExpression(expression);
// Parse the tokens into an expression tree
const parsed = this.parseTokens([...tokens]); // Clone to avoid mutation
// Extract variables
const variables = this.extractVariablesFromNode(parsed);
return {
original: expression,
parsed,
variables
};
}
/**
* Tokenizes an expression into an array of tokens
* @param expression The expression string
* @returns Array of tokens
*/
private tokenizeExpression(expression: string): string[] {
// Enhanced tokenizer with support for multi-character operators and functions
const regex = /(<=|>=|!=|==|[\+\-\*\/\^\(\),<>]|\d+(\.\d+)?|[a-zA-Z]+)/g;
return expression.match(regex) || [];
}
/**
* Parses tokens into an expression tree using recursive descent
* @param tokens Array of tokens
* @returns Root node of the expression tree
*/
private parseTokens(tokens: string[]): MathNode {
// Recursive descent parser with proper precedence
const parseExpression = (): MathNode => {
return parseComparison();
};
const parseComparison = (): MathNode => {
let left = parseAddition();
while (tokens.length > 0 && this.isComparisonOperator(tokens[0])) {
const operator = tokens.shift() as any;
const right = parseAddition();
left = {
type: 'operator',
operator,
operands: [left, right]
};
}
return left;
};
const parseAddition = (): MathNode => {
let left = parseMultiplication();
while (tokens.length > 0 && (tokens[0] === '+' || tokens[0] === '-')) {
const operator = tokens.shift() as '+' | '-';
const right = parseMultiplication();
left = {
type: 'operator',
operator,
operands: [left, right]
};
}
return left;
};
const parseMultiplication = (): MathNode => {
let left = parsePower();
while (tokens.length > 0 && (tokens[0] === '*' || tokens[0] === '/' || tokens[0] === '%')) {
const operator = tokens.shift() as '*' | '/' | '%';
const right = parsePower();
left = {
type: 'operator',
operator,
operands: [left, right]
};
}
return left;
};
const parsePower = (): MathNode => {
let left = parsePrimary();
while (tokens.length > 0 && tokens[0] === '^') {
tokens.shift(); // consume '^'
const right = parsePrimary();
left = {
type: 'operator',
operator: '^',
operands: [left, right]
};
}
return left;
};
const parsePrimary = (): MathNode => {
if (tokens.length === 0) {
throw new Error('Unexpected end of expression');
}
const token = tokens[0];
// Handle numbers
if (/^\d+(\.\d+)?$/.test(token)) {
tokens.shift();
return { type: 'number', value: parseFloat(token) };
}
// Handle variables and functions
if (/^[a-zA-Z]+$/.test(token)) {
tokens.shift();
// Check if this is a function
const nextToken = tokens.length > 0 ? tokens[0] : null;
if (nextToken === '(') {
tokens.shift(); // Consume '('
const args: MathNode[] = [];
// Parse arguments
let currentToken = tokens.length > 0 ? tokens[0] : null;
if (currentToken && currentToken !== ')') {
args.push(parseExpression());
currentToken = tokens.length > 0 ? tokens[0] : null;
while (currentToken === ',') {
tokens.shift(); // Consume ','
args.push(parseExpression());
currentToken = tokens.length > 0 ? tokens[0] : null;
}
}
const closingParen = tokens.shift();
if (closingParen !== ')') {
throw new Error('Expected closing parenthesis');
}
return {
type: 'function',
name: token as any,
arguments: args
};
}
return { type: 'variable', name: token };
}
// Handle parentheses
if (token === '(') {
tokens.shift();
const node = parseExpression();
const closingParen = tokens.shift();
if (closingParen !== ')') {
throw new Error('Expected closing parenthesis');
}
return node;
}
// Handle unary operators
if (token === '+' || token === '-') {
tokens.shift();
const operand = parsePrimary();
if (token === '-') {
// Unary minus
if (operand.type === 'number') {
return { type: 'number', value: -operand.value };
}
return {
type: 'operator',
operator: '*',
operands: [{ type: 'number', value: -1 }, operand]
};
}
// Unary plus does nothing
return operand;
}
throw new Error(`Unexpected token: ${token}`);
};
// Start parsing
const result = parseExpression();
if (tokens.length > 0) {
throw new Error(`Unexpected tokens at end of expression: ${tokens.join(' ')}`);
}
return result;
}
/**
* Checks if a token is a comparison operator
* @param token The token to check
* @returns True if it's a comparison operator
*/
private isComparisonOperator(token: string): boolean {
return ['<', '>', '<=', '>=', '==', '!='].includes(token);
}
/**
* Extracts variables from an expression node
* @param node The root node of the expression
* @returns Array of variable names
*/
extractVariablesFromNode(node: MathNode): string[] {
const variables = new Set<string>();
const visit = (node: MathNode): void => {
if (node.type === 'variable') {
variables.add(node.name);
} else if (node.type === 'operator') {
node.operands.forEach(visit);
} else if (node.type === 'function') {
node.arguments.forEach(visit);
}
};
visit(node);
return Array.from(variables);
}
/**
* Converts a node to a string representation
* @param node The node to convert
* @returns String representation
*/
nodeToString(node: MathNode): string {
switch (node.type) {
case 'number':
return this.formatNumber(node.value);
case 'variable':
return node.name;
case 'operator':
const operands = node.operands.map(op => {
const str = this.nodeToString(op);
// Add parentheses if needed based on precedence
if (op.type === 'operator' && this.needsParentheses(op.operator, node.operator)) {
return `(${str})`;
}
return str;
});
switch (node.operator) {
case '+':
return operands.join(' + ');
case '-':
if (operands.length === 1) {
return `-${operands[0]}`;
}
return `${operands[0]} - ${operands[1]}`;
case '*':
return operands.join(' * ');
case '/':
return `${operands[0]} / ${operands[1]}`;
case '^':
return `${operands[0]}^${operands[1]}`;
case '%':
return `${operands[0]} % ${operands[1]}`;
default:
return `${operands[0]} ${node.operator} ${operands[1]}`;
}
case 'function':
const args = node.arguments.map(arg => this.nodeToString(arg)).join(', ');
return `${node.name}(${args})`;
}
}
/**
* Determines if parentheses are needed based on operator precedence
* @param innerOp Inner operator
* @param outerOp Outer operator
* @returns True if parentheses are needed
*/
private needsParentheses(innerOp: string, outerOp: string): boolean {
const precedence: Record<string, number> = {
'+': 1,
'-': 1,
'*': 2,
'/': 2,
'%': 2,
'^': 3,
'<': 0,
'>': 0,
'<=': 0,
'>=': 0,
'==': 0,
'!=': 0
};
return (precedence[innerOp] || 0) < (precedence[outerOp] || 0);
}
/**
* Formats a number for display
* @param num The number to format
* @returns Formatted string
*/
private formatNumber(num: number): string {
// Fix floating point errors
const rounded = Math.round(num * 1e10) / 1e10;
return rounded === Math.round(rounded) ? rounded.toString() : rounded.toFixed(6).replace(/\.?0+$/, '');
}
}