mathsteps-experimental-fork
Version:
Step by step math solutions. Experimental Fork
344 lines (328 loc) • 11.3 kB
JavaScript
import { abs, gcd, unequal, sign, multiply, isNegative, divide, larger } from '../../config.js';
import clone from '../../newServices/nodeServices/clone.js';
import Node from '../../node/index.js';
import { ChangeTypes } from '../../types/changeType/ChangeTypes.js';
/* eslint-disable eqeqeq */
const NODE_CONST_ONE = Node.Creator.constant(1);
const NODE_CONST_ZERO = Node.Creator.constant(0);
function _getNodeTopArgs(node) {
let rv = [];
if (node.op == '/') {
if (node.args[0].op == '*')
rv = node.args[0].args;
else
rv = [node.args[0]];
}
else if (node.op == '*') {
rv = node.args;
}
else {
rv = [node];
}
return rv
}
function _getNodeBottomArgs(node) {
let rv = [];
if (node.op == '/') {
if (node.args[1].op == '*')
rv = node.args[1].args;
else
rv = [node.args[1]];
}
return rv
}
function _buildNodeFromArgs(args) {
let rv = null;
if (args.length == 0)
rv = NODE_CONST_ONE;
else if (args.length == 1)
rv = args[0];
else
rv = Node.Creator.operator('*', args);
return rv
}
function _extractRootAndExponentNodes(node) {
const rv = {
root: null,
exponent: null,
};
if (node.op == '^') {
rv.root = node.args[0];
rv.exponent = node.args[1];
}
else {
rv.root = node;
rv.exponent = NODE_CONST_ONE;
}
return rv
}
function _reduceByArgs(topArgs, bottomArgs) {
let reducedTotal = false;
let newNode = null;
let isNegative$1 = null;
let rv = null;
// Possible improvement: optimize it.
const originalTopArgs = topArgs;
const originalBottomArgs = bottomArgs;
topArgs = topArgs.slice();
bottomArgs = bottomArgs.slice();
// Search for reducable top-bottom pairs.
for (const factorTopIdx in topArgs) {
for (const factorBottomIdx in bottomArgs) {
let reduced = false;
let reducedTopNode = null;
let reducedBottomNode = null;
// Load factors pair to scan (top-bottom):
// ... * topFactor * ...
// -------------------------
// ... * bottomFactor * ...
let factorTop = topArgs[factorTopIdx];
let factorBottom = bottomArgs[factorBottomIdx];
if (!factorTop || !factorBottom)
continue
// Handle minus in numerator.
if (Node.Type.isUnaryMinus(factorTop)) {
factorTop = factorTop.args[0];
isNegative$1 = !(isNegative$1 || false);
}
// Handle minus in denominator.
if (Node.Type.isUnaryMinus(factorBottom)) {
factorBottom = factorBottom.args[0];
isNegative$1 = !(isNegative$1 || false);
}
if (factorTop.equals(factorBottom)) {
// Perferct reducable pair found.
// x/x gives 1
reduced = true;
}
else if (Node.Type.kemuIsConstantInteger(factorTop)
&& Node.Type.kemuIsConstantInteger(factorBottom)) {
// c1 / c2
const valueTop = abs(factorTop.value);
const valueBottom = abs(factorBottom.value);
const valueGcd = gcd(valueTop, valueBottom);
if (unequal(valueGcd, 1)) {
// GCD(c1, c2) is not one - we can simplify fraction.
reduced = true;
const signTop = sign(factorTop.value);
const signBottom = sign(factorBottom.value);
const signResult = multiply(signTop, signBottom);
if (isNegative(signResult))
isNegative$1 = !isNegative$1;
// Divide numerator and denominator by GCD.
if (unequal(valueTop, valueGcd))
reducedTopNode = Node.Creator.constant(divide(valueTop, valueGcd));
if (unequal(valueBottom, valueGcd))
reducedBottomNode = Node.Creator.constant(divide(valueBottom, valueGcd));
}
}
else {
const explodedTop = _extractRootAndExponentNodes(factorTop);
const explodedBottom = _extractRootAndExponentNodes(factorBottom);
if (explodedTop.root.equals(explodedBottom.root)
&& (Node.Type.isConstant(explodedTop.exponent))
&& (Node.Type.isConstant(explodedBottom.exponent))) {
reduced = true;
const exponentValueTop = explodedTop.exponent.value;
const exponentValueBottom = explodedBottom.exponent.value;
if (larger(exponentValueTop, exponentValueBottom)) {
// x^2/x gives x
const newExponent = Node.Creator.operator('-', [explodedTop.exponent, explodedBottom.exponent]);
reducedTopNode = Node.Creator.operator('^', [explodedTop.root, newExponent]);
}
else {
// x/x^2 gives 1/x
const newExponent = Node.Creator.operator('-', [explodedBottom.exponent, explodedTop.exponent]);
reducedBottomNode = Node.Creator.operator('^', [explodedBottom.root, newExponent]);
}
}
}
// Don't go on anymore if top-bottom pair matched.
if (reduced) {
topArgs[factorTopIdx] = reducedTopNode;
bottomArgs[factorBottomIdx] = reducedBottomNode;
reducedTotal = true;
}
else {
topArgs[factorTopIdx] = factorTop;
bottomArgs[factorBottomIdx] = factorBottom;
}
}
}
// Build result node if any reduction performed.
if (reducedTotal) {
// Possible improvement: Create altForm only when needed by caller.
// Build alternative form.
const altFormTopArgs = [];
const altFormBottomArgs = [];
topArgs.forEach((unused, idx) => {
if (originalTopArgs[idx] !== topArgs[idx]) {
altFormTopArgs[idx] = clone(originalTopArgs[idx]);
altFormTopArgs[idx].renderCross = true;
}
else {
altFormTopArgs[idx] = originalTopArgs[idx];
}
});
bottomArgs.forEach((unused, idx) => {
if (originalBottomArgs[idx] !== bottomArgs[idx]) {
altFormBottomArgs[idx] = clone(originalBottomArgs[idx]);
altFormBottomArgs[idx].renderCross = true;
}
else {
altFormBottomArgs[idx] = originalBottomArgs[idx];
}
});
// Remove null factors.
const newTopArgs = topArgs.filter(item => item);
let newBottomArgs = bottomArgs.filter(item => item);
// Better render for x/2 like result.
// We want x/2 gives 1/2x
if ((newBottomArgs.length === 1)
&& (newTopArgs.length > 0)
&& Node.Type.isConstant(newBottomArgs[0])
&& !Node.Type.isConstant(newTopArgs[0])) {
// ...
// --- gives 1/n * ...
// n
newTopArgs.unshift(Node.Creator.operator('/', [NODE_CONST_ONE, newBottomArgs[0]]));
newBottomArgs = [];
}
// Build final node after cross-out.
if (newBottomArgs.length > 0) {
const topNode = _buildNodeFromArgs(newTopArgs);
const bottomNode = _buildNodeFromArgs(newBottomArgs);
newNode = Node.Creator.operator('/', [topNode, bottomNode]);
}
else {
newNode = _buildNodeFromArgs(newTopArgs);
}
// Keep sign for: -x/x like.
if (isNegative$1)
newNode = Node.Creator.unaryMinus(newNode);
// Build result object.
rv = {
changeType: ChangeTypes.CANCEL_TERMS_FOR_FRACTION,
rootNode: newNode,
altForms: [
{
altFormType: 'aftCancelTermsCrossOutInfo',
node: Node.Creator.operator('/', [
Node.Creator.operator('*', altFormTopArgs),
Node.Creator.operator('*', altFormBottomArgs),
]),
},
],
};
}
return rv
}
function _crossNode(node) {
node.renderCross = true;
if (Node.Type.isUnaryMinus(node))
node.args[0].renderCross = true;
return node
}
function cancelTerms(node_) {
if (!node_.op || !node_.args) // Don't think this should happen
return null
const node = clone(node_); // Clone is required for some reason.
let rv = null;
if (node.op == '*') {
const leftNode = node.args[0];
const rightNode = node.args[1];
if ((leftNode.op == '/') || (rightNode.op == '/')) {
// Collect factors from both sides.
const leftTopArgs = _getNodeTopArgs(leftNode);
const leftBottomArgs = _getNodeBottomArgs(leftNode);
const rightTopArgs = _getNodeTopArgs(rightNode);
const rightBottomArgs = _getNodeBottomArgs(rightNode);
const topArgs = leftTopArgs.concat(rightTopArgs);
const bottomArgs = leftBottomArgs.concat(rightBottomArgs);
rv = _reduceByArgs(topArgs, bottomArgs);
}
if (rv == null) {
// x * (-y) * z gives -x * y * z
let isNegative = null;
const newArgs = [];
node.args.forEach((oneArg, idx) => {
if ((idx > 0) && Node.Type.isUnaryMinus(oneArg, true)) {
isNegative = !(isNegative || false);
oneArg = oneArg.args[0];
}
newArgs.push(oneArg);
});
if (isNegative !== null) {
rv = {
changeType: ChangeTypes.SIMPLIFY_SIGNS,
rootNode: Node.Creator.unaryMinus(Node.Creator.operator('*', newArgs)),
};
}
}
}
else if (node.op === '/') {
const topArgs = _getNodeTopArgs(node);
const bottomArgs = _getNodeBottomArgs(node);
rv = _reduceByArgs(topArgs, bottomArgs);
}
else if (node.op === '+') {
// ... + a + b + c + ...
// Possible improvement: Optimize it.
const altFormTerms = node.args.slice();
const newTerms = [];
for (let i = 0; i < altFormTerms.length; i++) {
// Fetch next term.
const nodeX = altFormTerms[i];
if (!nodeX.renderCross) {
// Search for matched x - x like pair (gives 0).
let isCancelledOut = false;
for (let j = i + 1; j < altFormTerms.length; j++) {
// ... x + ... + y + ...
const nodeY = altFormTerms[j];
if (!nodeY.renderCross) {
if ((Node.Type.isUnaryMinus(nodeY) && nodeX.equals(nodeY.args[0]))
|| (Node.Type.isUnaryMinus(nodeX) && nodeY.equals(nodeX.args[0]))) {
// ... + x + ... - x ...
// ^^^ ^^^
// or
// ... - x + ... + x ...
// ^^^ ^^^
altFormTerms[j] = _crossNode(clone(nodeY));
isCancelledOut = true;
break
}
}
}
// Update terms if needed.
if (isCancelledOut)
_crossNode(nodeX);
else
newTerms.push(nodeX);
}
}
// Build result object if changed.
if (newTerms.length !== node.args.length) {
let newNode = NODE_CONST_ZERO;
if (newTerms.length == 1) {
// x
newNode = newTerms[0];
}
else if (newTerms.length > 1) {
// a + b + c + ...
newNode = Node.Creator.operator('+', newTerms);
}
rv = {
changeType: ChangeTypes.CANCEL_TERMS_FOR_ADDITION,
rootNode: newNode,
altForms: [
{
altFormType: 'aftCancelTermsCrossOutInfo',
node: Node.Creator.operator('+', altFormTerms),
},
],
};
}
}
return rv
}
export { cancelTerms as default };