diff --git a/CLAUDE.md b/CLAUDE.md index 7bd5c6f..d8e8518 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,6 +88,12 @@ npm run test:performance # Performance benchmarks const parsec = new Parsec() await parsec.initialize() +// JavaScript-native evaluation (recommended) +const result = parsec.eval('2 + 3 * 4') // → 14 (number) + +// Raw C++ JSON evaluation (for platform consistency) +const rawResult = parsec.evalRaw('2 + 3 * 4') // → '{"val": "14", "type": "i"}' + // Batch evaluation const results = parsec.evaluateBatch(['2+2', 'sqrt(16)', 'sin(pi/2)']) @@ -307,11 +313,16 @@ import Parsec from './js/equations_parser_wrapper.js' const parsec = new Parsec() await parsec.initialize() -// Evaluate equations +// Evaluate equations (JavaScript-native values) const result = parsec.eval('2 + 3 * 4') // Returns: 14 const trig = parsec.eval('sin(pi/2)') // Returns: 1 const complex = parsec.eval('real(3+4i)') // Returns: 3 const string = parsec.eval('concat("a","b")') // Returns: "ab" + +// Raw C++ JSON evaluation (for platform consistency) +const rawResult = parsec.evalRaw('2 + 3 * 4') // Returns: '{"val": "14", "type": "i"}' +const rawTrig = parsec.evalRaw('sin(pi/2)') // Returns: '{"val": "1", "type": "f"}' +const rawString = parsec.evalRaw('concat("a","b")') // Returns: '{"val": "ab", "type": "s"}' ``` ### Test Structure Pattern diff --git a/README.md b/README.md index 5b4ef59..a26a92f 100644 --- a/README.md +++ b/README.md @@ -126,10 +126,17 @@ npm install **Direct Value Returns:** ```javascript +// JavaScript-native values (recommended) parsec.eval('2 + 3') // → 5 (number) parsec.eval('sin(pi/2)') // → 1.0 (number) parsec.eval('5 > 3') // → true (boolean) parsec.eval('concat("a","b")') // → "ab" (string) + +// Raw C++ JSON strings (for platform consistency) +parsec.evalRaw('2 + 3') // → '{"val": "5", "type": "i"}' +parsec.evalRaw('sin(pi/2)') // → '{"val": "1", "type": "f"}' +parsec.evalRaw('5 > 3') // → '{"val": "true", "type": "b"}' +parsec.evalRaw('concat("a","b")') // → '{"val": "ab", "type": "s"}' ``` ## 🧪 Comprehensive Testing @@ -475,7 +482,7 @@ npm run style:fix #### `parsec.eval(equation)` -Evaluate a single mathematical expression. +Evaluate a single mathematical expression and return JavaScript-native values. ```javascript const result = parsec.eval('2 + 3 * 4') @@ -488,6 +495,26 @@ const boolean = parsec.eval('5 > 3') // Returns: true ``` +#### `parsec.evalRaw(equation)` + +Evaluate a single mathematical expression and return raw C++ JSON strings for platform consistency. + +```javascript +const result = parsec.evalRaw('2 + 3 * 4') +// Returns: '{"val": "14", "type": "i"}' + +const text = parsec.evalRaw('concat("Hello", " World")') +// Returns: '{"val": "Hello World", "type": "s"}' + +const boolean = parsec.evalRaw('5 > 3') +// Returns: '{"val": "true", "type": "b"}' + +// Parse the JSON to access individual components +const parsed = JSON.parse(result) +console.log(parsed.val) // "14" +console.log(parsed.type) // "i" (integer) +``` + #### `parsec.evaluateBatch(equations)` Evaluate multiple expressions in one call. diff --git a/js/equations_parser_wrapper.js b/js/equations_parser_wrapper.js index ccd3ac3..aca46dd 100644 --- a/js/equations_parser_wrapper.js +++ b/js/equations_parser_wrapper.js @@ -139,6 +139,69 @@ class Parsec { } } + /** + * Evaluate a mathematical equation and return raw C++ JSON result (for platform consistency) + * @param {string} equation - The mathematical expression to evaluate + * @returns {string} The evaluated result as raw JSON string from C++ equations-parser + * @throws {Error} If the equation is invalid or evaluation fails + * + * This function provides platform-consistent output matching native implementations, + * returning the exact JSON format that C++ equations-parser produces. + * + * @example + * // Basic arithmetic + * evalRaw("2 + 3 * 4") // → '{"val": "14", "type": "i"}' + * + * @example + * // Trigonometric functions + * evalRaw("sin(pi/2)") // → '{"val": "1", "type": "f"}' + * + * @example + * // String functions + * evalRaw('concat("Hello", " World")') // → '{"val": "Hello World", "type": "s"}' + * + * @example + * // Boolean results + * evalRaw("5 > 3") // → '{"val": "true", "type": "b"}' + * + * @example + * // Special float values + * evalRaw("1/0") // → '{"val": "inf", "type": "f"}' + * evalRaw("sqrt(-1)") // → '{"val": "nan", "type": "f"}' + * + * @example + * // Error case (throws exception) + * evalRaw("invalid syntax") // throws Error: "Parse error message" + */ + evalRaw(equation) { + this._ensureModuleReady() + + try { + this._validateEquationInput(equation) + + console.log(`🧮 JS RAW: Evaluating equation: "${equation}"`) + + const jsonResult = this.module.eval_equation(equation) + console.log(`🧮 JS RAW: Raw result from C++: ${jsonResult}`) + + // Parse JSON only to check for errors and throw them as exceptions + const parsedResult = JSON.parse(jsonResult) + + if (parsedResult.error) { + console.log(`❌ JS RAW: Equation evaluation error: ${parsedResult.error}`) + throw new Error(parsedResult.error) + } + + console.log(`✅ JS RAW: Returning raw JSON result: ${jsonResult}`) + + // Return the raw JSON string from C++ for platform consistency + return jsonResult + } catch (error) { + console.error('❌ Error in evalRaw:', error.message || error) + throw error + } + } + /** * Get information about supported equation types and functions * @returns {Object} Information about supported functions and operators @@ -305,6 +368,60 @@ class Parsec { return results } + /** + * Run comprehensive tests of both eval() and evalRaw() functions + * @returns {Object} Test results with success/failure information for both functions + */ + runComprehensiveTestsBoth() { + this._ensureModuleReady() + + console.log('🧪 Running comprehensive tests for both eval() and evalRaw()...') + + const evalResults = this._createTestResultsContainer() + const evalRawResults = this._createTestResultsContainer() + const testCases = this._getTestCases() + + // Test eval() function + console.log('🧪 Testing eval() function...') + for (const testCase of testCases) { + this._runSingleTest(testCase, evalResults) + } + + // Test evalRaw() function + console.log('🧪 Testing evalRaw() function...') + for (const testCase of testCases) { + this._runSingleTestRaw(testCase, evalRawResults) + } + + const totalResults = { + eval: { + passed: evalResults.passed, + failed: evalResults.failed, + tests: evalResults.tests, + errors: evalResults.errors + }, + evalRaw: { + passed: evalRawResults.passed, + failed: evalRawResults.failed, + tests: evalRawResults.tests, + errors: evalRawResults.errors + }, + summary: { + totalPassed: evalResults.passed + evalRawResults.passed, + totalFailed: evalResults.failed + evalRawResults.failed, + evalSuccess: evalResults.failed === 0, + evalRawSuccess: evalRawResults.failed === 0, + bothSuccess: evalResults.failed === 0 && evalRawResults.failed === 0 + } + } + + console.log(`🧪 eval() results: ${evalResults.passed} passed, ${evalResults.failed} failed`) + console.log(`🧪 evalRaw() results: ${evalRawResults.passed} passed, ${evalRawResults.failed} failed`) + console.log(`🧪 Overall: ${totalResults.summary.totalPassed} passed, ${totalResults.summary.totalFailed} failed`) + + return totalResults + } + _createTestResultsContainer() { return { passed: 0, @@ -363,6 +480,21 @@ class Parsec { } } + _runSingleTestRaw(testCase, results) { + try { + const jsonResult = this.evalRaw(testCase.equation) + const parsedResult = JSON.parse(jsonResult) + + // For evalRaw, we expect the parsed 'val' field to match the expected value + const testResult = this._createTestResultRaw(testCase, jsonResult, parsedResult) + + this._evaluateTestResultRaw(testResult, testCase, parsedResult) + this._recordTestResult(testResult, results) + } catch (error) { + this._handleTestError(testCase, error, results) + } + } + _createTestResult(testCase, result) { return { equation: testCase.equation, @@ -373,6 +505,18 @@ class Parsec { } } + _createTestResultRaw(testCase, jsonResult, parsedResult) { + return { + equation: testCase.equation, + description: `${testCase.description} (RAW)`, + expected: testCase.expected, + actual: parsedResult.val, + actualJson: jsonResult, + actualType: parsedResult.type, + passed: false, + } + } + _evaluateTestResult(testResult, testCase, result) { // With the new API, result is the direct value, not an object const actualValue = result.toString() @@ -383,6 +527,27 @@ class Parsec { : this._compareValues(actualValue, expectedValue) } + _evaluateTestResultRaw(testResult, testCase, parsedResult) { + // For evalRaw, we compare the 'val' field from the parsed JSON + const actualValue = parsedResult.val.toString() + const expectedValue = testCase.expected.toString() + + // Also verify the JSON structure is correct + const hasValidStructure = + parsedResult.hasOwnProperty('val') && + parsedResult.hasOwnProperty('type') && + !parsedResult.hasOwnProperty('error') + + if (!hasValidStructure) { + testResult.passed = false + return + } + + testResult.passed = testCase.allowBooleanString + ? this._compareBooleanValues(actualValue, expectedValue) + : this._compareValues(actualValue, expectedValue) + } + _compareBooleanValues(actualValue, expectedValue) { return ( actualValue.toLowerCase() === expectedValue.toLowerCase() || diff --git a/tests/evalraw.test.js b/tests/evalraw.test.js new file mode 100644 index 0000000..2e8e1b1 --- /dev/null +++ b/tests/evalraw.test.js @@ -0,0 +1,345 @@ +/** + * EvalRaw Function Tests + * + * Tests the evalRaw() function which returns raw C++ JSON strings + * for platform consistency with native implementations. + */ + +import { describe, it, expect, beforeAll } from 'vitest' +import { createTestEvaluator } from './setup.js' + +describe('EvalRaw Function', () => { + let parsec + + beforeAll(async () => { + parsec = await createTestEvaluator() + }) + + describe('Basic Arithmetic - Raw JSON Output', () => { + it('should return JSON for addition operations', () => { + const result = parsec.evalRaw('2 + 3') + expect(typeof result).toBe('string') + + const parsed = JSON.parse(result) + expect(parsed).toHaveProperty('val') + expect(parsed).toHaveProperty('type') + expect(parsed.val).toBe('5') + expect(parsed.type).toBe('i') + expect(parsed).not.toHaveProperty('error') + }) + + it('should return JSON for subtraction operations', () => { + const result = parsec.evalRaw('10 - 4') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('6') + expect(parsed.type).toBe('i') + }) + + it('should return JSON for multiplication operations', () => { + const result = parsec.evalRaw('3 * 4') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('12') + expect(parsed.type).toBe('i') + }) + + it('should return JSON for division operations', () => { + const result = parsec.evalRaw('8 / 2') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('4') + expect(parsed.type).toBe('i') + }) + + it('should return JSON for floating point division', () => { + const result = parsec.evalRaw('7 / 2') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('3.5') + expect(parsed.type).toBe('f') + }) + }) + + describe('Special Float Values - Raw JSON Output', () => { + it('should return JSON for positive infinity', () => { + const result = parsec.evalRaw('5 / 0') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('inf') + expect(parsed.type).toBe('f') + }) + + it('should return JSON for negative infinity', () => { + const result = parsec.evalRaw('-5 / 0') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('-inf') + expect(parsed.type).toBe('f') + }) + + it('should return JSON for NaN (0/0)', () => { + const result = parsec.evalRaw('0 / 0') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('-nan') // C++ actually returns -nan + expect(parsed.type).toBe('f') + }) + + it('should return JSON for NaN (sqrt of negative)', () => { + const result = parsec.evalRaw('sqrt(-1)') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('-nan') // C++ actually returns -nan + expect(parsed.type).toBe('f') + }) + }) + + describe('Mathematical Functions - Raw JSON Output', () => { + it('should return JSON for trigonometric functions', () => { + const result = parsec.evalRaw('sin(0)') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('0') + expect(parsed.type).toBe('i') // C++ returns integer for exact zero + }) + + it('should return JSON for cosine function', () => { + const result = parsec.evalRaw('cos(0)') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('1') + expect(parsed.type).toBe('i') // C++ returns integer for exact one + }) + + it('should return JSON for square root', () => { + const result = parsec.evalRaw('sqrt(16)') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('4') + expect(parsed.type).toBe('i') // C++ returns integer for perfect squares + }) + + it('should return JSON for absolute value', () => { + const result = parsec.evalRaw('abs(-5)') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('5') + expect(parsed.type).toBe('i') + }) + }) + + describe('Boolean Operations - Raw JSON Output', () => { + it('should return JSON for greater than comparison', () => { + const result = parsec.evalRaw('5 > 3') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('true') + expect(parsed.type).toBe('b') + }) + + it('should return JSON for less than comparison', () => { + const result = parsec.evalRaw('2 < 1') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('false') + expect(parsed.type).toBe('b') + }) + + it('should return JSON for equality comparison', () => { + const result = parsec.evalRaw('3 == 3') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('true') + expect(parsed.type).toBe('b') + }) + + it('should return JSON for inequality comparison', () => { + const result = parsec.evalRaw('3 != 4') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('true') + expect(parsed.type).toBe('b') + }) + }) + + describe('String Functions - Raw JSON Output', () => { + it('should return JSON for string concatenation', () => { + const result = parsec.evalRaw('concat("Hello", " World")') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('Hello World') + expect(parsed.type).toBe('s') + }) + + it('should return JSON for string length', () => { + const result = parsec.evalRaw('length("test")') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('4') + expect(parsed.type).toBe('i') + }) + + it('should return JSON for string uppercase conversion', () => { + const result = parsec.evalRaw('toupper("hello")') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('HELLO') + expect(parsed.type).toBe('s') + }) + + it('should return JSON for string lowercase conversion', () => { + const result = parsec.evalRaw('tolower("WORLD")') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('world') + expect(parsed.type).toBe('s') + }) + }) + + describe('Complex Expressions - Raw JSON Output', () => { + it('should return JSON for order of operations', () => { + const result = parsec.evalRaw('2 + 3 * 4') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('14') + expect(parsed.type).toBe('i') + }) + + it('should return JSON for parentheses precedence', () => { + const result = parsec.evalRaw('(2 + 3) * 4') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('20') + expect(parsed.type).toBe('i') + }) + + it('should return JSON for nested function calls', () => { + const result = parsec.evalRaw('abs(sin(0) - 1)') + const parsed = JSON.parse(result) + + expect(parsed.val).toBe('1') + expect(parsed.type).toBe('i') // C++ returns integer for exact integer results + }) + }) + + describe('Error Handling - Raw JSON', () => { + it('should throw error for invalid syntax (not return error JSON)', () => { + expect(() => { + parsec.evalRaw('2 + )') // Actual syntax error - unmatched parenthesis + }).toThrow() + }) + + it('should throw error for undefined variables', () => { + expect(() => { + parsec.evalRaw('x + 1') + }).toThrow() + }) + + it('should throw error for empty equation', () => { + expect(() => { + parsec.evalRaw('') + }).toThrow() + }) + + it('should throw error for whitespace-only equation', () => { + expect(() => { + parsec.evalRaw(' ') + }).toThrow() + }) + }) + + describe('Comparison with eval() function', () => { + it('should return JSON string while eval() returns JavaScript type for integers', () => { + const rawResult = parsec.evalRaw('2 + 3') + const evalResult = parsec.eval('2 + 3') + + expect(typeof rawResult).toBe('string') + expect(typeof evalResult).toBe('number') + + const parsed = JSON.parse(rawResult) + expect(parsed.val).toBe('5') + expect(parsed.type).toBe('i') + expect(evalResult).toBe(5) + }) + + it('should return JSON string while eval() returns JavaScript type for floats', () => { + const rawResult = parsec.evalRaw('7 / 2') + const evalResult = parsec.eval('7 / 2') + + expect(typeof rawResult).toBe('string') + expect(typeof evalResult).toBe('number') + + const parsed = JSON.parse(rawResult) + expect(parsed.val).toBe('3.5') + expect(parsed.type).toBe('f') + expect(evalResult).toBe(3.5) + }) + + it('should return JSON string while eval() returns JavaScript type for booleans', () => { + const rawResult = parsec.evalRaw('5 > 3') + const evalResult = parsec.eval('5 > 3') + + expect(typeof rawResult).toBe('string') + expect(typeof evalResult).toBe('boolean') + + const parsed = JSON.parse(rawResult) + expect(parsed.val).toBe('true') + expect(parsed.type).toBe('b') + expect(evalResult).toBe(true) + }) + + it('should return JSON string while eval() returns JavaScript type for strings', () => { + const rawResult = parsec.evalRaw('concat("Hello", " World")') + const evalResult = parsec.eval('concat("Hello", " World")') + + expect(typeof rawResult).toBe('string') + expect(typeof evalResult).toBe('string') + + const parsed = JSON.parse(rawResult) + expect(parsed.val).toBe('Hello World') + expect(parsed.type).toBe('s') + expect(evalResult).toBe('Hello World') + }) + }) + + describe('Platform Consistency Tests', () => { + it('should match expected C++ CalcJson format exactly', () => { + const result = parsec.evalRaw('42') + const parsed = JSON.parse(result) + + // Verify exact format matches C++ CalcJson output + expect(parsed).toEqual({ + val: '42', + type: 'i' + }) + + // Ensure no extra properties + expect(Object.keys(parsed).sort()).toEqual(['type', 'val']) + }) + + it('should preserve C++ float formatting', () => { + const result = parsec.evalRaw('3.14159') + const parsed = JSON.parse(result) + + expect(parsed.type).toBe('f') + expect(parsed.val).toMatch(/^3\.14159/) + }) + + it('should preserve C++ special float values exactly', () => { + const tests = [ + { expr: '1/0', expectedVal: 'inf' }, + { expr: '-1/0', expectedVal: '-inf' }, + { expr: '0/0', expectedVal: '-nan' } // C++ actually returns -nan for 0/0 + ] + + tests.forEach(({ expr, expectedVal }) => { + const result = parsec.evalRaw(expr) + const parsed = JSON.parse(result) + + expect(parsed.val).toBe(expectedVal) + expect(parsed.type).toBe('f') + }) + }) + }) +}) \ No newline at end of file