mathsteps
Version:
Step by step math solutions
375 lines (344 loc) • 14.6 kB
JavaScript
const clone = require('../../util/clone');
const divideByGCD = require('./divideByGCD');
const print = require('../../util/print');
const ChangeTypes = require('../../ChangeTypes');
const Negative = require('../../Negative');
const Node = require('../../node');
// Used for cancelTerms to return a (possibly updated) numerator and denominator
class CancelOutStatus {
constructor(numerator, denominator, hasChanged=false) {
this.numerator = numerator;
this.denominator = denominator;
this.hasChanged = hasChanged;
}
}
// Cancels like terms in a fraction node
// e.g. (2x^2 * 5) / 2x^2 => 5 / 1
// Returns a Node.Status object
function cancelLikeTerms(node) {
if (!Node.Type.isOperator(node) || node.op !== '/') {
return Node.Status.noChange(node);
}
let newNode = clone(node);
const numerator = newNode.args[0];
const denominator = newNode.args[1];
// case 1: neither the numerator or denominator is a multiplication of terms
if (!isMultiplicationOfTerms(numerator) &&
!isMultiplicationOfTerms(denominator)) {
const cancelStatus = cancelTerms(numerator, denominator);
if (cancelStatus.hasChanged) {
newNode.args[0] = cancelStatus.numerator || Node.Creator.constant(1);
if (cancelStatus.denominator) {
newNode.args[1] = cancelStatus.denominator;
}
else {
// If we cancelled out the denominator, the node is now its numerator
// e.g. (2x*y) / 2x => y (note y isn't a fraction)
newNode = newNode.args[0];
}
return Node.Status.nodeChanged(
ChangeTypes.CANCEL_TERMS, node, newNode);
}
else {
return Node.Status.noChange(node);
}
}
// case 2: numerator is a multiplication of terms and denominator is not
// e.g. (2x^2 * 5) / 2x^2 => 5 / 1
// e.g. (x^2*y) / x => x^(2 - 1) * y (<-- note that the denominator goes
// away because we always adjust the exponent in the numerator)
else if (isMultiplicationOfTerms(numerator) &&
!isMultiplicationOfTerms(denominator)) {
const numeratorArgs = Node.Type.isParenthesis(numerator) ?
numerator.content.args : numerator.args;
for (let i = 0; i < numeratorArgs.length; i++) {
const cancelStatus = cancelTerms(numeratorArgs[i], denominator);
if (cancelStatus.hasChanged) {
if (cancelStatus.numerator) {
numeratorArgs[i] = cancelStatus.numerator;
}
// if the cancelling out got rid of the numerator node, we remove it from
// the list
else {
numeratorArgs.splice(i, 1);
// if the numerator is now a "multiplication" of only one term,
// change it to just that term
if (numeratorArgs.length === 1) {
newNode.args[0] = numeratorArgs[0];
}
}
if (cancelStatus.denominator) {
newNode.args[1] = cancelStatus.denominator;
}
else {
// If we cancelled out the denominator, the node is now its numerator
// e.g. (2x*y) / 2x => y (note y isn't a fraction)
newNode = newNode.args[0];
}
return Node.Status.nodeChanged(
ChangeTypes.CANCEL_TERMS, node, newNode);
}
}
return Node.Status.noChange(node);
}
// case 3: denominator is a multiplication of terms and numerator is not
// e.g. 2x^2 / (2x^2 * 5) => 1 / 5
// e.g. x / (x^2*y) => x^(1-2) / y
else if (isMultiplicationOfTerms(denominator)
&& !isMultiplicationOfTerms(numerator)) {
const denominatorArgs = Node.Type.isParenthesis(denominator) ?
denominator.content.args : denominator.args;
for (let i = 0; i < denominatorArgs.length; i++) {
const cancelStatus = cancelTerms(numerator, denominatorArgs[i]);
if (cancelStatus.hasChanged) {
newNode.args[0] = cancelStatus.numerator || Node.Creator.constant(1);
if (cancelStatus.denominator) {
denominatorArgs[i] = cancelStatus.denominator;
}
// if the cancelling out got rid of the denominator node, we remove it
// from the list
else {
denominatorArgs.splice(i, 1);
// if the denominator is now a "multiplication" of only one term,
// change it to just that term
if (denominatorArgs.length === 1) {
newNode.args[1] = denominatorArgs[0];
}
}
return Node.Status.nodeChanged(
ChangeTypes.CANCEL_TERMS, node, newNode);
}
}
return Node.Status.noChange(node);
}
// case 4: the numerator and denominator are both multiplications of terms
else {
const numeratorArgs = Node.Type.isParenthesis(numerator) ?
numerator.content.args : numerator.args;
const denominatorArgs = Node.Type.isParenthesis(denominator) ?
denominator.content.args : denominator.args;
for (let i = 0; i < numeratorArgs.length; i++) {
for (let j = 0; j < denominatorArgs.length; j++) {
const cancelStatus = cancelTerms(numeratorArgs[i], denominatorArgs[j]);
if (cancelStatus.hasChanged) {
if (cancelStatus.numerator) {
numeratorArgs[i] = cancelStatus.numerator;
}
// if the cancelling out got rid of the numerator node, we remove it
// from the list
else {
numeratorArgs.splice(i, 1);
// if the numerator is now a "multiplication" of only one term,
// change it to just that term
if (numeratorArgs.length === 1) {
newNode.args[0] = numeratorArgs[0];
}
}
if (cancelStatus.denominator) {
denominatorArgs[j] = cancelStatus.denominator;
}
// if the cancelling out got rid of the denominator node, we remove it
// from the list
else {
denominatorArgs.splice(j, 1);
// if the denominator is now a "multiplication" of only one term,
// change it to just that term
if (denominatorArgs.length === 1) {
newNode.args[1] = denominatorArgs[0];
}
}
return Node.Status.nodeChanged(
ChangeTypes.CANCEL_TERMS, node, newNode);
}
}
}
return Node.Status.noChange(node);
}
}
// Given a term in the numerator and a term in the denominator, cancels out
// like terms if possible. See the cases below for possible things that can
// be cancelled out and how they are cancelled out.
// Returns the new nodes for numerator and denominator with the common terms
// removed. If the entire numerator or denominator is cancelled out, it is
// returned as null. e.g. 4, 4x => null, x
function cancelTerms(numerator, denominator) {
// Deal with unary minuses by recursing on the argument
if (Node.Type.isUnaryMinus(numerator)) {
const cancelStatus = cancelTerms(numerator.args[0], denominator);
if (!cancelStatus.numerator) {
numerator = Node.Creator.constant(-1);
}
else if (Negative.isNegative(cancelStatus.numerator)) {
numerator = Negative.negate(cancelStatus.numerator);
}
else {
numerator.args[0] = cancelStatus.numerator;
}
denominator = cancelTerms.denominator;
return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged);
}
if (Node.Type.isUnaryMinus(denominator)) {
const cancelStatus = cancelTerms(numerator, denominator.args[0]);
numerator = cancelStatus.numerator;
if (cancelStatus.denominator) {
denominator.args[0] = cancelStatus.denominator;
}
else {
denominator = cancelStatus.denominator;
if (numerator) {
numerator = Negative.negate(numerator);
}
else {
numerator = Node.Creator.constant(-1);
}
}
return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged);
}
// Deal with parens similarily
if (Node.Type.isParenthesis(numerator)) {
const cancelStatus = cancelTerms(numerator.content, denominator);
if (cancelStatus.numerator) {
numerator.content = cancelStatus.numerator;
}
else {
// if the numerator was cancelled out, the numerator should be null
// and not null in parens.
numerator = cancelStatus.numerator;
}
denominator = cancelStatus.denominator;
return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged);
}
if (Node.Type.isParenthesis(denominator)) {
const cancelStatus = cancelTerms(numerator, denominator.content);
if (cancelStatus.denominator) {
denominator.content = cancelStatus.denominator;
}
else {
// if the denominator was cancelled out, the denominator should be null
// and not null in parens.
denominator = cancelStatus.denominator;
}
numerator = cancelStatus.numerator;
return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged);
}
// Now for the term cancelling ----
// case 1: the numerator term and denominator term are the same, so we cancel
// them out. e.g. (x+5)^100 / (x+5)^100 => null / null
if (print.ascii(numerator) === print.ascii(denominator)) {
return new CancelOutStatus(null, null, true);
}
// case 2: they're both exponent nodes with the same base
// e.g. (2x+5)^8 and (2x+5)^2
if (Node.Type.isOperator(numerator, '^') &&
Node.Type.isOperator(denominator, '^') &&
print.ascii(numerator.args[0]) === print.ascii(denominator.args[0])) {
const numeratorExponent = numerator.args[1];
let denominatorExponent = denominator.args[1];
// wrap the denominatorExponent in parens, in case it's complicated.
// If the parens aren't needed, they'll be removed with
// removeUnnecessaryParens at the end of this step.
denominatorExponent = Node.Creator.parenthesis(denominatorExponent);
const newExponent = Node.Creator.parenthesis(
Node.Creator.operator('-', [numeratorExponent, denominatorExponent]));
numerator.args[1] = newExponent;
return new CancelOutStatus(numerator, null, true);
}
// case 3: they're both polynomial terms, check if they have the same symbol
// e.g. 4x^2 / 5x^2 => 4 / 5
// e.g. 4x^3 / 5x^2 => 4x^(3-2) / 5
if (Node.PolynomialTerm.isPolynomialTerm(numerator) &&
Node.PolynomialTerm.isPolynomialTerm(denominator)) {
const numeratorTerm = new Node.PolynomialTerm(numerator);
const denominatorTerm = new Node.PolynomialTerm(denominator);
if (numeratorTerm.getSymbolName() !== denominatorTerm.getSymbolName()) {
return new CancelOutStatus(numerator, denominator);
}
const numeratorExponent = numeratorTerm.getExponentNode(true);
let denominatorExponent = denominatorTerm.getExponentNode(true);
if (print.ascii(numeratorExponent) === print.ascii(denominatorExponent)) {
// note this returns null if there's no coefficient (ie it's 1)
numerator = numeratorTerm.getCoeffNode();
}
else {
// wrap the denominatorExponent in parens, in case it's complicated.
// If the parens aren't needed, they'll be removed with
// removeUnnecessaryParens at the end of this step.
denominatorExponent = Node.Creator.parenthesis(denominatorExponent);
const newExponent = Node.Creator.parenthesis(
Node.Creator.operator('-', [numeratorExponent, denominatorExponent]));
numerator = Node.Creator.polynomialTerm(
numeratorTerm.getSymbolNode(),
newExponent,
numeratorTerm.getCoeffNode());
}
denominator = denominatorTerm.getCoeffNode();
return new CancelOutStatus(numerator, denominator, true);
}
// case 4: the numerator is a constant and denominator is a polynomial term that has a coefficient
// or is multiplication node
// e.g. 2 / 4x -> 1 / 2x
// e.g. ignore cases like: 2 / a and 2 / x^2
if (Node.Type.isConstant(numerator)
&& Node.Type.isOperator(denominator, '*')
&& Node.PolynomialTerm.isPolynomialTerm(denominator)) {
const denominatorTerm = new Node.PolynomialTerm(denominator);
const coeff = denominatorTerm.getCoeffNode();
const variable = denominatorTerm.getSymbolNode();
const exponent = denominatorTerm.getExponentNode();
// simplify a constant fraction (e.g 2 / 4)
const frac = Node.Creator.operator('/', [numerator, coeff]);
let newCoeff = clone(coeff);
const reduceStatus = divideByGCD(frac);
if (!reduceStatus.hasChanged()) {
return new CancelOutStatus(numerator, denominator, false);
}
// Sometimes the fraction reduces to a constant e.g. 6 / 2 -> 3,
// in which case `newCoeff` (the denominator coefficient) should be null
if (Node.Type.isConstant(reduceStatus.newNode)) {
numerator = reduceStatus.newNode;
newCoeff = null;
}
else {
[numerator, newCoeff] = reduceStatus.newNode.args;
}
denominator = Node.Creator.polynomialTerm(variable, exponent, newCoeff);
return new CancelOutStatus(numerator, denominator, true);
}
// case 5: both numerator and denominator are numbers within a more complicated fraction
// e.g. (35 * nthRoot (7)) / (5 * nthRoot(5)) -> (7 * nthRoot(7)) / nthRoot(5)
if (Node.Type.isConstant(numerator) && Node.Type.isConstant(denominator)) {
const frac = Node.Creator.operator('/', [numerator, denominator]);
const reduceStatus = divideByGCD(frac);
if (!reduceStatus.hasChanged()) {
return new CancelOutStatus(numerator, denominator, false);
}
if (Node.Type.isConstant(reduceStatus.newNode)) {
// Denominator is a factor of numerator (e.g 4 / 2 -> 2)
return new CancelOutStatus(reduceStatus.newNode, null, true);
}
// Sometimes the fraction reduces to a constant e.g. 6 / 2 -> 3,
// in which case `newCoeff` (the denominator coefficient) should be null
if (Node.Type.isConstant(reduceStatus.newNode)) {
numerator = reduceStatus.newNode;
denominator = null;
}
else {
[numerator, denominator] = reduceStatus.newNode.args;
}
return new CancelOutStatus(numerator, denominator, true);
}
return new CancelOutStatus(numerator, denominator);
}
// Returns true if node is a multiplication of terms that can be cancelled out
// e.g. 2 * 6^y => true
// e.g. 2 + 6 => false
// e.g. (2 * 6^y) => true
// e.g. 2x^2 => false (polynomial terms are considered as one single term)
function isMultiplicationOfTerms(node) {
if (Node.Type.isParenthesis(node)) {
return isMultiplicationOfTerms(node.content);
}
return (Node.Type.isOperator(node, '*') &&
!Node.PolynomialTerm.isPolynomialTerm(node));
}
module.exports = cancelLikeTerms;