import {
MathematicalArgument,
MathematicalTerm,
MathematicalCondition,
MathematicalOperator
} from '../types.js';
/**
* Interface for a recognized mathematical entity
*/
interface MathEntity {
type: 'number' | 'variable' | 'operation' | 'sequence' | 'equation';
value: any;
context?: string;
}
/**
* Enhanced parser for mathematical expressions with improved NLP
*/
export class EnhancedMathematicalParser {
private variableMap: Map<string, string>; // Maps statements to variable names
private sequenceMap: Map<string, number[]>; // Maps sequence names to values
constructor() {
this.variableMap = new Map<string, string>();
this.sequenceMap = new Map<string, number[]>();
}
/**
* Parse input text into a mathematical argument with enhanced NLP
* @param input Input text
* @returns Parsed mathematical argument
*/
parse(input: string): MathematicalArgument {
// Reset state for new parse
this.variableMap = new Map<string, string>();
this.sequenceMap = new Map<string, number[]>();
try {
// Preprocess natural language
const preprocessed = this.preprocessNaturalLanguage(input);
// Parse the preprocessed input
return this.parsePreprocessed(preprocessed);
} catch (error) {
// Transform error to be more helpful
throw this.enhanceError(error, input);
}
}
/**
* Preprocess natural language into a more structured format
* @param input Raw input text
* @returns Preprocessed text
*/
private preprocessNaturalLanguage(input: string): string {
let result = input;
// Replace natural language math phrases
const replacements = [
// Simple sequence with question mark: "2, 4, 6, 8, ?"
{ regex: /^([\d\s,]+)[,\s]*\?$/gi,
replacement: "SEQUENCE: $1\nQUESTION: What is the next term?" },
// Sequence description patterns
{ regex: /\b(find|what is|determine) the next (?:number|term|value) (?:in|of)?(?: the sequence)?:?\s*([\d\s,]+)/gi,
replacement: "SEQUENCE: $2\nQUESTION: What is the next term?" },
// Find the pattern in a sequence
{ regex: /\b(find|what is|determine) the pattern (?:in|of)(?: the sequence)? ([\d\s,]+)/gi,
replacement: "SEQUENCE: $2\nQUESTION: What is the pattern?" },
// General sequence questions - "What comes after 2, 4, 6, 8?" or "What comes next: 2, 4, 6, 8"
{ regex: /\b(?:what|find) (?:is|comes) (?:next|after):?\s+([\d\s,]+)\??/gi,
replacement: "SEQUENCE: $1\nQUESTION: What is the next term?" },
// "What comes next in X" pattern
{ regex: /\b(?:what|find) (?:comes|is) (?:next|after) in\s+([\d\s,]+)\??/gi,
replacement: "SEQUENCE: $1\nQUESTION: What is the next term?" },
// Equation solving with "where" - "Find x where x + 5 = 10"
{ regex: /\b(solve|find|calculate|determine)(?: for)? ([a-z])(?:\s+where\s+|\s+when\s+|\s+such that\s+)(.+)/gi,
replacement: "$3\nQUESTION: Solve for $2" },
// Equation solving patterns
{ regex: /\b(solve|find|calculate|determine|what is)(?: the value of)? ([a-z])(?: is| equals| =| when)? ([\d\+\-\*\/\= ]+)/gi,
replacement: "$2 = $3\nQUESTION: Solve for $2" },
// Word problem patterns for addition
{ regex: /\b(\w+) has (\d+) (?:\w+) and (\w+) has (\d+) (?:\w+)\. how many (?:\w+) do they have (?:altogether|in total|combined)\??/gi,
replacement: "$1_amount = $2\n$3_amount = $4\ntotal = $1_amount + $3_amount\nQUESTION: What is total?" },
// Word problem patterns for subtraction
{ regex: /\b(\w+) has (\d+) (?:\w+) and (?:gives|loses|uses) (\d+)(?: of them)?\. how many (?:\w+) (?:does|do) (?:\w+) have (?:left|remaining)\??/gi,
replacement: "initial = $2\nused = $3\nremaining = initial - used\nQUESTION: What is remaining?" },
];
// Apply each pattern replacement
for (const { regex, replacement } of replacements) {
result = result.replace(regex, replacement);
}
// Normalize sequence notation
result = this.normalizeSequenceNotation(result);
return result;
}
/**
* Normalize different sequence notations to a standard format
* @param input Input text
* @returns Normalized text
*/
private normalizeSequenceNotation(input: string): string {
let result = input;
// Match "SEQUENCE: 1, 2, 3, 4" pattern and normalize
const sequencePattern = /SEQUENCE:\s*([\d\s,]+)/g;
result = result.replace(sequencePattern, (match, sequence) => {
// Extract numbers from the sequence
const numbers = sequence.split(/\s*,\s*/)
.map((s: string) => s.trim())
.filter((s: string) => /^\d+$/.test(s))
.map((s: string) => parseInt(s, 10));
// Create a sequence name
const sequenceName = `sequence_${Date.now()}`;
this.sequenceMap.set(sequenceName, numbers);
// Return the normalized format
return `SEQUENCE: ${numbers.join(', ')}`;
});
return result;
}
/**
* Parse preprocessed input into a mathematical argument
* @param input Preprocessed input
* @returns Parsed mathematical argument
*/
private parsePreprocessed(input: string): MathematicalArgument {
const lines = input.trim().split('\n');
const result: MathematicalArgument = {
terms: [],
constraints: [],
question: ''
};
// Check if this looks like a simple sequence without any preprocessing
if (lines.length === 1 && /^\d+[\s,]+\d+/.test(lines[0])) {
const sequence = this.extractSequence(lines[0]);
if (sequence.length >= 2) {
const sequenceName = `sequence_${Date.now()}`;
result.terms.push({
type: 'sequence',
name: sequenceName,
index: 0
});
result.constraints.push({
type: 'pattern',
sequence: sequence.map(v => ({ type: 'number', value: v } as MathematicalTerm)),
pattern: 'sequence'
});
result.question = "What is the next term in the sequence?";
return result;
}
}
for (const line of lines) {
const trimmedLine = line.trim();
// Handle sequence lines
if (trimmedLine.startsWith('SEQUENCE:')) {
const sequencePart = trimmedLine.substring('SEQUENCE:'.length).trim();
const sequence = this.extractSequence(sequencePart);
// Add sequence as a term and constraint
if (sequence.length > 0) {
const sequenceName = `sequence_${Date.now()}`;
// Add sequence term
result.terms.push({
type: 'sequence',
name: sequenceName,
index: 0
});
// Add sequence constraint
result.constraints.push({
type: 'pattern',
sequence: sequence.map(v => ({ type: 'number', value: v } as MathematicalTerm)),
pattern: 'sequence'
});
}
continue;
}
// Handle question lines
if (trimmedLine.startsWith('QUESTION:')) {
result.question = trimmedLine.substring('QUESTION:'.length).trim();
continue;
}
// Handle equation lines (containing =)
if (trimmedLine.includes('=')) {
const constraint = this.parseEquation(trimmedLine);
if (constraint) {
result.constraints.push(constraint);
}
continue;
}
// Handle simple arithmetic expressions
const term = this.parseExpression(trimmedLine);
if (term) {
result.terms.push(term);
}
// If it looks like a question, store it
if (trimmedLine.includes('?')) {
result.question = trimmedLine;
}
}
// If no question was found, add a default one
if (!result.question && result.constraints.length > 0) {
if (this.hasSequence(result)) {
result.question = "What is the next term in the sequence?";
} else if (this.hasEquation(result)) {
// Find the variable in the equation
const variable = this.findVariableInConstraints(result.constraints);
if (variable) {
result.question = `Solve for ${variable}.`;
} else {
result.question = "Solve the equation.";
}
}
}
return result;
}
/**
* Check if the argument contains a sequence
* @param arg Mathematical argument
* @returns Whether the argument contains a sequence
*/
private hasSequence(arg: MathematicalArgument): boolean {
for (const constraint of arg.constraints) {
if (constraint.type === 'pattern') {
return true;
}
}
return false;
}
/**
* Check if the argument contains an equation
* @param arg Mathematical argument
* @returns Whether the argument contains an equation
*/
private hasEquation(arg: MathematicalArgument): boolean {
for (const constraint of arg.constraints) {
if (constraint.type === 'equals') {
return true;
}
}
return false;
}
/**
* Find a variable in constraints
* @param constraints Array of constraints
* @returns Variable name if found, null otherwise
*/
private findVariableInConstraints(constraints: MathematicalCondition[]): string | null {
for (const constraint of constraints) {
if (constraint.type === 'equals') {
const variables = this.extractVariables([constraint.left, constraint.right]);
if (variables.length > 0) {
return variables[0];
}
}
}
return null;
}
/**
* Extract variables from terms
* @param terms Array of terms
* @returns Array of variable names
*/
private extractVariables(terms: MathematicalTerm[]): string[] {
const variables: string[] = [];
for (const term of terms) {
if (term.type === 'variable') {
variables.push(term.name);
} else if (term.type === 'operation') {
variables.push(...this.extractVariables(term.operands));
}
}
return variables;
}
/**
* Extract sequence of numbers from text
* @param input Input text
* @returns Array of numbers
*/
private extractSequence(input: string): number[] {
// Remove trailing ? or ... before extracting numbers
const cleanInput = input.replace(/[?\.]+$/, '').trim();
const matches = cleanInput.match(/\d+/g);
return matches ? matches.map(Number) : [];
}
/**
* Parse an equation from a string
* @param input Input text
* @returns Equation constraint or null
*/
private parseEquation(input: string): MathematicalCondition | null {
// Split on equals sign
const parts = input.split('=');
if (parts.length !== 2) {
return null;
}
const left = this.parseExpression(parts[0].trim());
const right = this.parseExpression(parts[1].trim());
if (!left || !right) {
return null;
}
return {
type: 'equals',
left,
right
};
}
/**
* Parse a mathematical expression
* @param input Input text
* @returns Mathematical term or null
*/
private parseExpression(input: string): MathematicalTerm | null {
const trimmed = input.trim();
if (!trimmed) {
return null;
}
// Check if it's a number
if (/^-?\d*\.?\d+$/.test(trimmed)) {
return {
type: 'number',
value: parseFloat(trimmed)
};
}
// Check if it's a variable
if (/^[a-zA-Z][a-zA-Z0-9_]*$/.test(trimmed)) {
return {
type: 'variable',
name: trimmed
};
}
// Parse operations recursively
// Try different operators in precedence order
const operators = ['\\+', '-', '\\*', '\\/'];
for (const op of operators) {
const regex = new RegExp(`(.+?)\\s*${op}\\s*(.+)`);
const match = trimmed.match(regex);
if (match) {
const left = this.parseExpression(match[1]);
const right = this.parseExpression(match[2]);
if (left && right) {
const operator = op.replace('\\', '') as MathematicalOperator;
return {
type: 'operation',
operator,
operands: [left, right]
};
}
}
}
// Check for parenthesized expressions
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
return this.parseExpression(trimmed.substring(1, trimmed.length - 1));
}
return null;
}
/**
* Enhance error messages to be more helpful
* @param error Original error
* @param input Original input
* @returns Enhanced error
*/
private enhanceError(error: any, input: string): Error {
const errorMessage = error instanceof Error ? error.message : String(error);
return new Error(`Error parsing mathematical expression: ${errorMessage}
Please use clear mathematical notation:
• For sequences: 1, 2, 3, 4, ...
• For equations: x + 5 = 10
• For variables: Use letters like x, y, z
• For operations: +, -, *, /
Natural language is also supported:
• "Find the next number in 1, 2, 3, 4"
• "What comes next in 2, 4, 8, 16?"
• "Solve for x: x + 5 = 10"
• "John has 5 apples and Mary has 7. How many do they have together?"
Your input: "${input}"`);
}
/**
* Get formatted variable mapping
* @returns String representation of variable mapping
*/
getVariableMappingString(): string {
let result = "Mathematical entities identified:\n";
// Variables
result += "\nVariables:\n";
for (const [statement, name] of this.variableMap.entries()) {
result += `• ${name}: ${statement}\n`;
}
// Sequences
result += "\nSequences:\n";
for (const [name, values] of this.sequenceMap.entries()) {
result += `• ${name}: ${values.join(', ')}\n`;
}
return result;
}
}