mathsteps-experimental-fork
Version:
Step by step math solutions. Experimental Fork
352 lines (323 loc) • 10.5 kB
JavaScript
import math from '../index.js';
import Node from '../node/index.js';
import stepThrough from '../simplifyExpression/index.js';
import simplify from '../simplifyExpression/simplifyCore.js';
import { printAscii } from '../util/print.js';
import EquationTerm from './EquationTerm.js';
/* eslint-disable node/prefer-global/process,prefer-spread,prefer-rest-params */
const NODE_FALSE = Node.Creator.symbol('false');
const MAX_ITERATIONS = 20;
class Equation {
/**
* @param {{ leftNode?: any; rightNode?: any; comparator?: any; unknownVariable: any; equationAsText?: any; parent?: any; onStepCb?: any; id?: any; }} options
*/
constructor(options) {
// TODO: Validate input.
let comparator = null;
let leftNode = null;
let rightNode = null;
if (options.equationAsText) {
// Create equation from string e.g. '2x=3'
// Possible improvement: Handle inequalities: <, >, <, =, >=.
comparator = '=';
const text = options.equationAsText;
const separatorIdx = text.indexOf(comparator);
const leftAsText = text.substr(0, separatorIdx);
const rightAsText = text.substr(separatorIdx + 1);
leftNode = math.parseText(leftAsText);
rightNode = math.parseText(rightAsText);
}
else {
// Create equation from left/right nodes.
comparator = options.comparator;
leftNode = options.leftNode;
rightNode = options.rightNode;
}
this.parent = options.parent;
this.unknownVariable = options.unknownVariable;
this.onStepCb = options.onStepCb || function () { };
this.id = options.id || 'eq';
this.comparator = comparator;
this.left = this._parseNode(leftNode);
this.right = this._parseNode(rightNode);
// Possible improvement: Move solutions stuff to another class?
this.steps = [];
this.solutions = null;
}
toString() {
const rv = `${math.print(this.left.node)
} ${
this.comparator
} ${
math.print(this.right.node)}`;
return rv
}
getAsTeX() {
const rv = `${math.printAsTeX(this.left.node)
} ${
this.comparator
} ${
math.printAsTeX(this.right.node)}`;
return rv
}
getScheme() {
const rv = this.left.toString()
+ this.comparator
+ this.right.toString();
return rv
}
_log() {
}
_logStep(msg, showRepeatedSteps = true, ext = {}) {
// TODO: Optimize it.
if (msg) {
const columnEq = this.toString().padEnd(this.logWidthForEquation);
// eslint-disable-next-line eqeqeq
if (showRepeatedSteps || (columnEq != this.logLastColumnEq)) {
const columnMsg = msg.padEnd(this.logWidthForStepName);
this.getScheme();
// Possible improvement: Better column width adjust.
this.logWidthForStepName = Math.max(this.logWidthForStepName, columnMsg.length);
this.logWidthForEquation = Math.max(this.logWidthForEquation, columnEq.length);
this.logLastColumnEq = columnEq;
this.onStepCb({
stepId: msg,
equation: this,
ext,
});
}
}
}
_unflattenNode(node) {
// Possible improvement: Move to better place?
// Possible improvement: Optimize it.
return math.parseText(printAscii(node))
}
_simplifySide(side) {
let rv = side;
const steps = stepThrough.oldApi(side.node);
if (steps.length > 0)
rv = this._parseNode(steps[steps.length - 1].rootNode);
rv.simplifyHistory = steps;
return rv
}
_parseNode(node) {
let rv = null;
// TODO: Review flatten/unflatten mess.
node = this._unflattenNode(node);
if (!Node.Type.doesContainSymbol(node, this.unknownVariable)) {
// Node does not contain unknown variable.
// Treat as constant
rv = EquationTerm.createConstant(node);
}
else {
// Node contains unknown variable.
// Dispatch node type.
if (Node.Type.isSymbol(node)) {
// x = W1(x)
rv = EquationTerm.createPolynomial(node, 1);
}
else if (Node.Type.isFunction(node)) {
// Known function.
// f(x)
rv = EquationTerm.createFunction(node);
}
else if (Node.Type.isUnaryMinus(node)) {
// -f(x)
rv = EquationTerm.neg(this._parseNode(node.args[0]));
}
else if (Node.Type.isOperator(node)) {
// Complex node.
// Perform arithmetic on child equation terms recursively.
if (node.args.length === 2) {
const a = this._parseNode(node.args[0]);
const b = this._parseNode(node.args[1]);
switch (node.op) {
case '+':
rv = EquationTerm.add(a, b);
break
case '-':
rv = EquationTerm.sub(a, b);
break
case '*':
rv = EquationTerm.mul(a, b);
break
case '/':
rv = EquationTerm.div(a, b);
break
case '^':
rv = EquationTerm.pow(a, b);
break
}
}
else {
throw new Error(`two many operands (${node.args.length}): ${node}`)
}
}
}
if (rv == null)
throw new Error(`unhandled node (${node.op}): ${node}`)
return rv
}
swapSides() {
this._logStep('swap_sides')
// Swap left and right nodes.
// eslint-disable-next-line no-sequences,no-unexpected-multiline
[this.left, this.right] = [this.right, this.left];
}
simplifyLeft() {
this.left = this._simplifySide(this.left);
this._logStep('simplify_left', false);
}
simplifyRight() {
this.right = this._simplifySide(this.right);
this._logStep('simplify_right', false);
}
applyStep(stepName, newNodes, ext) {
if (stepName === 'solution') {
this.applySolution(newNodes.items);
}
else {
// TODO: Handle comparator change.
const newLeftNode = newNodes.args[0];
const newRightNode = newNodes.args[1];
this.left = this._parseNode(newLeftNode);
this.right = this._parseNode(newRightNode);
this._logStep(stepName, true, ext);
}
}
applyRules(poolOfRules, options = {}) {
let goOn = true;
let iterIdx = 0;
const simplifyOptions = {
stopOnFirstStep: true,
unknownVariable: this.unknownVariable,
};
while (goOn) {
this._logStep('...', false);
// Simplify both side before each step if needed.
if (options && options.simplifyBeforeEachStepEnabled) {
this.simplifyLeft();
this.simplifyRight();
}
// TODO: Handle other comparators.
const eqNode = new math.FunctionNode('EQ', [this.left.node, this.right.node]);
const steps = simplify(eqNode, poolOfRules, null, simplifyOptions);
if (steps.length > 0) {
// Possible improvement: Better step data customization.
const stepData = {
c: steps[0].matches.placeholders.a,
fx: steps[0].matches.placeholders.fx,
};
this.applyStep(steps[0].ruleApplied.id, steps[0].nodeAfter, stepData);
// Simplify both sides aftere each step if needed.
if (options && options.simplifyAfterEachStepEnabled) {
this.simplifyLeft();
this.simplifyRight();
}
if (this.isSolved()) {
// Equation is solved.
// Don't go on anymore.
goOn = false;
}
}
else {
// No any rules applied.
// We stucks without solution for now.
goOn = false;
}
// Go to next iteration.
iterIdx++;
if (iterIdx > MAX_ITERATIONS)
throw new Error('too many steps')
}
}
_isOkAsSolution(node) {
let rv = true;
if (Node.Type.isNthRoot(node)
&& Node.Type.kemuIsConstantNegative(node.args[0])) {
// Skip sqrt(-n) like.
// Possible improvement: Handle complex numbers?
rv = false;
}
else if (Node.Type.isNthRoot(node)
&& Node.Type.isUnaryMinus(node.args[0])
&& Node.Type.kemuIsConstantPositive(node.args[0].args[0])) {
// Skip sqrt(-n) like.
// Possible improvement: Handle complex numbers?
rv = false;
}
else if (node.args) {
for (const idx in node.args) {
if (!this._isOkAsSolution(node.args[idx])) {
// At least one child-node can't be a solution.
// Don't go on anymore.
rv = false;
break
}
}
}
return rv
}
applySolution(solutions) {
const arrayOfSolutions = [];
const arrayOfSolutionsSteps = [];
solutions.forEach((oneItem) => {
const steps = stepThrough.oldApi(oneItem);
if (steps.length > 0)
oneItem = steps[steps.length - 1].rootNode;
if (this._isOkAsSolution(oneItem)) {
if (!oneItem.equals(NODE_FALSE)) {
arrayOfSolutions.push(oneItem);
arrayOfSolutionsSteps.push(steps);
}
}
else {
this._log('skipped solution |', math.print(oneItem));
this._logStep('skipped_solution', true, { skippedSolution: oneItem });
}
});
// Apply new solutions.
// Distinghuish between unsolved and no-solutions states.
if (arrayOfSolutions.length === 0)
this.solutions = [NODE_FALSE];
else
this.solutions = arrayOfSolutions;
this._logStep('solution', true, { arrayOfSolutions, arrayOfSolutionsSteps });
}
getSolutions() {
return this.solutions
}
getSolutionsAsText() {
let rv = `${this.unknownVariable} = `;
if (this.isSolved()) {
// Equation is solved, go on.
if (this.solutions.length === 1) {
// There is only one solution.
rv += math.print(this.solutions[0]);
}
else {
// There is more than one solution, render them one-by-one.
let sep = '';
rv += '[';
this.solutions.forEach((oneSolution) => {
rv += sep + math.print(oneSolution);
sep = ', ';
});
rv += ']';
}
}
else {
// Not solved - solution is unknown.
rv += '?';
}
return rv
}
isSolved() {
return (this.solutions != null)
}
getId() {
return this.id
}
}
export { Equation as default };