Skip to content

Commit b4b2c34

Browse files
authored
Update 4ndyMath.js
1 parent 4fb4d23 commit b4b2c34

1 file changed

Lines changed: 36 additions & 221 deletions

File tree

4ndyMath.js

Lines changed: 36 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
const _4ndyMath = {
55
VERSION: '3.5.0',
66
_cache: new Map(),
7-
8-
// Built-in functions and their derivative rules
97
_functions: {
108
sin: { fn: Math.sin, d: (u, du) => ({ type: 'operator', op: '*', left: { type: 'function', name: 'cos', arg: u }, right: du }) },
119
cos: { fn: Math.cos, d: (u, du) => ({
@@ -33,60 +31,37 @@
3331
exp: { fn: Math.exp, d: (u, du) => ({
3432
type: 'operator', op: '*', left: { type: 'function', name: 'exp', arg: u }, right: du
3533
}) },
36-
// sec(x)=1/cos(x) defined for differentiation purposes only
3734
sec: { fn: (x) => 1/Math.cos(x), d: (u, du) => ({
3835
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
4142
}) }
4243
},
4344

44-
// Core validation and error handling
4545
_validate: {
4646
input: (expr) => {
4747
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');
5049
},
5150
division: (n) => {
5251
if (n === 0) throw new Error('Division by zero');
5352
}
5453
},
5554

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
7355
tokenize: function(expr) {
7456
this._validate.input(expr);
75-
// Regex: numbers, words, operators, parentheses, commas, equals sign.
7657
const tokenRegex = /(\d+\.?\d*|\.\d+)|([a-zA-Z_πφ]+)|([+\-*/·×÷^(),=])/g;
7758
const tokens = [];
7859
let match;
7960
while ((match = tokenRegex.exec(expr)) !== null) {
8061
if (match[1]) {
8162
tokens.push({ type: 'number', value: parseFloat(match[1]) });
8263
} 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] });
9065
} else if (match[3]) {
9166
const char = match[3];
9267
if (char === ',') {
@@ -96,201 +71,48 @@
9671
}
9772
}
9873
}
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 = [];
10574
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();
14979
}
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);
15880
}
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);
16481
}
165-
return output;
82+
return this._addImplicitMultiplication(tokens);
16683
},
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;
17993
}
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));
18894
}
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++) {
27599
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++) {
277101
matrix[j][k] -= factor * matrix[i][k];
278102
}
279103
}
280104
}
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++) {
286109
solution[i] -= matrix[i][j] * solution[j];
287110
}
288111
solution[i] /= matrix[i][i];
289112
}
290113
return solution;
291114
},
292115

293-
// Evaluate a mathematical expression with optional variable substitution
294116
evaluate: function(expr, variables = {}) {
295117
const cacheKey = `eval:${expr}:${JSON.stringify(variables)}`;
296118
if (this._cache.has(cacheKey)) return this._cache.get(cacheKey);
@@ -301,16 +123,13 @@
301123
return result;
302124
},
303125

304-
// --- Advanced Symbolic Differentiation and Expression Tree Building ---
305-
306-
// Build an expression tree from RPN
307126
_buildExpressionTree: function(rpn) {
308127
const stack = [];
309128
rpn.forEach(token => {
310129
if (token.type === 'number' || token.type === 'variable') {
311130
stack.push({ type: token.type, value: token.value });
312131
} else if (token.type === 'function') {
313-
// Assume single-argument functions
132+
314133
const arg = stack.pop();
315134
stack.push({ type: 'function', name: token.value, arg });
316135
} else if (token.type === 'operator') {
@@ -323,7 +142,6 @@
323142
return stack[0];
324143
},
325144

326-
// Symbolically differentiate an expression (as a string) with respect to a variable (default "x")
327145
differentiate: function(expr, variable = 'x') {
328146
const tokens = this.tokenize(expr);
329147
const rpn = this.parseToRPN(tokens);
@@ -332,7 +150,6 @@
332150
return this._treeToString(dTree);
333151
},
334152

335-
// Recursive differentiation of an expression tree
336153
_differentiateTree: function(node, variable) {
337154
// Constant: derivative is 0
338155
if (node.type === 'number') {
@@ -342,7 +159,7 @@
342159
if (node.type === 'variable') {
343160
return { type: 'number', value: node.value === variable ? 1 : 0 };
344161
}
345-
// Operator node
162+
346163
if (node.type === 'operator') {
347164
const op = node.op;
348165
const u = node.left, v = node.right;
@@ -364,7 +181,6 @@
364181
right: { type: 'operator', op: '^', left: v, right: { type: 'number', value: 2 } }
365182
};
366183
case '^':
367-
// Handle power rule: assume v is constant or u is constant for simplicity
368184
if (v.type === 'number') {
369185
// d/dx u^c = c*u^(c-1)*du
370186
return {
@@ -395,21 +211,21 @@
395211
throw new Error(`Unsupported operator for differentiation: ${op}`);
396212
}
397213
}
398-
// Function node
214+
399215
if (node.type === 'function') {
400216
const func = node.name;
401217
const u = node.arg;
402218
const du = this._differentiateTree(u, variable);
403219
if (!this._functions.hasOwnProperty(func)) {
404220
throw new Error(`No derivative rule for function ${func}`);
405221
}
406-
// Use the derivative rule provided in _functions
222+
407223
return this._functions[func].d(u, du);
408224
}
409225
throw new Error("Unknown node type in differentiation");
410226
},
411227

412-
// Convert an expression tree back into a string (simple unparenthesized format)
228+
413229
_treeToString: function(node) {
414230
if (node.type === 'number') return node.value.toString();
415231
if (node.type === 'variable') return node.value;
@@ -423,6 +239,5 @@
423239
}
424240
};
425241

426-
// Attach to the global window object for browser use.
427242
window._4ndyMath = _4ndyMath;
428243
})();

0 commit comments

Comments
 (0)