import {
MathematicalArgument,
MathematicalTerm,
MathematicalCondition,
MathematicalOperator
} from '../types.js';
/**
* Parser for mathematical expressions and problems
*/
export class MathematicalParser {
/**
* Parse input text into a mathematical argument
* @param input Input text
* @returns Parsed mathematical argument
*/
parse(input: string): MathematicalArgument {
const lines = input.trim().split('\n');
const result: MathematicalArgument = {
terms: [],
constraints: [],
question: ''
};
for (const line of lines) {
const trimmedLine = line.trim();
// Check if it's a sequence first (before checking for question)
if (this.isSequence(trimmedLine)) {
const sequenceData = this.parseSequence(trimmedLine);
// Create a single sequence term that represents all the values
if (sequenceData.length > 0) {
const sequenceName = `sequence_${Date.now()}`;
// Extract values from the sequence
const sequenceValues = [];
// Number terms are at odd indices (1, 3, 5...)
for (let i = 1; i < sequenceData.length; i += 2) {
const term = sequenceData[i];
if (term.type === 'number') {
sequenceValues.push(term.value);
}
}
// Create a special term to represent the sequence values
if (sequenceValues.length > 0) {
result.terms.push({
type: 'sequence',
name: sequenceName,
index: 0 // Used to store values array
});
// Add the constraint with the actual sequence values
result.constraints.push({
type: 'pattern',
sequence: sequenceValues.map(v => ({type: 'number', value: v} as MathematicalTerm)),
pattern: 'sequence'
});
}
}
// If line contains a question mark, also store as question
if (trimmedLine.includes('?')) {
result.question = trimmedLine;
}
continue;
}
// Check if it's a question
if (trimmedLine.includes('?')) {
result.question = trimmedLine;
continue;
}
// Check if it's a constraint (contains =, <, >, etc.)
if (this.isConstraint(trimmedLine)) {
const constraint = this.parseConstraint(trimmedLine);
if (constraint) {
result.constraints.push(constraint);
}
continue;
}
// Try to parse as a general expression
const term = this.parseExpression(trimmedLine);
if (term) {
result.terms.push(term);
}
}
return result;
}
/**
* Check if a line contains a sequence
* @param line Input line
* @returns Boolean indicating if it's a sequence
*/
private isSequence(line: string): boolean {
// Look for patterns like: 1, 2, 3, ...
// or: 1, 2, 3, ?
// or: a_1 = 1, a_2 = 2, ...
const cleanLine = line.replace(/[?\.]+$/, '').trim(); // Remove trailing ? or ...
return /^\d+\s*,\s*\d+/.test(cleanLine) || /([a-zA-Z]_?\d+\s*=\s*\d+)/.test(cleanLine);
}
/**
* Check if a line contains a constraint
* @param line Input line
* @returns Boolean indicating if it's a constraint
*/
private isConstraint(line: string): boolean {
// Look for comparison operators
return /[<>=≤≥]/.test(line) || line.includes('equals') || line.includes('is');
}
/**
* Parse a sequence from input text
* @param input Input text containing sequence
* @returns Array of sequence terms
*/
private parseSequence(input: string): MathematicalTerm[] {
const terms: MathematicalTerm[] = [];
// Handle simple sequence: 1, 2, 3, ...
if (/^\d+\s*,\s*\d+/.test(input)) {
const sequence = this.extractSequence(input);
for (let i = 0; i < sequence.length; i++) {
terms.push({
type: 'sequence',
name: `element_${i}`,
index: i
});
// Also add the number term
terms.push({
type: 'number',
value: sequence[i]
});
}
}
// Handle named sequence: a_n = n + 1
else if (/[a-zA-Z]_?[a-zA-Z0-9]?\s*=/.test(input)) {
const match = input.match(/([a-zA-Z]_?[a-zA-Z0-9]?)\s*=\s*(.+)/);
if (match) {
const name = match[1];
const expression = match[2];
// Parse the expression to create a sequence term
const pattern = this.parseExpression(expression);
if (pattern) {
terms.push({
type: 'sequence',
name: name,
index: 0 // Will be updated based on context
});
}
}
}
return terms;
}
/**
* 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 a constraint from input text
* @param input Input text containing constraint
* @returns Parsed constraint or null
*/
private parseConstraint(input: string): MathematicalCondition | null {
// Handle equals constraint
if (input.includes('=')) {
const parts = input.split('=');
if (parts.length === 2) {
const left = this.parseExpression(parts[0]);
const right = this.parseExpression(parts[1]);
if (left && right) {
return {
type: 'equals',
left: left,
right: right
};
}
}
}
// Handle comparison constraints
const comparisons = ['<=', '>=', '<', '>'];
for (const op of comparisons) {
if (input.includes(op)) {
const parts = input.split(op);
if (parts.length === 2) {
const left = this.parseExpression(parts[0]);
const right = this.parseExpression(parts[1]);
if (left && right) {
return {
type: 'compare',
operator: op as '<' | '>' | '<=' | '>=',
left: left,
right: right
};
}
}
}
}
return null;
}
/**
* Parse a mathematical expression
* @param input Input text
* @returns Parsed mathematical term or null
*/
private parseExpression(input: string): MathematicalTerm | null {
const trimmed = input.trim();
// 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
};
}
// Check if it's an operation
for (const operator of this.getOperators()) {
if (trimmed.includes(operator)) {
const operands = this.parseOperation(trimmed, operator);
if (operands && operands.length >= 2) {
return {
type: 'operation',
operator: operator as MathematicalOperator,
operands: operands
};
}
}
}
// Check if it's a sequence reference
if (/[a-zA-Z]_?\d*/.test(trimmed)) {
const match = trimmed.match(/([a-zA-Z]+)_?(\d*)/);
if (match) {
return {
type: 'sequence',
name: match[1],
index: match[2] ? parseInt(match[2]) : 0
};
}
}
return null;
}
/**
* Parse an operation with given operator
* @param input Input text
* @param operator Operator to parse
* @returns Array of operands or null
*/
private parseOperation(input: string, operator: string): MathematicalTerm[] | null {
const parts = input.split(operator);
const operands: MathematicalTerm[] = [];
for (const part of parts) {
const operand = this.parseExpression(part.trim());
if (operand) {
operands.push(operand);
}
}
return operands.length > 0 ? operands : null;
}
/**
* Get list of supported operators
* @returns Array of operator strings
*/
private getOperators(): MathematicalOperator[] {
return ['+', '-', '*', '/', '%'];
}
/**
* Tokenize input string
* @param input Input text
* @returns Array of tokens
*/
private tokenize(input: string): string[] {
// Simple tokenization by splitting on spaces and operators
const operators = this.getOperators();
let result = [input];
for (const operator of operators) {
const newResult: string[] = [];
for (const token of result) {
if (token.includes(operator)) {
const parts = token.split(operator);
newResult.push(...parts.filter(p => p.length > 0));
newResult.push(operator);
} else {
newResult.push(token);
}
}
result = newResult.filter(t => t.length > 0);
}
return result;
}
/**
* Parse a number from token
* @param token Token to parse
* @returns Number value or NaN
*/
private parseNumber(token: string): number {
return parseFloat(token.trim());
}
}