From 4d1b0ec142918df066c183a6bce5d5e3cacebc33 Mon Sep 17 00:00:00 2001 From: Jesus Cardenas Date: Wed, 2 Apr 2025 11:50:14 +0200 Subject: [PATCH 1/3] Math Expressions Document --- docs/features/math-expressions.md | 160 ++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/features/math-expressions.md diff --git a/docs/features/math-expressions.md b/docs/features/math-expressions.md new file mode 100644 index 0000000..b718e8f --- /dev/null +++ b/docs/features/math-expressions.md @@ -0,0 +1,160 @@ +# Math-Like Expressions + +## Overview + +This feature enhances the user experience by displaying mathematical expressions in a more familiar and readable format. Instead of using JavaScript notation (e.g., `3*x+9`), expressions will be shown in a traditional mathematical notation (e.g., `3x + 9`). This makes the application more intuitive for users with mathematical backgrounds and reduces confusion between programming and mathematical notations. + +## User Stories + +1. As a user, I want to see mathematical expressions in a familiar format that matches textbooks and mathematical literature. +2. As a user, I want to input expressions using either JavaScript or mathematical notation for flexibility. +3. As a user, I want to see proper mathematical formatting for exponents, fractions, and special functions. +4. As a user, I want consistent mathematical notation across the entire application (formula editor, tooltips, measurements). + +## Implementation Checklist + +
+[ ] Expression Parser Updates + +- [ ] Create a bidirectional converter between JS and math notation +- [ ] Handle basic operations (multiplication, division, exponents) +- [ ] Support special functions (sqrt, sin, cos, tan, etc.) +- [ ] Implement fraction notation conversion +- [ ] Add validation for both notation formats +
+ +
+[ ] Formula Editor Updates + +- [ ] Add real-time preview of mathematical notation +- [ ] Implement syntax highlighting for math expressions +- [ ] Add autocomplete suggestions in mathematical format +- [ ] Create toggle between JS and math notation input modes +- [ ] Update error messages to reference math notation +
+ +
+[ ] UI Component Updates + +- [ ] Update FormulaDisplay component to use math notation +- [ ] Add MathRenderer component for consistent rendering +- [ ] Implement LaTeX-style rendering for complex expressions +- [ ] Update tooltips to show expressions in math notation +- [ ] Add copy buttons for both notation formats +
+ +
+[ ] Documentation Updates + +- [ ] Update user documentation with new notation examples +- [ ] Add notation guide and cheat sheet +- [ ] Document supported mathematical symbols +- [ ] Create migration guide for existing formulas +
+ +
+[ ] Testing + +- [ ] Unit tests for notation conversion +- [ ] Tests for special cases and edge cases +- [ ] Component tests for FormulaDisplay +- [ ] Integration tests for FormulaEditor +- [ ] E2E tests for formula input and display +
+ +## Technical Details + +### Expression Conversion + +```typescript +interface ExpressionConverter { + // Convert from JS notation to math notation + toMathNotation(jsExpr: string): string; + + // Convert from math notation to JS notation + toJSNotation(mathExpr: string): string; + + // Validate expression in either notation + validate(expr: string, format: 'js' | 'math'): boolean; +} + +// Example conversions: +// JS -> Math +// 2*x -> 2x +// x**2 -> x² +// Math.sqrt(x) -> √x +// Math.sin(x) -> sin(x) +``` + +### UI Components + +```typescript +interface MathRendererProps { + expression: string; + format: 'js' | 'math'; + inline?: boolean; + className?: string; +} + +interface FormulaDisplayProps extends MathRendererProps { + editable?: boolean; + onExpressionChange?: (expr: string, format: 'js' | 'math') => void; +} +``` + +### Expression Format Examples + +| Operation | JS Notation | Math Notation | +|-------------|----------------|---------------| +| Multiply | `3*x` | `3x` | +| Power | `x**2` | `x²` | +| Square Root | `Math.sqrt(x)` | `√x` | +| Fraction | `(x+1)/(x-1)` | `(x+1)/(x-1)`| +| Functions | `Math.sin(x)` | `sin(x)` | + +### Key UX Considerations + +1. Maintain backward compatibility with JS notation +2. Provide clear visual feedback for notation conversion +3. Handle copy/paste operations intelligently +4. Support keyboard shortcuts for common mathematical symbols +5. Ensure consistent rendering across different screen sizes + +## Dependencies + +- MathJax or KaTeX for mathematical rendering +- CodeMirror or Monaco Editor for syntax highlighting +- Custom parser for notation conversion +- FormulaEditor component updates +- MathRenderer component (new) + +## Implementation Examples + +Example implementation files will be created at: +- `docs/implementation-example-MathRenderer.md` +- `docs/implementation-example-ExpressionConverter.md` +- `docs/implementation-example-FormulaEditor.md` + +### Example Expression Parsing + +```typescript +// Example of how the parser will handle different notations +const examples = { + multiplication: { + js: '2*x*y', + math: '2xy' + }, + exponents: { + js: 'x**2 + y**3', + math: 'x² + y³' + }, + functions: { + js: 'Math.sin(x)*Math.sqrt(y)', + math: 'sin(x)√y' + }, + fractions: { + js: '(x+1)/(y-1)', + math: '(x+1)/(y-1)' + } +}; +``` \ No newline at end of file From 54a9d9e7ff816e014e37781e3ef5fd34c1dfe265 Mon Sep 17 00:00:00 2001 From: Jesus Cardenas Date: Wed, 2 Apr 2025 12:44:25 +0200 Subject: [PATCH 2/3] feat: implement math expression converter with tests - Add ExpressionConverter class with bidirectional JS-Math notation conversion - Add comprehensive test suite for expression conversion - Update implementation checklist in docs - Fix formula utils to handle math notation --- docs/features/math-expressions.md | 18 +- .../__tests__/expressionConverter.test.ts | 109 +++++++++++ src/utils/expressionConverter.ts | 174 ++++++++++++++++++ src/utils/formulaUtils.ts | 49 ++++- 4 files changed, 331 insertions(+), 19 deletions(-) create mode 100644 src/utils/__tests__/expressionConverter.test.ts create mode 100644 src/utils/expressionConverter.ts diff --git a/docs/features/math-expressions.md b/docs/features/math-expressions.md index b718e8f..1201c74 100644 --- a/docs/features/math-expressions.md +++ b/docs/features/math-expressions.md @@ -14,13 +14,13 @@ This feature enhances the user experience by displaying mathematical expressions ## Implementation Checklist
-[ ] Expression Parser Updates +[✓] Expression Parser Updates -- [ ] Create a bidirectional converter between JS and math notation -- [ ] Handle basic operations (multiplication, division, exponents) -- [ ] Support special functions (sqrt, sin, cos, tan, etc.) -- [ ] Implement fraction notation conversion -- [ ] Add validation for both notation formats +- [✓] Create a bidirectional converter between JS and math notation +- [✓] Handle basic operations (multiplication, division, exponents) +- [✓] Support special functions (sqrt, sin, cos, tan, etc.) +- [✓] Implement fraction notation conversion +- [✓] Add validation for both notation formats
@@ -53,10 +53,10 @@ This feature enhances the user experience by displaying mathematical expressions
-[ ] Testing +[-] Testing -- [ ] Unit tests for notation conversion -- [ ] Tests for special cases and edge cases +- [✓] Unit tests for notation conversion +- [✓] Tests for special cases and edge cases - [ ] Component tests for FormulaDisplay - [ ] Integration tests for FormulaEditor - [ ] E2E tests for formula input and display diff --git a/src/utils/__tests__/expressionConverter.test.ts b/src/utils/__tests__/expressionConverter.test.ts new file mode 100644 index 0000000..855b603 --- /dev/null +++ b/src/utils/__tests__/expressionConverter.test.ts @@ -0,0 +1,109 @@ +import { expressionConverter } from '../expressionConverter'; + +describe('ExpressionConverter', () => { + describe('toMathNotation', () => { + it('should convert basic arithmetic operations', () => { + expect(expressionConverter.toMathNotation('2*x')).toBe('2x'); + expect(expressionConverter.toMathNotation('x*2')).toBe('2x'); + expect(expressionConverter.toMathNotation('x**2')).toBe('x^2'); + expect(expressionConverter.toMathNotation('2*x*3')).toBe('2x×3'); + }); + + it('should convert Math functions', () => { + expect(expressionConverter.toMathNotation('Math.sqrt(x)')).toBe('√(x)'); + expect(expressionConverter.toMathNotation('Math.sin(x)')).toBe('sin(x)'); + expect(expressionConverter.toMathNotation('Math.cos(x)')).toBe('cos(x)'); + expect(expressionConverter.toMathNotation('Math.tan(x)')).toBe('tan(x)'); + expect(expressionConverter.toMathNotation('Math.log(x)')).toBe('ln(x)'); + expect(expressionConverter.toMathNotation('Math.exp(x)')).toBe('e^(x)'); + expect(expressionConverter.toMathNotation('Math.abs(x)')).toBe('|x|'); + }); + + it('should convert constants', () => { + expect(expressionConverter.toMathNotation('Math.PI')).toBe('π'); + expect(expressionConverter.toMathNotation('Math.E')).toBe('e'); + }); + + it('should handle complex expressions', () => { + expect(expressionConverter.toMathNotation('2*Math.sin(x)**2')).toBe('2sin(x)^2'); + expect(expressionConverter.toMathNotation('Math.sqrt(x**2 + y**2)')).toBe('√(x^2 + y^2)'); + expect(expressionConverter.toMathNotation('Math.sin(Math.PI*x)')).toBe('sin(π×x)'); + }); + }); + + describe('toJSNotation', () => { + it('should convert basic arithmetic operations', () => { + expect(expressionConverter.toJSNotation('2x')).toBe('2*x'); + expect(expressionConverter.toJSNotation('x^2')).toBe('x**2'); + expect(expressionConverter.toJSNotation('2x×3')).toBe('2*x*3'); + }); + + it('should convert math functions', () => { + expect(expressionConverter.toJSNotation('√(x)')).toBe('Math.sqrt(x)'); + expect(expressionConverter.toJSNotation('sin(x)')).toBe('Math.sin(x)'); + expect(expressionConverter.toJSNotation('cos(x)')).toBe('Math.cos(x)'); + expect(expressionConverter.toJSNotation('tan(x)')).toBe('Math.tan(x)'); + expect(expressionConverter.toJSNotation('ln(x)')).toBe('Math.log(x)'); + expect(expressionConverter.toJSNotation('|x|')).toBe('Math.abs(x)'); + }); + + it('should convert constants', () => { + expect(expressionConverter.toJSNotation('π')).toBe('Math.PI'); + expect(expressionConverter.toJSNotation('e^(x)')).toBe('Math.exp(x)'); + }); + + it('should handle complex expressions', () => { + expect(expressionConverter.toJSNotation('2sin(x)^2')).toBe('2*Math.sin(x)**2'); + expect(expressionConverter.toJSNotation('√(x^2 + y^2)')).toBe('Math.sqrt(x**2 + y**2)'); + expect(expressionConverter.toJSNotation('sin(π×x)')).toBe('Math.sin(Math.PI*x)'); + }); + }); + + describe('validate', () => { + it('should validate valid JS expressions', () => { + expect(expressionConverter.validate('2*x', 'js')).toBe(true); + expect(expressionConverter.validate('Math.sin(x)', 'js')).toBe(true); + expect(expressionConverter.validate('x**2 + 2*x + 1', 'js')).toBe(true); + }); + + it('should validate valid math expressions', () => { + expect(expressionConverter.validate('2x', 'math')).toBe(true); + expect(expressionConverter.validate('sin(x)', 'math')).toBe(true); + expect(expressionConverter.validate('x^2 + 2x + 1', 'math')).toBe(true); + }); + + it('should reject invalid expressions', () => { + expect(expressionConverter.validate('2**', 'js')).toBe(false); + expect(expressionConverter.validate('sin(', 'math')).toBe(false); + expect(expressionConverter.validate('x + + y', 'js')).toBe(false); + }); + }); + + describe('bidirectional conversion', () => { + it('should preserve meaning when converting back and forth', () => { + const jsExpressions = [ + '2*x + 1', + 'Math.sin(x)**2', + 'Math.sqrt(x**2 + y**2)', + 'Math.sin(Math.PI*x)' + ]; + + for (const jsExpr of jsExpressions) { + const mathExpr = expressionConverter.toMathNotation(jsExpr); + const backToJs = expressionConverter.toJSNotation(mathExpr); + + // Create test functions to compare results + const f1 = new Function('x', 'y', `return ${jsExpr}`); + const f2 = new Function('x', 'y', `return ${backToJs}`); + + // Test with some sample values + const testValues = [-1, 0, 1, Math.PI]; + for (const x of testValues) { + for (const y of testValues) { + expect(f1(x, y)).toBeCloseTo(f2(x, y), 10); + } + } + } + }); + }); +}); \ No newline at end of file diff --git a/src/utils/expressionConverter.ts b/src/utils/expressionConverter.ts new file mode 100644 index 0000000..738a139 --- /dev/null +++ b/src/utils/expressionConverter.ts @@ -0,0 +1,174 @@ +import { validateFormula } from './formulaUtils'; +import { Formula } from '@/types/formula'; + +interface Token { + type: 'number' | 'variable' | 'operator' | 'function' | 'constant' | 'parenthesis'; + value: string; +} + +interface JSToMathRule { + js: RegExp; + math: string; +} + +interface MathToJSRule { + math: RegExp; + js: string; +} + +const jsToMathRules: JSToMathRule[] = [ + // Math functions + { js: /Math\.sqrt\(/g, math: '√(' }, + { js: /Math\.sin\(/g, math: 'sin(' }, + { js: /Math\.cos\(/g, math: 'cos(' }, + { js: /Math\.tan\(/g, math: 'tan(' }, + { js: /Math\.log\(/g, math: 'ln(' }, + { js: /Math\.abs\(/g, math: '|' }, + { js: /Math\.exp\(/g, math: 'e^(' }, + // Constants + { js: /Math\.PI/g, math: 'π' }, + { js: /Math\.E/g, math: 'e' }, + // Operators + { js: /\*\*/g, math: '^' }, + { js: /(\d+)\s*\*\s*([a-zA-Z])/g, math: '$1$2' }, + { js: /([a-zA-Z])\s*\*\s*(\d+)/g, math: '$2$1' }, + { js: /\*/g, math: '×' }, +]; + +const mathToJSRules: MathToJSRule[] = [ + // Math functions + { math: /√\(/g, js: 'Math.sqrt(' }, + { math: /sin\(/g, js: 'Math.sin(' }, + { math: /cos\(/g, js: 'Math.cos(' }, + { math: /tan\(/g, js: 'Math.tan(' }, + { math: /ln\(/g, js: 'Math.log(' }, + { math: /\|([^|]+)\|/g, js: 'Math.abs($1)' }, + // Constants + { math: /π/g, js: 'Math.PI' }, + { math: /e\^/g, js: 'Math.exp' }, + // Operators + { math: /\^/g, js: '**' }, + { math: /(\d+)([a-zA-Z])/g, js: '$1*$2' }, + { math: /([a-zA-Z])(\d+)/g, js: '$1*$2' }, + { math: /×/g, js: '*' }, +]; + +export class ExpressionConverter { + /** + * Convert from JS notation to math notation + */ + toMathNotation(expression: string): string { + let result = expression; + + // Handle Math functions and constants first + for (const [js, math] of Object.entries({ + 'Math.sqrt(': '√(', + 'Math.sin(': 'sin(', + 'Math.cos(': 'cos(', + 'Math.tan(': 'tan(', + 'Math.log(': 'ln(', + 'Math.abs(': '|', + 'Math.exp(': 'e^(', + 'Math.PI': 'π', + 'Math.E': 'e' + })) { + result = result.replace(new RegExp(js.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), math); + } + + // Handle operators + result = result.replace(/\*\*/g, '^'); + + // Handle chained multiplication with variables (e.g., 2*x*3 -> 2x×3) + result = result.replace(/(\d+)\s*\*\s*([a-zA-Z])\s*\*\s*(\d+)/g, '$1$2×$3'); + + // Handle simple multiplication with variables + result = result.replace(/(\d+)\s*\*\s*([a-zA-Z])/g, '$1$2'); + result = result.replace(/([a-zA-Z])\s*\*\s*(\d+)/g, '$2$1'); + + // Handle remaining multiplication + result = result.replace(/\*/g, '×'); + + // Handle absolute value closing + result = result.replace(/\|([^|]+)\)/g, '|$1|'); + + return result; + } + + /** + * Convert from math notation to JS notation + */ + toJSNotation(expression: string): string { + let result = expression; + + // Handle absolute value first + result = result.replace(/\|([^|]+)\|/g, 'Math.abs($1)'); + + // Handle Math functions and constants + for (const [math, js] of Object.entries({ + '√(': 'Math.sqrt(', + 'sin(': 'Math.sin(', + 'cos(': 'Math.cos(', + 'tan(': 'Math.tan(', + 'ln(': 'Math.log(', + 'π': 'Math.PI', + 'e^(': 'Math.exp(' + })) { + result = result.replace(new RegExp(math.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), js); + } + + // Handle operators + result = result.replace(/\^/g, '**'); + + // Handle implicit multiplication + result = result.replace(/(\d+)([a-zA-Z])/g, '$1*$2'); + result = result.replace(/([a-zA-Z])(\d+)/g, '$1*$2'); + + // Handle remaining multiplication + result = result.replace(/×/g, '*'); + + return result; + } + + /** + * Validate expression in either notation + */ + validate(expression: string, type: 'js' | 'math'): boolean { + try { + // Convert math notation to JS if needed + const jsExpression = type === 'math' ? this.toJSNotation(expression) : expression; + + // Basic syntax validation + if (jsExpression.includes('++') || jsExpression.includes('--') || + jsExpression.includes('+ +') || jsExpression.includes('- -') || + jsExpression.includes('**-') || /[+\-*/]{2,}/.test(jsExpression.replace(/\*\*/g, '^'))) { + return false; + } + + // Check for unmatched parentheses + const openCount = (jsExpression.match(/\(/g) || []).length; + const closeCount = (jsExpression.match(/\)/g) || []).length; + if (openCount !== closeCount) { + return false; + } + + // Create a safe evaluation context with Math functions + const context = { + Math, + x: 1, // Sample value for testing + y: 1, + z: 1, + }; + + // Try to evaluate the expression + const fn = new Function(...Object.keys(context), `return ${jsExpression}`); + fn(...Object.values(context)); + + return true; + } catch { + return false; + } + } +} + +// Export a singleton instance +export const expressionConverter = new ExpressionConverter(); \ No newline at end of file diff --git a/src/utils/formulaUtils.ts b/src/utils/formulaUtils.ts index ce29764..c0cdc2d 100644 --- a/src/utils/formulaUtils.ts +++ b/src/utils/formulaUtils.ts @@ -1,5 +1,6 @@ import { Formula, FormulaPoint, FormulaExample, FormulaType } from "@/types/formula"; import { Point } from "@/types/shapes"; +import { expressionConverter } from './expressionConverter'; // Constants const MAX_SAMPLES = 100000; @@ -63,19 +64,28 @@ const createFunctionFromExpression = ( return () => NaN; } - if (expression === 'Math.exp(x)') { + // First try to convert from math notation to JS notation + let jsExpression = expression; + try { + jsExpression = expressionConverter.toJSNotation(expression); + } catch (e) { + // If conversion fails, assume it's already in JS notation + console.log('Math notation conversion failed, assuming JS notation:', e); + } + + if (jsExpression === 'Math.exp(x)') { return (x: number) => Math.exp(x) * scaleFactor; } - if (expression === '1 / (1 + Math.exp(-x))') { + if (jsExpression === '1 / (1 + Math.exp(-x))') { return (x: number) => (1 / (1 + Math.exp(-x))) * scaleFactor; } - if (expression === 'Math.sqrt(Math.abs(x))') { + if (jsExpression === 'Math.sqrt(Math.abs(x))') { return (x: number) => Math.sqrt(Math.abs(x)) * scaleFactor; } try { // Only wrap x in parentheses if it's not part of another identifier (like Math.exp) - const scaledExpression = expression.replace(/(? number ): FormulaPoint[] => { const points: FormulaPoint[] = []; - const chars = detectFunctionCharacteristics(formula.expression); + + // First try to convert from math notation to JS notation + let jsExpression = formula.expression; + try { + jsExpression = expressionConverter.toJSNotation(formula.expression); + } catch (e) { + // If conversion fails, assume it's already in JS notation + console.log('Math notation conversion failed, assuming JS notation:', e); + } + + const chars = detectFunctionCharacteristics(jsExpression); const { isLogarithmic, allowsNegativeX, hasPow } = chars; let prevY: number | null = null; let prevX: number | null = null; // Special case for complex formulas to detect and handle rapid changes - const isComplexFormula = formula.expression === 'Math.pow(x * 2, 2) + Math.pow((5 * Math.pow(x * 4, 2) - Math.sqrt(Math.abs(x))) * 2, 2) - 1'; + const isComplexFormula = jsExpression === 'Math.pow(x * 2, 2) + Math.pow((5 * Math.pow(x * 4, 2) - Math.sqrt(Math.abs(x))) * 2, 2) - 1'; for (const x of xValues) { let y: number; @@ -400,8 +420,8 @@ const evaluatePoints = ( if (isLogarithmic) { if (Math.abs(x) < 1e-10) { // Skip points too close to zero for log functions - y = NaN; - isValidDomain = false; + y = NaN; + isValidDomain = false; } else { y = fn(x); // Additional validation for logarithmic results @@ -523,9 +543,18 @@ export const validateFormula = (formula: Formula): { isValid: boolean; error?: s return { isValid: false, error: 'Expression cannot be empty' }; } + // First try to convert from math notation to JS notation + let jsExpression = formula.expression; + try { + jsExpression = expressionConverter.toJSNotation(formula.expression); + } catch (e) { + // If conversion fails, assume it's already in JS notation + console.log('Math notation conversion failed, assuming JS notation:', e); + } + // Validate based on formula type if (formula.type === 'parametric') { - const [xExpr, yExpr] = formula.expression.split(';').map(expr => expr.trim()); + const [xExpr, yExpr] = jsExpression.split(';').map(expr => expr.trim()); if (!xExpr || !yExpr) { return { isValid: false, error: 'Parametric expression must be in format "x(t);y(t)"' }; } @@ -546,7 +575,7 @@ export const validateFormula = (formula: Formula): { isValid: boolean; error?: s // For function and polar types try { // Test if the expression can be compiled - const scaledExpression = formula.expression.replace(/(\W|^)x(\W|$)/g, '$1(x)$2'); + const scaledExpression = jsExpression.replace(/(\W|^)x(\W|$)/g, '$1(x)$2'); new Function('x', ` const {sin, cos, tan, exp, log, sqrt, abs, pow, PI, E} = Math; return (${scaledExpression}); From 25d411613ce2b5ada25ad2680df0d1d6eb0e4e3e Mon Sep 17 00:00:00 2001 From: Jesus Cardenas Date: Wed, 2 Apr 2025 15:41:24 +0200 Subject: [PATCH 3/3] Revert "feat: implement math expression converter with tests" This reverts commit 54a9d9e7ff816e014e37781e3ef5fd34c1dfe265. --- docs/features/math-expressions.md | 18 +- .../__tests__/expressionConverter.test.ts | 109 ----------- src/utils/expressionConverter.ts | 174 ------------------ src/utils/formulaUtils.ts | 49 +---- 4 files changed, 19 insertions(+), 331 deletions(-) delete mode 100644 src/utils/__tests__/expressionConverter.test.ts delete mode 100644 src/utils/expressionConverter.ts diff --git a/docs/features/math-expressions.md b/docs/features/math-expressions.md index 1201c74..b718e8f 100644 --- a/docs/features/math-expressions.md +++ b/docs/features/math-expressions.md @@ -14,13 +14,13 @@ This feature enhances the user experience by displaying mathematical expressions ## Implementation Checklist
-[✓] Expression Parser Updates +[ ] Expression Parser Updates -- [✓] Create a bidirectional converter between JS and math notation -- [✓] Handle basic operations (multiplication, division, exponents) -- [✓] Support special functions (sqrt, sin, cos, tan, etc.) -- [✓] Implement fraction notation conversion -- [✓] Add validation for both notation formats +- [ ] Create a bidirectional converter between JS and math notation +- [ ] Handle basic operations (multiplication, division, exponents) +- [ ] Support special functions (sqrt, sin, cos, tan, etc.) +- [ ] Implement fraction notation conversion +- [ ] Add validation for both notation formats
@@ -53,10 +53,10 @@ This feature enhances the user experience by displaying mathematical expressions
-[-] Testing +[ ] Testing -- [✓] Unit tests for notation conversion -- [✓] Tests for special cases and edge cases +- [ ] Unit tests for notation conversion +- [ ] Tests for special cases and edge cases - [ ] Component tests for FormulaDisplay - [ ] Integration tests for FormulaEditor - [ ] E2E tests for formula input and display diff --git a/src/utils/__tests__/expressionConverter.test.ts b/src/utils/__tests__/expressionConverter.test.ts deleted file mode 100644 index 855b603..0000000 --- a/src/utils/__tests__/expressionConverter.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { expressionConverter } from '../expressionConverter'; - -describe('ExpressionConverter', () => { - describe('toMathNotation', () => { - it('should convert basic arithmetic operations', () => { - expect(expressionConverter.toMathNotation('2*x')).toBe('2x'); - expect(expressionConverter.toMathNotation('x*2')).toBe('2x'); - expect(expressionConverter.toMathNotation('x**2')).toBe('x^2'); - expect(expressionConverter.toMathNotation('2*x*3')).toBe('2x×3'); - }); - - it('should convert Math functions', () => { - expect(expressionConverter.toMathNotation('Math.sqrt(x)')).toBe('√(x)'); - expect(expressionConverter.toMathNotation('Math.sin(x)')).toBe('sin(x)'); - expect(expressionConverter.toMathNotation('Math.cos(x)')).toBe('cos(x)'); - expect(expressionConverter.toMathNotation('Math.tan(x)')).toBe('tan(x)'); - expect(expressionConverter.toMathNotation('Math.log(x)')).toBe('ln(x)'); - expect(expressionConverter.toMathNotation('Math.exp(x)')).toBe('e^(x)'); - expect(expressionConverter.toMathNotation('Math.abs(x)')).toBe('|x|'); - }); - - it('should convert constants', () => { - expect(expressionConverter.toMathNotation('Math.PI')).toBe('π'); - expect(expressionConverter.toMathNotation('Math.E')).toBe('e'); - }); - - it('should handle complex expressions', () => { - expect(expressionConverter.toMathNotation('2*Math.sin(x)**2')).toBe('2sin(x)^2'); - expect(expressionConverter.toMathNotation('Math.sqrt(x**2 + y**2)')).toBe('√(x^2 + y^2)'); - expect(expressionConverter.toMathNotation('Math.sin(Math.PI*x)')).toBe('sin(π×x)'); - }); - }); - - describe('toJSNotation', () => { - it('should convert basic arithmetic operations', () => { - expect(expressionConverter.toJSNotation('2x')).toBe('2*x'); - expect(expressionConverter.toJSNotation('x^2')).toBe('x**2'); - expect(expressionConverter.toJSNotation('2x×3')).toBe('2*x*3'); - }); - - it('should convert math functions', () => { - expect(expressionConverter.toJSNotation('√(x)')).toBe('Math.sqrt(x)'); - expect(expressionConverter.toJSNotation('sin(x)')).toBe('Math.sin(x)'); - expect(expressionConverter.toJSNotation('cos(x)')).toBe('Math.cos(x)'); - expect(expressionConverter.toJSNotation('tan(x)')).toBe('Math.tan(x)'); - expect(expressionConverter.toJSNotation('ln(x)')).toBe('Math.log(x)'); - expect(expressionConverter.toJSNotation('|x|')).toBe('Math.abs(x)'); - }); - - it('should convert constants', () => { - expect(expressionConverter.toJSNotation('π')).toBe('Math.PI'); - expect(expressionConverter.toJSNotation('e^(x)')).toBe('Math.exp(x)'); - }); - - it('should handle complex expressions', () => { - expect(expressionConverter.toJSNotation('2sin(x)^2')).toBe('2*Math.sin(x)**2'); - expect(expressionConverter.toJSNotation('√(x^2 + y^2)')).toBe('Math.sqrt(x**2 + y**2)'); - expect(expressionConverter.toJSNotation('sin(π×x)')).toBe('Math.sin(Math.PI*x)'); - }); - }); - - describe('validate', () => { - it('should validate valid JS expressions', () => { - expect(expressionConverter.validate('2*x', 'js')).toBe(true); - expect(expressionConverter.validate('Math.sin(x)', 'js')).toBe(true); - expect(expressionConverter.validate('x**2 + 2*x + 1', 'js')).toBe(true); - }); - - it('should validate valid math expressions', () => { - expect(expressionConverter.validate('2x', 'math')).toBe(true); - expect(expressionConverter.validate('sin(x)', 'math')).toBe(true); - expect(expressionConverter.validate('x^2 + 2x + 1', 'math')).toBe(true); - }); - - it('should reject invalid expressions', () => { - expect(expressionConverter.validate('2**', 'js')).toBe(false); - expect(expressionConverter.validate('sin(', 'math')).toBe(false); - expect(expressionConverter.validate('x + + y', 'js')).toBe(false); - }); - }); - - describe('bidirectional conversion', () => { - it('should preserve meaning when converting back and forth', () => { - const jsExpressions = [ - '2*x + 1', - 'Math.sin(x)**2', - 'Math.sqrt(x**2 + y**2)', - 'Math.sin(Math.PI*x)' - ]; - - for (const jsExpr of jsExpressions) { - const mathExpr = expressionConverter.toMathNotation(jsExpr); - const backToJs = expressionConverter.toJSNotation(mathExpr); - - // Create test functions to compare results - const f1 = new Function('x', 'y', `return ${jsExpr}`); - const f2 = new Function('x', 'y', `return ${backToJs}`); - - // Test with some sample values - const testValues = [-1, 0, 1, Math.PI]; - for (const x of testValues) { - for (const y of testValues) { - expect(f1(x, y)).toBeCloseTo(f2(x, y), 10); - } - } - } - }); - }); -}); \ No newline at end of file diff --git a/src/utils/expressionConverter.ts b/src/utils/expressionConverter.ts deleted file mode 100644 index 738a139..0000000 --- a/src/utils/expressionConverter.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { validateFormula } from './formulaUtils'; -import { Formula } from '@/types/formula'; - -interface Token { - type: 'number' | 'variable' | 'operator' | 'function' | 'constant' | 'parenthesis'; - value: string; -} - -interface JSToMathRule { - js: RegExp; - math: string; -} - -interface MathToJSRule { - math: RegExp; - js: string; -} - -const jsToMathRules: JSToMathRule[] = [ - // Math functions - { js: /Math\.sqrt\(/g, math: '√(' }, - { js: /Math\.sin\(/g, math: 'sin(' }, - { js: /Math\.cos\(/g, math: 'cos(' }, - { js: /Math\.tan\(/g, math: 'tan(' }, - { js: /Math\.log\(/g, math: 'ln(' }, - { js: /Math\.abs\(/g, math: '|' }, - { js: /Math\.exp\(/g, math: 'e^(' }, - // Constants - { js: /Math\.PI/g, math: 'π' }, - { js: /Math\.E/g, math: 'e' }, - // Operators - { js: /\*\*/g, math: '^' }, - { js: /(\d+)\s*\*\s*([a-zA-Z])/g, math: '$1$2' }, - { js: /([a-zA-Z])\s*\*\s*(\d+)/g, math: '$2$1' }, - { js: /\*/g, math: '×' }, -]; - -const mathToJSRules: MathToJSRule[] = [ - // Math functions - { math: /√\(/g, js: 'Math.sqrt(' }, - { math: /sin\(/g, js: 'Math.sin(' }, - { math: /cos\(/g, js: 'Math.cos(' }, - { math: /tan\(/g, js: 'Math.tan(' }, - { math: /ln\(/g, js: 'Math.log(' }, - { math: /\|([^|]+)\|/g, js: 'Math.abs($1)' }, - // Constants - { math: /π/g, js: 'Math.PI' }, - { math: /e\^/g, js: 'Math.exp' }, - // Operators - { math: /\^/g, js: '**' }, - { math: /(\d+)([a-zA-Z])/g, js: '$1*$2' }, - { math: /([a-zA-Z])(\d+)/g, js: '$1*$2' }, - { math: /×/g, js: '*' }, -]; - -export class ExpressionConverter { - /** - * Convert from JS notation to math notation - */ - toMathNotation(expression: string): string { - let result = expression; - - // Handle Math functions and constants first - for (const [js, math] of Object.entries({ - 'Math.sqrt(': '√(', - 'Math.sin(': 'sin(', - 'Math.cos(': 'cos(', - 'Math.tan(': 'tan(', - 'Math.log(': 'ln(', - 'Math.abs(': '|', - 'Math.exp(': 'e^(', - 'Math.PI': 'π', - 'Math.E': 'e' - })) { - result = result.replace(new RegExp(js.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), math); - } - - // Handle operators - result = result.replace(/\*\*/g, '^'); - - // Handle chained multiplication with variables (e.g., 2*x*3 -> 2x×3) - result = result.replace(/(\d+)\s*\*\s*([a-zA-Z])\s*\*\s*(\d+)/g, '$1$2×$3'); - - // Handle simple multiplication with variables - result = result.replace(/(\d+)\s*\*\s*([a-zA-Z])/g, '$1$2'); - result = result.replace(/([a-zA-Z])\s*\*\s*(\d+)/g, '$2$1'); - - // Handle remaining multiplication - result = result.replace(/\*/g, '×'); - - // Handle absolute value closing - result = result.replace(/\|([^|]+)\)/g, '|$1|'); - - return result; - } - - /** - * Convert from math notation to JS notation - */ - toJSNotation(expression: string): string { - let result = expression; - - // Handle absolute value first - result = result.replace(/\|([^|]+)\|/g, 'Math.abs($1)'); - - // Handle Math functions and constants - for (const [math, js] of Object.entries({ - '√(': 'Math.sqrt(', - 'sin(': 'Math.sin(', - 'cos(': 'Math.cos(', - 'tan(': 'Math.tan(', - 'ln(': 'Math.log(', - 'π': 'Math.PI', - 'e^(': 'Math.exp(' - })) { - result = result.replace(new RegExp(math.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), js); - } - - // Handle operators - result = result.replace(/\^/g, '**'); - - // Handle implicit multiplication - result = result.replace(/(\d+)([a-zA-Z])/g, '$1*$2'); - result = result.replace(/([a-zA-Z])(\d+)/g, '$1*$2'); - - // Handle remaining multiplication - result = result.replace(/×/g, '*'); - - return result; - } - - /** - * Validate expression in either notation - */ - validate(expression: string, type: 'js' | 'math'): boolean { - try { - // Convert math notation to JS if needed - const jsExpression = type === 'math' ? this.toJSNotation(expression) : expression; - - // Basic syntax validation - if (jsExpression.includes('++') || jsExpression.includes('--') || - jsExpression.includes('+ +') || jsExpression.includes('- -') || - jsExpression.includes('**-') || /[+\-*/]{2,}/.test(jsExpression.replace(/\*\*/g, '^'))) { - return false; - } - - // Check for unmatched parentheses - const openCount = (jsExpression.match(/\(/g) || []).length; - const closeCount = (jsExpression.match(/\)/g) || []).length; - if (openCount !== closeCount) { - return false; - } - - // Create a safe evaluation context with Math functions - const context = { - Math, - x: 1, // Sample value for testing - y: 1, - z: 1, - }; - - // Try to evaluate the expression - const fn = new Function(...Object.keys(context), `return ${jsExpression}`); - fn(...Object.values(context)); - - return true; - } catch { - return false; - } - } -} - -// Export a singleton instance -export const expressionConverter = new ExpressionConverter(); \ No newline at end of file diff --git a/src/utils/formulaUtils.ts b/src/utils/formulaUtils.ts index c0cdc2d..ce29764 100644 --- a/src/utils/formulaUtils.ts +++ b/src/utils/formulaUtils.ts @@ -1,6 +1,5 @@ import { Formula, FormulaPoint, FormulaExample, FormulaType } from "@/types/formula"; import { Point } from "@/types/shapes"; -import { expressionConverter } from './expressionConverter'; // Constants const MAX_SAMPLES = 100000; @@ -64,28 +63,19 @@ const createFunctionFromExpression = ( return () => NaN; } - // First try to convert from math notation to JS notation - let jsExpression = expression; - try { - jsExpression = expressionConverter.toJSNotation(expression); - } catch (e) { - // If conversion fails, assume it's already in JS notation - console.log('Math notation conversion failed, assuming JS notation:', e); - } - - if (jsExpression === 'Math.exp(x)') { + if (expression === 'Math.exp(x)') { return (x: number) => Math.exp(x) * scaleFactor; } - if (jsExpression === '1 / (1 + Math.exp(-x))') { + if (expression === '1 / (1 + Math.exp(-x))') { return (x: number) => (1 / (1 + Math.exp(-x))) * scaleFactor; } - if (jsExpression === 'Math.sqrt(Math.abs(x))') { + if (expression === 'Math.sqrt(Math.abs(x))') { return (x: number) => Math.sqrt(Math.abs(x)) * scaleFactor; } try { // Only wrap x in parentheses if it's not part of another identifier (like Math.exp) - const scaledExpression = jsExpression.replace(/(? number ): FormulaPoint[] => { const points: FormulaPoint[] = []; - - // First try to convert from math notation to JS notation - let jsExpression = formula.expression; - try { - jsExpression = expressionConverter.toJSNotation(formula.expression); - } catch (e) { - // If conversion fails, assume it's already in JS notation - console.log('Math notation conversion failed, assuming JS notation:', e); - } - - const chars = detectFunctionCharacteristics(jsExpression); + const chars = detectFunctionCharacteristics(formula.expression); const { isLogarithmic, allowsNegativeX, hasPow } = chars; let prevY: number | null = null; let prevX: number | null = null; // Special case for complex formulas to detect and handle rapid changes - const isComplexFormula = jsExpression === 'Math.pow(x * 2, 2) + Math.pow((5 * Math.pow(x * 4, 2) - Math.sqrt(Math.abs(x))) * 2, 2) - 1'; + const isComplexFormula = formula.expression === 'Math.pow(x * 2, 2) + Math.pow((5 * Math.pow(x * 4, 2) - Math.sqrt(Math.abs(x))) * 2, 2) - 1'; for (const x of xValues) { let y: number; @@ -420,8 +400,8 @@ const evaluatePoints = ( if (isLogarithmic) { if (Math.abs(x) < 1e-10) { // Skip points too close to zero for log functions - y = NaN; - isValidDomain = false; + y = NaN; + isValidDomain = false; } else { y = fn(x); // Additional validation for logarithmic results @@ -543,18 +523,9 @@ export const validateFormula = (formula: Formula): { isValid: boolean; error?: s return { isValid: false, error: 'Expression cannot be empty' }; } - // First try to convert from math notation to JS notation - let jsExpression = formula.expression; - try { - jsExpression = expressionConverter.toJSNotation(formula.expression); - } catch (e) { - // If conversion fails, assume it's already in JS notation - console.log('Math notation conversion failed, assuming JS notation:', e); - } - // Validate based on formula type if (formula.type === 'parametric') { - const [xExpr, yExpr] = jsExpression.split(';').map(expr => expr.trim()); + const [xExpr, yExpr] = formula.expression.split(';').map(expr => expr.trim()); if (!xExpr || !yExpr) { return { isValid: false, error: 'Parametric expression must be in format "x(t);y(t)"' }; } @@ -575,7 +546,7 @@ export const validateFormula = (formula: Formula): { isValid: boolean; error?: s // For function and polar types try { // Test if the expression can be compiled - const scaledExpression = jsExpression.replace(/(\W|^)x(\W|$)/g, '$1(x)$2'); + const scaledExpression = formula.expression.replace(/(\W|^)x(\W|$)/g, '$1(x)$2'); new Function('x', ` const {sin, cos, tan, exp, log, sqrt, abs, pow, PI, E} = Math; return (${scaledExpression});