|
4 | 4 | const _4ndyMath = { |
5 | 5 | VERSION: '3.5.0', |
6 | 6 | _cache: new Map(), |
7 | | - |
8 | | - // Built-in functions and their derivative rules |
9 | 7 | _functions: { |
10 | 8 | sin: { fn: Math.sin, d: (u, du) => ({ type: 'operator', op: '*', left: { type: 'function', name: 'cos', arg: u }, right: du }) }, |
11 | 9 | cos: { fn: Math.cos, d: (u, du) => ({ |
|
33 | 31 | exp: { fn: Math.exp, d: (u, du) => ({ |
34 | 32 | type: 'operator', op: '*', left: { type: 'function', name: 'exp', arg: u }, right: du |
35 | 33 | }) }, |
36 | | - // sec(x)=1/cos(x) defined for differentiation purposes only |
37 | 34 | sec: { fn: (x) => 1/Math.cos(x), d: (u, du) => ({ |
38 | 35 | type: 'operator', op: '*', |
39 | | - left: { type: 'operator', op: '*', left: { type: 'number', value: 1 }, right: { type: 'function', name: 'tan', arg: u } }, |
40 | | - right: { type: 'operator', op: '^', left: { type: 'function', name: 'sec', arg: u }, right: { type: 'number', value: 2 } } |
| 36 | + left: { |
| 37 | + type: 'operator', op: '*', |
| 38 | + left: { type: 'function', name: 'sec', arg: u }, |
| 39 | + right: { type: 'function', name: 'tan', arg: u } |
| 40 | + }, |
| 41 | + right: du |
41 | 42 | }) } |
42 | 43 | }, |
43 | 44 |
|
44 | | - // Core validation and error handling |
45 | 45 | _validate: { |
46 | 46 | input: (expr) => { |
47 | 47 | if (typeof expr !== 'string') throw new Error('Input must be a string'); |
48 | | - // Allow letters, numbers, whitespace and math symbols including comma for function args. |
49 | | - if (expr.match(/[^a-z0-9\s+\-*/·×÷^(),.=]/gi)) throw new Error('Invalid characters in expression'); |
| 48 | + if (expr.match(/[^a-z0-9\s+\-*/·×÷^(),.]/gi)) throw new Error('Invalid characters in expression'); |
50 | 49 | }, |
51 | 50 | division: (n) => { |
52 | 51 | if (n === 0) throw new Error('Division by zero'); |
53 | 52 | } |
54 | 53 | }, |
55 | 54 |
|
56 | | - // Operator configuration |
57 | | - _ops: { |
58 | | - precedence: { |
59 | | - '+': 2, '-': 2, |
60 | | - '*': 3, '·': 3, '×': 3, |
61 | | - '/': 3, '÷': 3, |
62 | | - '^': 4 |
63 | | - }, |
64 | | - associativity: { |
65 | | - '^': 'right', |
66 | | - '*': 'left', '·': 'left', '×': 'left', |
67 | | - '/': 'left', '÷': 'left', |
68 | | - '+': 'left', '-': 'left' |
69 | | - } |
70 | | - }, |
71 | | - |
72 | | - // Tokenizer: identifies numbers, variables, operators, parentheses, commas, and functions |
73 | 55 | tokenize: function(expr) { |
74 | 56 | this._validate.input(expr); |
75 | | - // Regex: numbers, words, operators, parentheses, commas, equals sign. |
76 | 57 | const tokenRegex = /(\d+\.?\d*|\.\d+)|([a-zA-Z_πφ]+)|([+\-*/·×÷^(),=])/g; |
77 | 58 | const tokens = []; |
78 | 59 | let match; |
79 | 60 | while ((match = tokenRegex.exec(expr)) !== null) { |
80 | 61 | if (match[1]) { |
81 | 62 | tokens.push({ type: 'number', value: parseFloat(match[1]) }); |
82 | 63 | } else if (match[2]) { |
83 | | - // Check if token is a known function name |
84 | | - const lowerVal = match[2].toLowerCase(); |
85 | | - if (this._functions.hasOwnProperty(lowerVal)) { |
86 | | - tokens.push({ type: 'function', value: lowerVal }); |
87 | | - } else { |
88 | | - tokens.push({ type: 'variable', value: match[2] }); |
89 | | - } |
| 64 | + tokens.push({ type: 'variable', value: match[2] }); |
90 | 65 | } else if (match[3]) { |
91 | 66 | const char = match[3]; |
92 | 67 | if (char === ',') { |
|
96 | 71 | } |
97 | 72 | } |
98 | 73 | } |
99 | | - return this._addImplicitMultiplication(tokens); |
100 | | - }, |
101 | | - |
102 | | - // Handle implicit multiplication cases (e.g., 2x, 3(4+5), π(2+3)) |
103 | | - _addImplicitMultiplication: (tokens) => { |
104 | | - const processed = []; |
105 | 74 | for (let i = 0; i < tokens.length; i++) { |
106 | | - processed.push(tokens[i]); |
107 | | - const current = tokens[i]; |
108 | | - const next = tokens[i + 1]; |
109 | | - if (next && ( |
110 | | - // number followed by variable, function, or open parenthesis |
111 | | - (current.type === 'number' && (next.type === 'variable' || next.type === 'function' || next.value === '(')) || |
112 | | - // variable or closing parenthesis followed by number, variable, function, or open parenthesis |
113 | | - ((current.type === 'variable' || current.value === ')') && (next.type === 'number' || next.type === 'variable' || next.type === 'function' || next.value === '(')) |
114 | | - )) { |
115 | | - processed.push({ type: 'operator', value: '·' }); |
116 | | - } |
117 | | - } |
118 | | - return processed; |
119 | | - }, |
120 | | - |
121 | | - // Shunting-yard algorithm implementation |
122 | | - parseToRPN: function(tokens) { |
123 | | - const output = []; |
124 | | - const stack = []; |
125 | | - tokens.forEach(token => { |
126 | | - if (token.type === 'number' || token.type === 'variable') { |
127 | | - output.push(token); |
128 | | - } else if (token.type === 'function') { |
129 | | - stack.push(token); |
130 | | - } else if (token.value === ',') { |
131 | | - // Until the token at the top is a left parenthesis, pop operators to output. |
132 | | - while (stack.length && stack[stack.length - 1].value !== '(') { |
133 | | - output.push(stack.pop()); |
134 | | - } |
135 | | - if (!stack.length) { |
136 | | - throw new Error("Misplaced comma or mismatched parentheses"); |
137 | | - } |
138 | | - } else if (token.value === '(') { |
139 | | - stack.push(token); |
140 | | - } else if (token.value === ')') { |
141 | | - while (stack.length && stack[stack.length - 1].value !== '(') { |
142 | | - output.push(stack.pop()); |
143 | | - } |
144 | | - if (!stack.length) throw new Error("Mismatched parentheses"); |
145 | | - stack.pop(); |
146 | | - // If the token at the top of the stack is a function, pop it onto the output. |
147 | | - if (stack.length && stack[stack.length - 1].type === 'function') { |
148 | | - output.push(stack.pop()); |
| 75 | + if (tokens[i].type === 'variable' && this._functions[tokens[i].value.toLowerCase()]) { |
| 76 | + if (i + 1 < tokens.length && tokens[i + 1].type === 'operator' && tokens[i + 1].value === '(') { |
| 77 | + tokens[i].type = 'function'; |
| 78 | + tokens[i].value = tokens[i].value.toLowerCase(); |
149 | 79 | } |
150 | | - } else if (token.type === 'operator') { |
151 | | - while (stack.length && stack[stack.length - 1].value !== '(' && |
152 | | - ((this._ops.precedence[token.value] < this._ops.precedence[stack[stack.length - 1].value]) || |
153 | | - (this._ops.precedence[token.value] === this._ops.precedence[stack[stack.length - 1].value] && |
154 | | - this._ops.associativity[token.value] === 'left'))) { |
155 | | - output.push(stack.pop()); |
156 | | - } |
157 | | - stack.push(token); |
158 | 80 | } |
159 | | - }); |
160 | | - while (stack.length) { |
161 | | - const op = stack.pop(); |
162 | | - if (op.value === '(' || op.value === ')') throw new Error("Mismatched parentheses"); |
163 | | - output.push(op); |
164 | 81 | } |
165 | | - return output; |
| 82 | + return this._addImplicitMultiplication(tokens); |
166 | 83 | }, |
167 | | - |
168 | | - // Evaluate an RPN expression with variable and function support |
169 | | - _evaluateRPN_withVariables: function(rpn, variables) { |
170 | | - const stack = []; |
171 | | - rpn.forEach(token => { |
172 | | - if (token.type === 'number') { |
173 | | - stack.push(token.value); |
174 | | - } else if (token.type === 'variable') { |
175 | | - if (variables.hasOwnProperty(token.value)) { |
176 | | - stack.push(variables[token.value]); |
177 | | - } else { |
178 | | - throw new Error(`Variable ${token.value} not defined`); |
| 84 | + |
| 85 | + _solveLinearSystem: (system) => { |
| 86 | + const matrix = system.map(eq => [...Object.values(eq.coefficients), eq.constant]); |
| 87 | + const n = matrix.length; |
| 88 | + for (let i = 0; i < n; i++) { |
| 89 | + let maxRow = i; |
| 90 | + for (let j = i + 1; j < n; j++) { |
| 91 | + if (Math.abs(matrix[j][i]) > Math.abs(matrix[maxRow][i])) { |
| 92 | + maxRow = j; |
179 | 93 | } |
180 | | - } else if (token.type === 'function') { |
181 | | - // Assume one-argument functions for now |
182 | | - const arg = stack.pop(); |
183 | | - stack.push(this._functions[token.value].fn(arg)); |
184 | | - } else if (token.type === 'operator') { |
185 | | - const b = stack.pop(); |
186 | | - const a = stack.pop(); |
187 | | - stack.push(this._performOperation(token.value, a, b)); |
188 | 94 | } |
189 | | - }); |
190 | | - if (stack.length !== 1) throw new Error('Invalid expression'); |
191 | | - return stack[0]; |
192 | | - }, |
193 | | - |
194 | | - // Performs the operation on two operands |
195 | | - _performOperation: (op, a, b) => { |
196 | | - switch(op) { |
197 | | - case '+': return a + b; |
198 | | - case '-': return a - b; |
199 | | - case '*': case '·': case '×': return a * b; |
200 | | - case '/': case '÷': |
201 | | - _4ndyMath._validate.division(b); |
202 | | - return a / b; |
203 | | - case '^': return Math.pow(a, b); |
204 | | - default: throw new Error(`Unknown operator: ${op}`); |
205 | | - } |
206 | | - }, |
207 | | - |
208 | | - // Equation solver core: supports equations of the form "LHS = RHS" with variable "x" |
209 | | - solve: function(equation) { |
210 | | - const sides = equation.split('='); |
211 | | - if (sides.length !== 2) throw new Error("Equation must contain one '=' sign"); |
212 | | - const [left, right] = sides.map(side => side.trim()); |
213 | | - const leftRPN = this.parseToRPN(this.tokenize(left)); |
214 | | - const rightRPN = this.parseToRPN(this.tokenize(right)); |
215 | | - const equationTree = this._buildEquationTree(leftRPN, rightRPN); |
216 | | - return this._solveTree(equationTree); |
217 | | - }, |
218 | | - |
219 | | - // Build a simple equation tree f(x)=LHS-RHS by evaluating f(x) at multiple points |
220 | | - _buildEquationTree: function(leftRPN, rightRPN) { |
221 | | - const f = (x) => this._evaluateRPN_withVariables(leftRPN, { x }) - this._evaluateRPN_withVariables(rightRPN, { x }); |
222 | | - const f0 = f(0), f1 = f(1), f2 = f(2); |
223 | | - const secondDiff = f2 - 2 * f1 + f0; |
224 | | - if (Math.abs(secondDiff) < 1e-8) { |
225 | | - return { type: 'linear', coefficients: { x: f1 - f0 }, constant: f0 }; |
226 | | - } else { |
227 | | - const A = secondDiff / 2; |
228 | | - const B = f1 - f0 - A; |
229 | | - const C = f0; |
230 | | - return { type: 'quadratic', a: A, b: B, c: C }; |
231 | | - } |
232 | | - }, |
233 | | - |
234 | | - // Solve the built equation tree |
235 | | - _solveTree: function(tree) { |
236 | | - switch(tree.type) { |
237 | | - case 'linear': return this._solveLinear(tree); |
238 | | - case 'quadratic': return this._solveQuadratic(tree); |
239 | | - default: throw new Error('Unsolvable or unsupported equation type'); |
240 | | - } |
241 | | - }, |
242 | | - |
243 | | - // Linear equation solver: m*x + c = 0 |
244 | | - _solveLinear: (eq) => { |
245 | | - const m = eq.coefficients.x; |
246 | | - if (Math.abs(m) < 1e-8) { |
247 | | - if (Math.abs(eq.constant) < 1e-8) return { x: 'All real numbers' }; |
248 | | - throw new Error('No solution exists'); |
249 | | - } |
250 | | - return { x: -eq.constant / m }; |
251 | | - }, |
252 | | - |
253 | | - // Quadratic equation solver: ax^2 + bx + c = 0 |
254 | | - _solveQuadratic: (eq) => { |
255 | | - const { a, b, c } = eq; |
256 | | - const disc = b**2 - 4 * a * c; |
257 | | - if (disc < 0) return { roots: [] }; |
258 | | - if (Math.abs(disc) < 1e-8) return { root: -b / (2 * a) }; |
259 | | - return { |
260 | | - root1: (-b + Math.sqrt(disc)) / (2 * a), |
261 | | - root2: (-b - Math.sqrt(disc)) / (2 * a) |
262 | | - }; |
263 | | - }, |
264 | | - |
265 | | - // System of equations solver using Gaussian elimination (expects array of equations) |
266 | | - _solveLinearSystem: (system) => { |
267 | | - const matrix = system.map(eq => [ |
268 | | - ...Object.values(eq.coefficients), |
269 | | - eq.constant |
270 | | - ]); |
271 | | - // Gaussian elimination |
272 | | - for (let i = 0; i < matrix.length; i++) { |
273 | | - let pivot = matrix[i][i]; |
274 | | - for (let j = i + 1; j < matrix.length; j++) { |
| 95 | + [matrix[i], matrix[maxRow]] = [matrix[maxRow], matrix[i]]; |
| 96 | + const pivot = matrix[i][i]; |
| 97 | + if (Math.abs(pivot) < 1e-8) throw new Error('Matrix is singular'); |
| 98 | + for (let j = i + 1; j < n; j++) { |
275 | 99 | const factor = matrix[j][i] / pivot; |
276 | | - for (let k = i; k < matrix[0].length; k++) { |
| 100 | + for (let k = i; k < n + 1; k++) { |
277 | 101 | matrix[j][k] -= factor * matrix[i][k]; |
278 | 102 | } |
279 | 103 | } |
280 | 104 | } |
281 | | - // Back substitution |
282 | | - const solution = new Array(matrix.length); |
283 | | - for (let i = matrix.length - 1; i >= 0; i--) { |
284 | | - solution[i] = matrix[i][matrix[0].length - 1]; |
285 | | - for (let j = i + 1; j < matrix.length; j++) { |
| 105 | + const solution = new Array(n); |
| 106 | + for (let i = n - 1; i >= 0; i--) { |
| 107 | + solution[i] = matrix[i][n]; |
| 108 | + for (let j = i + 1; j < n; j++) { |
286 | 109 | solution[i] -= matrix[i][j] * solution[j]; |
287 | 110 | } |
288 | 111 | solution[i] /= matrix[i][i]; |
289 | 112 | } |
290 | 113 | return solution; |
291 | 114 | }, |
292 | 115 |
|
293 | | - // Evaluate a mathematical expression with optional variable substitution |
294 | 116 | evaluate: function(expr, variables = {}) { |
295 | 117 | const cacheKey = `eval:${expr}:${JSON.stringify(variables)}`; |
296 | 118 | if (this._cache.has(cacheKey)) return this._cache.get(cacheKey); |
|
301 | 123 | return result; |
302 | 124 | }, |
303 | 125 |
|
304 | | - // --- Advanced Symbolic Differentiation and Expression Tree Building --- |
305 | | - |
306 | | - // Build an expression tree from RPN |
307 | 126 | _buildExpressionTree: function(rpn) { |
308 | 127 | const stack = []; |
309 | 128 | rpn.forEach(token => { |
310 | 129 | if (token.type === 'number' || token.type === 'variable') { |
311 | 130 | stack.push({ type: token.type, value: token.value }); |
312 | 131 | } else if (token.type === 'function') { |
313 | | - // Assume single-argument functions |
| 132 | + |
314 | 133 | const arg = stack.pop(); |
315 | 134 | stack.push({ type: 'function', name: token.value, arg }); |
316 | 135 | } else if (token.type === 'operator') { |
|
323 | 142 | return stack[0]; |
324 | 143 | }, |
325 | 144 |
|
326 | | - // Symbolically differentiate an expression (as a string) with respect to a variable (default "x") |
327 | 145 | differentiate: function(expr, variable = 'x') { |
328 | 146 | const tokens = this.tokenize(expr); |
329 | 147 | const rpn = this.parseToRPN(tokens); |
|
332 | 150 | return this._treeToString(dTree); |
333 | 151 | }, |
334 | 152 |
|
335 | | - // Recursive differentiation of an expression tree |
336 | 153 | _differentiateTree: function(node, variable) { |
337 | 154 | // Constant: derivative is 0 |
338 | 155 | if (node.type === 'number') { |
|
342 | 159 | if (node.type === 'variable') { |
343 | 160 | return { type: 'number', value: node.value === variable ? 1 : 0 }; |
344 | 161 | } |
345 | | - // Operator node |
| 162 | + |
346 | 163 | if (node.type === 'operator') { |
347 | 164 | const op = node.op; |
348 | 165 | const u = node.left, v = node.right; |
|
364 | 181 | right: { type: 'operator', op: '^', left: v, right: { type: 'number', value: 2 } } |
365 | 182 | }; |
366 | 183 | case '^': |
367 | | - // Handle power rule: assume v is constant or u is constant for simplicity |
368 | 184 | if (v.type === 'number') { |
369 | 185 | // d/dx u^c = c*u^(c-1)*du |
370 | 186 | return { |
|
395 | 211 | throw new Error(`Unsupported operator for differentiation: ${op}`); |
396 | 212 | } |
397 | 213 | } |
398 | | - // Function node |
| 214 | + |
399 | 215 | if (node.type === 'function') { |
400 | 216 | const func = node.name; |
401 | 217 | const u = node.arg; |
402 | 218 | const du = this._differentiateTree(u, variable); |
403 | 219 | if (!this._functions.hasOwnProperty(func)) { |
404 | 220 | throw new Error(`No derivative rule for function ${func}`); |
405 | 221 | } |
406 | | - // Use the derivative rule provided in _functions |
| 222 | + |
407 | 223 | return this._functions[func].d(u, du); |
408 | 224 | } |
409 | 225 | throw new Error("Unknown node type in differentiation"); |
410 | 226 | }, |
411 | 227 |
|
412 | | - // Convert an expression tree back into a string (simple unparenthesized format) |
| 228 | + |
413 | 229 | _treeToString: function(node) { |
414 | 230 | if (node.type === 'number') return node.value.toString(); |
415 | 231 | if (node.type === 'variable') return node.value; |
|
423 | 239 | } |
424 | 240 | }; |
425 | 241 |
|
426 | | - // Attach to the global window object for browser use. |
427 | 242 | window._4ndyMath = _4ndyMath; |
428 | 243 | })(); |
0 commit comments