herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
468 lines (407 loc) • 14.1 kB
JavaScript
/**
* Expression evaluation module for herta.js
*/
const Complex = require('complex.js');
// Expression module
const expression = {};
/**
* Evaluate a mathematical expression
* @param {string} expr - The expression to evaluate
* @param {Object} [scope] - Variables and their values
* @returns {number|Complex|Array|string} - The result of the evaluation
*/
expression.evaluate = function (expr, scope = {}) {
// Parse and evaluate the expression
try {
// Handle unit conversions
if (expr.includes(' to ')) {
return handleUnitConversion(expr);
}
// Handle matrix expressions
if (expr.includes('det(')) {
return handleMatrixExpressions(expr, scope);
}
// Handle complex numbers
if (expr.includes('i') && !expr.match(/[a-zA-Z]+i[a-zA-Z]*/)) {
return handleComplexExpression(expr, scope);
}
// Handle trigonometric expressions with degrees
if (expr.includes('deg')) {
return handleDegreeExpressions(expr, scope);
}
// Standard expression evaluation
return evaluateStandardExpression(expr, scope);
} catch (error) {
throw new Error(`Error evaluating expression: ${error.message}`);
}
};
/**
* Handle unit conversions
* @param {string} expr - The expression with unit conversion
* @returns {string} - The result with the target unit
*/
function handleUnitConversion(expr) {
const parts = expr.split(' to ');
if (parts.length !== 2) {
throw new Error('Invalid unit conversion format');
}
const sourceExpr = parts[0].trim();
const targetUnit = parts[1].trim();
// Extract the value and source unit
const valueMatch = sourceExpr.match(/^([\d.]+)\s*([a-zA-Z]+)$/);
if (!valueMatch) {
throw new Error('Invalid source format for unit conversion');
}
const value = parseFloat(valueMatch[1]);
const sourceUnit = valueMatch[2];
// Perform the conversion
return convertUnit(value, sourceUnit, targetUnit);
}
/**
* Convert a value from one unit to another
* @param {number} value - The value to convert
* @param {string} sourceUnit - The source unit
* @param {string} targetUnit - The target unit
* @returns {string} - The converted value with the target unit
*/
function convertUnit(value, sourceUnit, targetUnit) {
// Define conversion factors
const unitConversions = {
// Length
m: {
cm: 100, mm: 1000, km: 0.001, inch: 39.3701, ft: 3.28084, mi: 0.000621371
},
cm: {
m: 0.01, mm: 10, km: 0.00001, inch: 0.393701, ft: 0.0328084, mi: 0.00000621371
},
mm: {
m: 0.001, cm: 0.1, km: 0.000001, inch: 0.0393701, ft: 0.00328084, mi: 0.000000621371
},
km: {
m: 1000, cm: 100000, mm: 1000000, inch: 39370.1, ft: 3280.84, mi: 0.621371
},
inch: {
m: 0.0254, cm: 2.54, mm: 25.4, km: 0.0000254, ft: 0.0833333, mi: 0.0000157828
},
ft: {
m: 0.3048, cm: 30.48, mm: 304.8, km: 0.0003048, inch: 12, mi: 0.000189394
},
mi: {
m: 1609.34, cm: 160934, mm: 1609340, km: 1.60934, inch: 63360, ft: 5280
},
// Weight/Mass
kg: {
g: 1000, mg: 1000000, lb: 2.20462, oz: 35.274
},
g: {
kg: 0.001, mg: 1000, lb: 0.00220462, oz: 0.035274
},
mg: {
kg: 0.000001, g: 0.001, lb: 0.00000220462, oz: 0.000035274
},
lb: {
kg: 0.453592, g: 453.592, mg: 453592, oz: 16
},
oz: {
kg: 0.0283495, g: 28.3495, mg: 28349.5, lb: 0.0625
},
// Volume
l: {
ml: 1000, gal: 0.264172, qt: 1.05669, pt: 2.11338, cup: 4.22675
},
ml: {
l: 0.001, gal: 0.000264172, qt: 0.00105669, pt: 0.00211338, cup: 0.00422675
},
gal: {
l: 3.78541, ml: 3785.41, qt: 4, pt: 8, cup: 16
},
qt: {
l: 0.946353, ml: 946.353, gal: 0.25, pt: 2, cup: 4
},
pt: {
l: 0.473176, ml: 473.176, gal: 0.125, qt: 0.5, cup: 2
},
cup: {
l: 0.236588, ml: 236.588, gal: 0.0625, qt: 0.25, pt: 0.5
},
// Temperature
c: { f: (v) => v * 9 / 5 + 32, k: (v) => v + 273.15 },
f: { c: (v) => (v - 32) * 5 / 9, k: (v) => (v - 32) * 5 / 9 + 273.15 },
k: { c: (v) => v - 273.15, f: (v) => (v - 273.15) * 9 / 5 + 32 },
// Time
s: {
ms: 1000, min: 1 / 60, h: 1 / 3600, day: 1 / 86400
},
ms: {
s: 0.001, min: 0.0000166667, h: 2.77778e-7, day: 1.15741e-8
},
min: {
s: 60, ms: 60000, h: 1 / 60, day: 1 / 1440
},
h: {
s: 3600, ms: 3600000, min: 60, day: 1 / 24
},
day: {
s: 86400, ms: 86400000, min: 1440, h: 24
}
};
// Normalize units to lowercase
sourceUnit = sourceUnit.toLowerCase();
targetUnit = targetUnit.toLowerCase();
// Check if conversion is possible
if (!unitConversions[sourceUnit] || !unitConversions[sourceUnit][targetUnit]) {
throw new Error(`Cannot convert from ${sourceUnit} to ${targetUnit}`);
}
// Perform the conversion
let result;
const conversion = unitConversions[sourceUnit][targetUnit];
if (typeof conversion === 'function') {
// For conversions that require a formula (like temperature)
result = conversion(value);
} else {
// For conversions that use a simple factor
result = value * conversion;
}
// Format the result
return `${result} ${targetUnit}`;
}
/**
* Handle matrix expressions
* @param {string} expr - The expression with matrix operations
* @param {Object} scope - Variables and their values
* @returns {number|Array} - The result of the matrix operation
*/
function handleMatrixExpressions(expr, scope) {
// Handle determinant calculation
if (expr.includes('det(')) {
const matrixMatch = expr.match(/det\(\[(.+)\]\)/);
if (matrixMatch) {
const matrixStr = matrixMatch[1];
// Parse the matrix from the string
const matrix = parseMatrix(matrixStr);
// Calculate the determinant
return calculateDeterminant(matrix);
}
}
// For other matrix operations, we would need a more sophisticated parser
throw new Error('Unsupported matrix operation');
}
/**
* Parse a matrix from a string representation
* @param {string} matrixStr - The string representation of the matrix
* @returns {Array<Array<number>>} - The parsed matrix
*/
function parseMatrix(matrixStr) {
// Split by semicolons to get rows
const rows = matrixStr.split(';');
// Parse each row
return rows.map((row) => {
// Remove brackets and split by commas
const cleanRow = row.trim().replace(/[\[\]]/g, '');
return cleanRow.split(',').map((val) => parseFloat(val.trim()));
});
}
/**
* Calculate the determinant of a matrix
* @param {Array<Array<number>>} matrix - The matrix
* @returns {number} - The determinant
*/
function calculateDeterminant(matrix) {
// Check if it's a square matrix
const n = matrix.length;
if (!matrix.every((row) => row.length === n)) {
throw new Error('Determinant can only be calculated for square matrices');
}
// Base case for 1x1 matrix
if (n === 1) {
return matrix[0][0];
}
// Base case for 2x2 matrix
if (n === 2) {
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
}
// For larger matrices, use cofactor expansion
let det = 0;
for (let j = 0; j < n; j++) {
det += matrix[0][j] * cofactor(matrix, 0, j);
}
return det;
}
/**
* Calculate the cofactor of a matrix element
* @param {Array<Array<number>>} matrix - The matrix
* @param {number} row - The row index
* @param {number} col - The column index
* @returns {number} - The cofactor
*/
function cofactor(matrix, row, col) {
const minor = calculateMinor(matrix, row, col);
return ((row + col) % 2 === 0 ? 1 : -1) * minor;
}
/**
* Calculate the minor of a matrix element
* @param {Array<Array<number>>} matrix - The matrix
* @param {number} row - The row to exclude
* @param {number} col - The column to exclude
* @returns {number} - The minor determinant
*/
function calculateMinor(matrix, row, col) {
// Create a submatrix by excluding the specified row and column
const submatrix = [];
const n = matrix.length;
for (let i = 0; i < n; i++) {
if (i === row) continue;
const newRow = [];
for (let j = 0; j < n; j++) {
if (j === col) continue;
newRow.push(matrix[i][j]);
}
submatrix.push(newRow);
}
// Calculate the determinant of the submatrix
return calculateDeterminant(submatrix);
}
/**
* Handle complex number expressions
* @param {string} expr - The expression with complex numbers
* @param {Object} scope - Variables and their values
* @returns {Complex} - The result as a complex number
*/
function handleComplexExpression(expr, scope) {
// Replace 'i' with Complex.I for evaluation
const jsExpr = expr.replace(/([0-9])i/g, '$1*I').replace(/i/g, 'I');
// Create a scope with Complex.I
const complexScope = { ...scope, I: new Complex(0, 1) };
// Evaluate the expression
const result = evaluateStandardExpression(jsExpr, complexScope);
// Convert the result to a string if it's a complex number
if (result instanceof Complex) {
return result;
}
return result;
}
/**
* Handle expressions with degrees
* @param {string} expr - The expression with degrees
* @param {Object} scope - Variables and their values
* @returns {number} - The result
*/
function handleDegreeExpressions(expr, scope) {
// Replace 'deg' with conversion to radians
const radianExpr = expr.replace(/([\d.]+)\s*deg/g, (match, p1) => `(${p1} * Math.PI / 180)`);
// Evaluate the modified expression
return evaluateStandardExpression(radianExpr, scope);
}
/**
* Evaluate a standard mathematical expression
* @param {string} expr - The expression to evaluate
* @param {Object} scope - Variables and their values
* @returns {number|Complex|Array} - The result of the evaluation
*/
function evaluateStandardExpression(expr, scope) {
// Replace mathematical functions with their JavaScript equivalents
const jsExpr = expr
.replace(/\^/g, '**') // Replace ^ with **
.replace(/sin\(/g, 'Math.sin(') // Replace sin with Math.sin
.replace(/cos\(/g, 'Math.cos(') // Replace cos with Math.cos
.replace(/tan\(/g, 'Math.tan(') // Replace tan with Math.tan
.replace(/asin\(/g, 'Math.asin(') // Replace asin with Math.asin
.replace(/acos\(/g, 'Math.acos(') // Replace acos with Math.acos
.replace(/atan\(/g, 'Math.atan(') // Replace atan with Math.atan
.replace(/atan2\(/g, 'Math.atan2(') // Replace atan2 with Math.atan2
.replace(/sqrt\(/g, 'Math.sqrt(') // Replace sqrt with Math.sqrt
.replace(/abs\(/g, 'Math.abs(') // Replace abs with Math.abs
.replace(/log\(/g, 'Math.log(') // Replace log with Math.log
.replace(/exp\(/g, 'Math.exp(') // Replace exp with Math.exp
.replace(/pi/gi, 'Math.PI') // Replace pi with Math.PI
.replace(/e(?![a-zA-Z])/g, 'Math.E'); // Replace e with Math.E (but not in variable names)
// Create a function with the scope variables as parameters
const scopeKeys = Object.keys(scope);
const scopeValues = scopeKeys.map((key) => scope[key]);
// Create and evaluate the function
try {
const func = new Function(...scopeKeys, `return ${jsExpr};`);
return func(...scopeValues);
} catch (error) {
throw new Error(`Error evaluating expression: ${error.message}`);
}
}
/**
* Parse a string into a mathematical expression
* @param {string} expr - The expression string
* @returns {Function} - A function that evaluates the expression
*/
expression.parse = function (expr) {
// Create a function that evaluates the expression with the given scope
return function (scope = {}) {
return expression.evaluate(expr, scope);
};
};
/**
* Compile a string into an optimized function
* @param {string} expr - The expression string
* @returns {Function} - An optimized function that evaluates the expression
*/
expression.compile = function (expr) {
// This is a simplified implementation
// In a full implementation, we would parse the expression into an AST
// and generate optimized code
return expression.parse(expr);
};
/**
* Symbolically derive an expression
* @param {string} expr - The expression to differentiate
* @param {string} variable - The variable to differentiate with respect to
* @returns {string} - The derivative expression
*/
expression.derivative = function (expr, variable) {
// This is a simplified implementation of symbolic differentiation
// In a full implementation, we would parse the expression into an AST
// and apply differentiation rules
// Handle some basic cases
// Constant rule: d/dx(c) = 0
if (!expr.includes(variable)) {
return '0';
}
// Power rule: d/dx(x^n) = n*x^(n-1)
const powerMatch = expr.match(new RegExp(`${variable}\\^(\\d+)`));
if (powerMatch && expr === `${variable}^${powerMatch[1]}`) {
const power = parseInt(powerMatch[1]);
if (power === 0) {
return '0';
} if (power === 1) {
return '1';
}
return `${power}*${variable}^${power - 1}`;
}
// Linear rule: d/dx(x) = 1
if (expr === variable) {
return '1';
}
// Sum rule: d/dx(f(x) + g(x)) = f'(x) + g'(x)
if (expr.includes('+')) {
const terms = expr.split('+').map((term) => term.trim());
const derivatives = terms.map((term) => expression.derivative(term, variable));
return derivatives.join(' + ');
}
// Difference rule: d/dx(f(x) - g(x)) = f'(x) - g'(x)
if (expr.includes('-')) {
const terms = expr.split('-').map((term) => term.trim());
const derivatives = terms.map((term) => expression.derivative(term, variable));
return derivatives.join(' - ');
}
// Product rule: d/dx(f(x)*g(x)) = f'(x)*g(x) + f(x)*g'(x)
if (expr.includes('*')) {
const factors = expr.split('*').map((factor) => factor.trim());
if (factors.length === 2) {
const f = factors[0];
const g = factors[1];
const df = expression.derivative(f, variable);
const dg = expression.derivative(g, variable);
return `${df}*${g} + ${f}*${dg}`;
}
}
// For more complex expressions, return a placeholder
return `d/d${variable}(${expr})`;
};
module.exports = expression;