UNPKG

mathsteps-experimental-fork

Version:

Step by step math solutions. Experimental Fork

200 lines (191 loc) 6.98 kB
import clone from '../newServices/nodeServices/clone.js'; import { parseText } from '../newServices/nodeServices/parseText.js'; import checks from '../checks/index.js'; import { ChangeTypes } from '../types/changeType/ChangeTypes.js'; import { printAscii } from '../util/print.js'; import { getAllRuleResponsesForNextStep } from './getAllRuleResponsesForNextStep.js'; import { kemuFlatten, kemuNormalizeConstantNodes } from './kemuSimplifyCommonServices.js'; import _kemuSortArgs from './kemuSortArgs.js'; import { POOLED_TOGETHER_POOL_OF_RULES } from './theIndexRulePool.js'; /* eslint-disable no-throw-literal */ const MAX_STEP_COUNT = 64; function _applyRulesOnChildNode(fct, nodeBox, nodeIdx, simplifyCtx, nodeParent) { let isChanged = false; if (simplifyCtx.iterIdx < MAX_STEP_COUNT) { // Apply simplify function on current node. const node = nodeBox[nodeIdx]; const status = fct(node, simplifyCtx, nodeParent); if (status) { // Current node changed. // Report simplify step. isChanged = true; nodeBox[nodeIdx] = status.rootNode; // Track last changed node. // We'll start next simplify step from this node. simplifyCtx.lastChangedNodeBox = nodeBox; simplifyCtx.lastChangedNodeIdx = nodeIdx; simplifyCtx.lastChangedNodeParent = nodeParent; simplifyCtx.onStepCb(status); } else if (node.args) { // Nothing changed on current node. // Go into child nodes recursively. for (const argIdx in node.args) isChanged |= _applyRulesOnChildNode(fct, node.args, argIdx, simplifyCtx, node); } } return isChanged } // Given a mathjs expression node, steps through simplifying the expression. // Returns a list of details about each step. function stepThrough(node, options = {}) { if (options.isDebugMode) console.log(`\n\nSimplifying: ${printAscii(node)}`); // // Pre-process node. // node = kemuFlatten(node); // Add hard-coded first step with original expression. if (options.onStepCb) { options.onStepCb({ changeType: ChangeTypes.KEMU_ORIGINAL_EXPRESSION, rootNode: clone(node), }); } if (!checks.hasUnsupportedNodes(node)) { // // Set-up simplify context passed along all steps. // const simplifyCtx = { iterIdx: 0, rootNode: node, expressionCtx: options.expressionCtx, // Callback called when any part of expression (node) changed. // We use it to track/collect steps. onStepCb: (stepMeta) => { // Validate step fields. if (!stepMeta.changeType) throw 'missing change type' if (!stepMeta.rootNode) throw 'missing root node' if (options.isDebugMode) logSteps(stepMeta); simplifyCtx.rootNode = kemuFlatten(simplifyCtx.rootNode); simplifyCtx.rootNode = kemuNormalizeConstantNodes(simplifyCtx.rootNode); simplifyCtx.iterIdx++; // Possible improvement: Optimize it. if (stepMeta.altForms) ; stepMeta.rootNode = clone(simplifyCtx.rootNode); // eslint-disable-next-line no-use-before-define oldNode = stepMeta.rootNode; if (options.onStepCb) options.onStepCb(stepMeta); }, }; // // Apply simplify rules to the expression. // const originalNode = clone(node); let oldNode = originalNode; let goOn = true; let isShuffled = false; let expressionAsTextAfterShuffle = null; // Step through the math expression until nothing changes. while (goOn) { goOn = false; // Process last changed node or root. const nodeBox = simplifyCtx.lastChangedNodeBox || simplifyCtx; const nodeIdx = simplifyCtx.lastChangedNodeIdx || 'rootNode'; const nodeParent = simplifyCtx.lastChangedNodeParent; // Apply common rules first. for (const ruleIdx in POOLED_TOGETHER_POOL_OF_RULES) { if (options[`disable${ruleIdx}`]) continue const fct = POOLED_TOGETHER_POOL_OF_RULES[ruleIdx]; if (_applyRulesOnChildNode(fct, nodeBox, nodeIdx, simplifyCtx, nodeParent)) { goOn = true; isShuffled = false; break } } // Back to root if child node went to dead-end. if (!goOn && simplifyCtx.lastChangedNodeBox) { // Reset current node to the root. goOn = true; simplifyCtx.lastChangedNodeBox = null; simplifyCtx.lastChangedNodeIdx = null; simplifyCtx.lastChangedNodeParent = null; } // Try refactor whole expression from scratch if still dead-end. if (!goOn && !isShuffled) { const exprAsText = printAscii(simplifyCtx.rootNode); if (expressionAsTextAfterShuffle !== exprAsText) { // Try shuffle only once after each dead-end. isShuffled = true; goOn = true; // Reset current node to the root. simplifyCtx.lastChangedNodeBox = null; simplifyCtx.lastChangedNodeIdx = null; simplifyCtx.lastChangedNodeParent = null; simplifyCtx.rootNode = parseText(exprAsText); expressionAsTextAfterShuffle = exprAsText; } } // Limit iterations to avoid potentially hung-up. if (simplifyCtx.iterIdx === MAX_STEP_COUNT) { console.error(`Math error: Potential infinite loop for expression: ${printAscii(originalNode)}`); goOn = false; } } // // Post-process node. // node = oldNode; // Possible improvement: Optimize it. const newNode = _kemuSortArgs(clone(oldNode)); const oldNodeAsText = printAscii(oldNode); const newNodeAsText = printAscii(newNode); if (oldNodeAsText !== newNodeAsText) { if (options.onStepCb) { options.onStepCb({ changeType: ChangeTypes.REARRANGE_COEFF, rootNode: newNode, }); } node = newNode; } } return node } function logSteps(nodeStatus) { console.log(nodeStatus.changeType); console.log(printAscii(nodeStatus.rootNode)); if (nodeStatus.substeps.length > 0) { console.log('substeps: '); nodeStatus.substeps.forEach((substep, idx) => { console.log('...', idx, '|', printAscii(substep.rootNode)); }); } } const oldApi = function (node, options = {}) { const steps = []; stepThrough(node, { isDebugMode: options.isDebugMode, expressionCtx: options.expressionCtx, onStepCb: (oneStep) => { steps.push(oneStep); }, }); return steps }; const newApi = function (node, options = {}) { if (options.getAllNextStepPossibilities) return getAllRuleResponsesForNextStep(node, options) else return stepThrough(node, options) }; const stepThrough$1 = { oldApi, newApi, }; export { stepThrough$1 as default, newApi, oldApi };