├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── README.md ├── RESOURCES.md ├── ROADMAP.md ├── index.js ├── lib ├── ChangeTypes.js ├── Negative.js ├── Symbols.js ├── TreeSearch.js ├── checks │ ├── canAddLikeTerms.js │ ├── canFindRoots.js │ ├── canMultiplyLikeTermConstantNodes.js │ ├── canMultiplyLikeTermPolynomialNodes.js │ ├── canMultiplyLikeTermsNthRoots.js │ ├── canRearrangeCoefficient.js │ ├── canSimplifyPolynomialTerms.js │ ├── hasUnsupportedNodes.js │ ├── index.js │ ├── isQuadratic.js │ └── resolvesToConstant.js ├── equation │ ├── Equation.js │ ├── Status.js │ └── index.js ├── factor │ ├── ConstantFactors.js │ ├── factorQuadratic.js │ ├── index.js │ └── stepThrough.js ├── node │ ├── Creator.js │ ├── CustomType.js │ ├── MixedNumber.js │ ├── NthRootTerm.js │ ├── PolynomialTerm.js │ ├── Status.js │ ├── Term.js │ ├── Type.js │ └── index.js ├── simplifyExpression │ ├── arithmeticSearch │ │ └── index.js │ ├── basicsSearch │ │ ├── convertMixedNumberToImproperFraction.js │ │ ├── index.js │ │ ├── rearrangeCoefficient.js │ │ ├── reduceExponentByZero.js │ │ ├── reduceMultiplicationByZero.js │ │ ├── reduceZeroDividedByAnything.js │ │ ├── removeAdditionOfZero.js │ │ ├── removeDivisionByOne.js │ │ ├── removeExponentBaseOne.js │ │ ├── removeExponentByOne.js │ │ ├── removeMultiplicationByNegativeOne.js │ │ ├── removeMultiplicationByOne.js │ │ └── simplifyDoubleUnaryMinus.js │ ├── breakUpNumeratorSearch │ │ └── index.js │ ├── collectAndCombineSearch │ │ ├── ConstantOrConstantPower.js │ │ ├── LikeTermCollector.js │ │ ├── addLikeTerms.js │ │ ├── evaluateConstantSum.js │ │ ├── index.js │ │ └── multiplyLikeTerms.js │ ├── distributeSearch │ │ └── index.js │ ├── divisionSearch │ │ └── index.js │ ├── fractionsSearch │ │ ├── addConstantAndFraction.js │ │ ├── addConstantFractions.js │ │ ├── cancelLikeTerms.js │ │ ├── divideByGCD.js │ │ ├── index.js │ │ ├── simplifyFractionSigns.js │ │ └── simplifyPolynomialFraction.js │ ├── functionsSearch │ │ ├── absoluteValue.js │ │ ├── index.js │ │ └── nthRoot.js │ ├── index.js │ ├── multiplyFractionsSearch │ │ └── index.js │ ├── simplify.js │ └── stepThrough.js ├── solveEquation │ ├── EquationOperations.js │ ├── index.js │ └── stepThrough.js └── util │ ├── Util.js │ ├── evaluate.js │ ├── flattenOperands.js │ ├── print.js │ └── removeUnnecessaryParens.js ├── package-lock.json ├── package.json ├── scripts └── git-hooks │ └── pre-commit.sh └── test ├── Negative.test.js ├── Node ├── MixedNumber.test.js ├── NthRootTerm.test.js ├── PolynomialTerm.test.js └── Type.test.js ├── Symbols.test.js ├── TestUtil.js ├── canAddLikeTermPolynomialNodes.test.js ├── canMultiplyLikeTermConstantNodes.test.js ├── canMultiplyLikeTermPolynomialNodes.test.js ├── canRearrangeCoefficient.test.js ├── checks ├── checks.test.js ├── hasUnsupportedNodes.test.js ├── isQuadratic.test.js └── resolvesToConstant.test.js ├── equation.test.js ├── factor ├── ConstantFactors.test.js ├── factor.test.js └── factorQuadratic.test.js ├── simplifyExpression ├── arithmeticSearch │ └── arithmeticSearch.test.js ├── basicsSearch │ ├── convertMixedNumberToImproperFraction.test.js │ ├── rearrangeCoefficient.test.js │ ├── reduceExponentByZero.test.js │ ├── reduceMutliplicationByZero.test.js │ ├── reduceZeroDividedByAnything.test.js │ ├── removeAdditionOfZero.test.js │ ├── removeDivisionByOne.test.js │ ├── removeExponentBaseOne.test.js │ ├── removeExponentByOne.test.js │ ├── removeMultiplicationByNegativeOne.test.js │ ├── removeMultiplicationByOne.test.js │ ├── simplifyDoubleUnaryMinus.test.js │ └── testSimplify.js ├── breakUpNumeratorSearch │ └── breakUpNumeratorSearch.test.js ├── collectAndCombineSearch │ ├── LikeTermCollector.test.js │ ├── collectAndCombineSearch.test.js │ └── evaluateConstantSum.test.js ├── distributeSearch │ └── distributeSearch.test.js ├── divisionSearch │ └── divisionSearch.test.js ├── fractionsSearch │ ├── addConstantAndFraction.test.js │ ├── addConstantFractions.test.js │ ├── cancelLikeTerms.test.js │ ├── divideByGCD.test.js │ ├── simplifyFractionSigns.test.js │ └── simplifyPolynomialFraction.test.js ├── functionsSearch │ ├── absoluteValue.test.js │ └── nthRoot.test.js ├── multiplyFractionsSearch │ └── multiplyFractionsSearch.test.js ├── oneStep.test.js └── simplify.test.js ├── solveEquation └── solveEquation.test.js └── util ├── Util.test.js ├── flattenOperands.test.js ├── print.test.js └── removeUnnecessaryParens.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "plugins": [ 7 | "sort-requires" 8 | ], 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "sourceType": "module" 12 | }, 13 | "globals": { 14 | "require": true, 15 | "module": true, 16 | "describe": true, 17 | "it": true 18 | }, 19 | "rules": { 20 | "indent": [ 21 | "error", 22 | 2 23 | ], 24 | "linebreak-style": [ 25 | "error", 26 | "unix" 27 | ], 28 | "quotes": [ 29 | "error", 30 | "single" 31 | ], 32 | "semi": [ 33 | "error", 34 | "always" 35 | ], 36 | "eqeqeq": [ 37 | "error", 38 | "always" 39 | ], 40 | "prefer-const": [ 41 | "error", { 42 | "destructuring": "any" 43 | } 44 | ], 45 | "no-trailing-spaces": [ 46 | "error" 47 | ], 48 | "object-shorthand": [ 49 | "error", 50 | "always", { 51 | "avoidQuotes": true 52 | } 53 | ], 54 | "brace-style": [ 55 | "error", 56 | "stroustrup", 57 | { 58 | "allowSingleLine": true 59 | } 60 | ], 61 | "sort-requires/sort-requires": [ 62 | "error" 63 | ], 64 | "keyword-spacing": [ 65 | "error" 66 | ], 67 | "strict": [ 68 | "error" 69 | ], 70 | "eol-last": [ 71 | "error", 72 | "always" 73 | ], 74 | "no-multiple-empty-lines": [ 75 | "error", 76 | { 77 | "max": 2, 78 | "maxEOF": 1 79 | } 80 | ], 81 | "no-trailing-spaces": [ 82 | "error" 83 | ] 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-node@v2 9 | with: 10 | node-version: '6' 11 | - name: Install 12 | run: npm install 13 | - name: Lint 14 | run: npm run lint 15 | - name: Test 16 | run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *swp 2 | node_modules 3 | *.log 4 | .DS_Store 5 | .idea 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mathsteps@socratic.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to mathsteps 2 | 3 | #### 🎉 We're excited to have you helping out! Thank you so much for your time 🎉 4 | 5 | ## Contents 6 | 7 | ### Before you get started 8 | 9 | - [Code of Conduct](#code-of-conduct) 10 | - [How (and why) we built mathsteps](#how-and-why-we-built-mathsteps) 11 | - [Expression trees in mathJS](#expression-trees-in-mathjs) 12 | - [Coding conventions](#coding-conventions) 13 | 14 | ### Contributing 15 | 16 | - [Ways to help out](#ways-to-help-out) 17 | - [Creating a pull request](#creating-a-pull-request) 18 | - [Testing](#testing) 19 | 20 | 21 | ## Before you get started 22 | 23 | ### Code of Conduct 24 | 25 | This project adheres to the Contributor Covenant 26 | [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to 27 | uphold this code. Please report unacceptable behavior to mathsteps@socratic.org 28 | 29 | ### How (and why) we built mathsteps 30 | 31 | Read about how and why we built mathsteps on our 32 | [blog](https://blog.socratic.org/stepping-into-math-open-sourcing-our-step-by-step-solver-9b5da066ae36). 33 | 34 | ### Expression trees in mathJS 35 | 36 | Most of this code iterates over expression trees to make step by step 37 | simplifications. We use 38 | [mathJS expresison trees](http://mathjs.org/docs/expressions/expression_trees.html#expression-trees), 39 | which we recommend you learn a bit about. 40 | 41 | There are a few different types of nodes that show up in the tree. This stepper 42 | uses OperationNode, ParenthesisNode, ConstantNode, SymbolNode, and FunctionNode. 43 | You can also read about them on the 44 | [mathJS expressions documentation](http://mathjs.org/docs/expressions/expression_trees.html#nodes). 45 | 46 | Keep in mind when dealing with these trees that child nodes are called different 47 | things depending on the parent node type. For example, operation nodes have 48 | "args" as their children, and parenthesis nodes have a single child called 49 | "content". 50 | 51 | **Tricky catch**: any subtraction in the tree will be converted to an addition, 52 | by negating the number being subtracted, e.g. `2 - 3` would be `2 + -3` in the 53 | tree. This is so that all addition and subtraction is flat. For instance, 54 | `2 + 3 - 5 + 8` would become one addition operation with `2`, `3`, `-5`, and `8` 55 | as its child nodes. This is a common strategy for computer algebra systems but 56 | can be confusing and easy to forget. So at most points in the codebase, there 57 | should be no operators with `-` sign. If you're curious what the code that 58 | modifies subtraction looks like, you can find it in 59 | [flattenOperands.js](/lib/util/flattenOperands.js). 60 | 61 | ### Coding conventions 62 | 63 | mathsteps follows the node.js code style as described 64 | [here](https://github.com/felixge/node-style-guide). 65 | 66 | To lint your code, run `npm run lint .` 67 | 68 | ## Contributing 69 | 70 | ### Ways to help out 71 | 72 | - **Spread the word!** If you think mathsteps is cool, tell your friends! Let 73 | them know they can use this and that they can contribute. 74 | - **Suggest features!** Have an idea for something mathsteps should solve or a 75 | way for it to teach math better? If your idea is not an 76 | [existing issue](https://github.com/socraticorg/mathsteps/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement), 77 | create a new issue with the label "enhancement". 78 | - **Report bugs!** If the bug is not an 79 | [existing issue](https://github.com/socraticorg/mathsteps/issues?q=is%3Aopen+is%3Aissue+label%3Abug), 80 | create a new issue with the label "bug" and provide as many details as you 81 | can, so that someone else can reproduce it. 82 | - **Contribute code!** 83 | We'd love to have more contributors working on this. 84 | Check out the section below with more information on how to contribute, 85 | and feel free to email us at mathsteps@socratic.org with any questions! 86 | 87 | ### Creating a pull request 88 | 89 | We're excited to see your [pull request](https://help.github.com/articles/about-pull-requests/)! 90 | 91 | - If you want to work on something, please comment on the 92 | [related issue](https://github.com/socraticorg/mathsteps/issues) on GitHub 93 | before you get started, so others are aware that you're working on it. If 94 | there's no existing issue for the change you'd like to make, you can 95 | [create a new issue](https://github.com/socraticorg/mathsteps/issues/new). 96 | 97 | - The best issues to work on are [these issues that are not assigned or long term goals](https://github.com/socraticorg/mathsteps/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20-label%3Aassigned%20%20-label%3Aquestion%20%20-label%3A%22longer%20term%20goals%22%20%20-label%3A%22needs%20further%20discussion%22%20) 98 | 99 | - Make sure all the unit tests pass (with `npm test`) before creating the pull 100 | request, and please add your own tests for the changes that you made. If 101 | you're not sure how to add tests, or are confused about why tests are failing, 102 | it's fine to create the pull request first and we'll help you get things 103 | working. 104 | 105 | ### Testing 106 | 107 | - Make sure you properly unit-test your changes. 108 | - Run tests with `npm test` 109 | - Install Git hooks with `npm run setup-hooks`. This will add a pre-commit hook 110 | which makes sure tests are passing and the code is eslint-compliant. 111 | - If you want to see what the expression tree looks like at any point in the 112 | code (for debugging), you can log a `node` as an expression string (e.g. 113 | '2x + 5') with `console.log(print.ascii(node))`, and you can log the full tree 114 | structure with `console.log(JSON.stringify(node, null, 2))` 115 | 116 | 117 | There's lots to be done, lots of students to help, and we're so glad you'll be a 118 | part of this. 119 | 120 | Thanks! ❤️ ❤️ 121 | 122 | _mathsteps team_ 123 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | 4 | ## 2017-10-26, version 0.1.7 5 | 6 | There's been a lot of great changes since the last release, here are the main updates 7 | 8 | Functionality and Teaching Enhancements: 9 | 10 | - new pedagogy for multiply powers integers #153 11 | - exposing the factoring module and adding more coverage #148 12 | - simplify roots of any degree #183 13 | - more cases for cancelling terms #182 14 | - greatest common denominator substep #188 15 | - multiply nthRoots #189 16 | - multiply fractions with parenthesis #185 17 | - remove unnecessary parens before solving equations #205 18 | - multiply denominators with terms #88 19 | - Better sum-product factoring steps #210 20 | 21 | 22 | Bug Fixes 23 | 24 | - fix the check for perfect roots of a constant when there's roundoff error #224 25 | - large negtive number rounding #216 26 | 27 | Other: 28 | 29 | - (code structure) generalizing polynomial terms #190 30 | - latex printing for equations 31 | - added linting rules #222 32 | 33 | 34 | ## 2017-04-03, version 0.1.6 35 | 36 | updated mathjs to incorporate vulnerability patch #149 37 | 38 | Functionality Enhancements: 39 | 40 | - Added factoring support #104 41 | - Fixed #138: Better handling of distribution with fractions. Thanks @lexiross ! 42 | - Fixed #126: Add parens in util > print where necessary. Thanks @Flyr1Q ! 43 | 44 | Bug fixes: 45 | 46 | - Fixed #113: handle exponents on coefficients of polynomial terms. Thanks @shirleymiao ! 47 | - Fixed #111 (nthRoot() existence check). Thanks @shirleymiao ! 48 | 49 | Refactoring + Documentation + other dev enhancements: 50 | 51 | - Fixed #107: Improve our linter. Thanks @Raibaz ! 52 | - Added Travis continuous integration 53 | - Refactor test to use TestUtil. Thanks @nitin42 ! 54 | - Work on #58: Adding missing tests. Thanks @nitin42 ! 55 | 56 | ## 2017-01-29, version 0.1.5 57 | 58 | Reverted #82 (Added script to check the installed node version) and mention 59 | node version requiremnts in the README. 60 | 61 | ## 2017-01-29, version 0.1.4 62 | 63 | Functionality Enhancements: 64 | 65 | - Fixed #39: Add rule to simplify 1^x to 1. Thanks @michaelmior ! 66 | - Fixed #82: Added script to check the installed node version. Thanks @Raibaz ! 67 | 68 | Bug fixes: 69 | 70 | - Fixed #77: bug where oldNode was null on every step. Thanks @hmaurer ! 71 | - Handle unary minus nodes that have an argument that is a parentheses. Thanks 72 | @tkosan ! 73 | 74 | Refactoring + Documentation + other dev enhancements: 75 | 76 | - Fixed #73: replace New Kids on the Block video with one that's not restricted 77 | in most of the world 78 | - Fixed #80: Use object literal property value shorthand. Thanks @cspanda ! 79 | - Fixed #62: Separated basicsSearch simplifications into their own files. Thanks 80 | @Raibaz ! 81 | - Fixed #78: pre-commit hook to run tests and linter before a git commit. Thanks 82 | @hmaurer ! 83 | - Improvements from #44: Added Linting rules. Thanks @biyasbasak ! 84 | - Fixed #91: Refactor isOperator to accept operator parameter. Thanks 85 | @mcarthurgill ! 86 | - Fixed #86: Clean up CONTRIBUTING.md. Thanks @faheel ! 87 | - Fixed #34: Make a helper function getRadicandNode. Thanks @lexiross ! 88 | - Fixed #95: Create RESOURCES.md for people to share relevant software, 89 | projects, and papers 90 | - Fixed #102: Add a complete code example for solving an equation. Thanks 91 | @karuppiah7890 ! 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A step by step solver for math 2 | 3 | [![Join the chat at https://gitter.im/mathsteps-chat/Lobby](https://badges.gitter.im/mathsteps-chat/Lobby.svg)](https://gitter.im/mathsteps-chat/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | https://www.youtube.com/watch?v=iCrargw1rrM 6 | 7 | ## Requirements 8 | 9 | Mathsteps requires Node version > 6.0.0 10 | 11 | ## Usage 12 | 13 | To install mathsteps using npm: 14 | 15 | npm install mathsteps 16 | 17 | ```js 18 | const mathsteps = require('mathsteps'); 19 | 20 | const steps = mathsteps.simplifyExpression('2x + 2x + x + x'); 21 | 22 | steps.forEach(step => { 23 | console.log("before change: " + step.oldNode.toString()); // before change: 2 x + 2 x + x + x 24 | console.log("change: " + step.changeType); // change: ADD_POLYNOMIAL_TERMS 25 | console.log("after change: " + step.newNode.toString()); // after change: 6 x 26 | console.log("# of substeps: " + step.substeps.length); // # of substeps: 3 27 | }); 28 | ``` 29 | 30 | To solve an equation: 31 | ```js 32 | const steps = mathsteps.solveEquation('2x + 3x = 35'); 33 | 34 | steps.forEach(step => { 35 | console.log("before change: " + step.oldEquation.ascii()); // e.g. before change: 2x + 3x = 35 36 | console.log("change: " + step.changeType); // e.g. change: SIMPLIFY_LEFT_SIDE 37 | console.log("after change: " + step.newEquation.ascii()); // e.g. after change: 5x = 35 38 | console.log("# of substeps: " + step.substeps.length); // e.g. # of substeps: 2 39 | }); 40 | ``` 41 | 42 | (if you're using mathsteps v0.1.6 or lower, use `.print()` instead of `.ascii()`) 43 | 44 | To see all the change types: 45 | ```js 46 | const changes = mathsteps.ChangeTypes; 47 | ``` 48 | 49 | 50 | 51 | ## Contributing 52 | 53 | Hi! If you're interested in working on this, that would be super awesome! 54 | Learn more here: [CONTRIBUTING.md](CONTRIBUTING.md). 55 | 56 | ## Build 57 | 58 | First clone the project from github: 59 | 60 | git clone https://github.com/google/mathsteps.git 61 | cd mathsteps 62 | 63 | Install the project dependencies: 64 | 65 | npm install 66 | 67 | ## Test 68 | 69 | To execute tests for the library, install the project dependencies once: 70 | 71 | npm install 72 | 73 | Then, the tests can be executed: 74 | 75 | npm test 76 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | A rough roadmap for `mathsteps` 4 | 5 | Last time we did a ground truth to see what concepts are needed to answer 6 | questions asked in the Soratic app, this was the breakdown (note that some 7 | concepts might overlap and both need to be covered for us to solve the problem): 8 | 9 | - Factoring: 22.84% of questions 10 | - Solve Equation: 17.99% of questions 11 | - Systems of equations: 6.34% of questions 12 | - Negative exponents: 1.96% of questions 13 | 14 | ## Version 1.x 15 | 16 | - Factoring quadratics to solve equations ([open issues that are 17 | related](https://github.com/socraticorg/mathsteps/issues?utf8=%E2%9C%93&q=is%3Aopen%20equation)) 18 | - Factoring (quadratics and other simple factoring) to simplify polynomial 19 | fractions 20 | - Improving exponents ([open issues that are 21 | related](https://github.com/socraticorg/mathsteps/issues?q=is%3Aissue+is%3Aopen+exponents+label%3Aexponents)) 22 | - Improving roots ([open issues that are 23 | related](https://github.com/socraticorg/mathsteps/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Aroots%20)) 24 | - Systems of equations ([#48](https://github.com/socraticorg/mathsteps/issues/48)) 25 | 26 | ## Version 2.x 27 | 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('./lib/ChangeTypes'); 2 | const factor = require('./lib/factor'); 3 | const simplifyExpression = require('./lib/simplifyExpression'); 4 | const solveEquation = require('./lib/solveEquation'); 5 | 6 | module.exports = { 7 | factor, 8 | simplifyExpression, 9 | solveEquation, 10 | ChangeTypes, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/Negative.js: -------------------------------------------------------------------------------- 1 | const NodeCreator = require('./node/Creator'); 2 | const NodeType = require('./node/Type'); 3 | const PolynomialTerm = require('./node/PolynomialTerm'); 4 | 5 | const Negative = {}; 6 | 7 | // Returns if the given node is negative. Treats a unary minus as a negative, 8 | // as well as a negative constant value or a constant fraction that would 9 | // evaluate to a negative number 10 | Negative.isNegative = function(node) { 11 | if (NodeType.isUnaryMinus(node)) { 12 | return !Negative.isNegative(node.args[0]); 13 | } 14 | else if (NodeType.isConstant(node)) { 15 | return parseFloat(node.value) < 0; 16 | } 17 | else if (NodeType.isConstantFraction(node)) { 18 | const numeratorValue = parseFloat(node.args[0].value); 19 | const denominatorValue = parseFloat(node.args[1].value); 20 | if (numeratorValue < 0 || denominatorValue < 0) { 21 | return !(numeratorValue < 0 && denominatorValue < 0); 22 | } 23 | } 24 | else if (PolynomialTerm.isPolynomialTerm(node)) { 25 | const polyNode = new PolynomialTerm(node); 26 | return Negative.isNegative(polyNode.getCoeffNode(true)); 27 | } 28 | 29 | return false; 30 | }; 31 | 32 | // Given a node, returns the negated node 33 | // If naive is true, then we just add an extra unary minus to the expression 34 | // otherwise, we do the actual negation 35 | // E.g. 36 | // not naive: -3 -> 3, x -> -x 37 | // naive: -3 -> --3, x -> -x 38 | Negative.negate = function(node, naive=false) { 39 | if (NodeType.isConstantFraction(node)) { 40 | node.args[0] = Negative.negate(node.args[0], naive); 41 | return node; 42 | } 43 | else if (PolynomialTerm.isPolynomialTerm(node)) { 44 | return Negative.negatePolynomialTerm(node, naive); 45 | } 46 | else if (!naive) { 47 | if (NodeType.isUnaryMinus(node)) { 48 | return node.args[0]; 49 | } 50 | else if (NodeType.isConstant(node)) { 51 | return NodeCreator.constant(0 - parseFloat(node.value)); 52 | } 53 | } 54 | return NodeCreator.unaryMinus(node); 55 | }; 56 | 57 | // Multiplies a polynomial term by -1 and returns the new node 58 | // If naive is true, then we just add an extra unary minus to the expression 59 | // otherwise, we do the actual negation 60 | // E.g. 61 | // not naive: -3x -> 3x, x -> -x 62 | // naive: -3x -> --3x, x -> -x 63 | Negative.negatePolynomialTerm = function(node, naive=false) { 64 | if (!PolynomialTerm.isPolynomialTerm(node)) { 65 | throw Error('node is not a polynomial term'); 66 | } 67 | const polyNode = new PolynomialTerm(node); 68 | 69 | let newCoeff; 70 | if (!polyNode.hasCoeff()) { 71 | newCoeff = NodeCreator.constant(-1); 72 | } 73 | else { 74 | const oldCoeff = polyNode.getCoeffNode(); 75 | if (oldCoeff.value === '-1') { 76 | newCoeff = null; 77 | } 78 | else if (polyNode.hasFractionCoeff()) { 79 | let numerator = oldCoeff.args[0]; 80 | numerator = Negative.negate(numerator, naive); 81 | 82 | const denominator = oldCoeff.args[1]; 83 | newCoeff = NodeCreator.operator('/', [numerator, denominator]); 84 | } 85 | else { 86 | newCoeff = Negative.negate(oldCoeff, naive); 87 | if (newCoeff.value === '1') { 88 | newCoeff = null; 89 | } 90 | } 91 | } 92 | return NodeCreator.polynomialTerm( 93 | polyNode.getSymbolNode(), polyNode.getExponentNode(), newCoeff); 94 | }; 95 | 96 | module.exports = Negative; 97 | -------------------------------------------------------------------------------- /lib/Symbols.js: -------------------------------------------------------------------------------- 1 | const Node = require('./node'); 2 | 3 | const Symbols = {}; 4 | 5 | // returns the set of all the symbols in an equation 6 | Symbols.getSymbolsInEquation = function(equation) { 7 | const leftSymbols = Symbols.getSymbolsInExpression(equation.leftNode); 8 | const rightSymbols = Symbols.getSymbolsInExpression(equation.rightNode); 9 | const symbols = new Set([...leftSymbols, ...rightSymbols]); 10 | return symbols; 11 | }; 12 | 13 | // return the set of symbols in the expression tree 14 | Symbols.getSymbolsInExpression = function(expression) { 15 | const symbolNodes = expression.filter(node => node.isSymbolNode); // all the symbol nodes 16 | const symbols = symbolNodes.map(node => node.name); // all the symbol nodes' names 17 | const symbolSet = new Set(symbols); // to get rid of duplicates 18 | return symbolSet; 19 | }; 20 | 21 | // Iterates through a node and returns the last term with the symbol name 22 | // Returns null if no terms with the symbol name are in the node. 23 | // e.g. 4x^2 + 2x + y + 2 with `symbolName=x` would return 2x 24 | Symbols.getLastSymbolTerm = function(node, symbolName) { 25 | // First check if the node itself is a polyomial term with symbolName 26 | if (isSymbolTerm(node, symbolName)) { 27 | return node; 28 | } 29 | // If it's a sum of terms, look through the operands for a term 30 | // with `symbolName` 31 | else if (Node.Type.isOperator(node, '+')) { 32 | for (let i = node.args.length - 1; i >= 0 ; i--) { 33 | const child = node.args[i]; 34 | if (Node.Type.isOperator(child, '+')) { 35 | return Symbols.getLastSymbolTerm(child, symbolName); 36 | } 37 | else if (isSymbolTerm(child, symbolName)) { 38 | return child; 39 | } 40 | } 41 | } 42 | else if (Node.Type.isParenthesis(node)) { 43 | return Symbols.getLastSymbolTerm(node.content, symbolName); 44 | } 45 | 46 | return null; 47 | }; 48 | 49 | // Iterates through a node and returns the last term that does not have the 50 | // symbolName including other polynomial terms, and constants or constant 51 | // fractions 52 | // e.g. 4x^2 with `symbolName=x` would return 4 53 | // e.g. 4x^2 + 2x + 2/4 with `symbolName=x` would return 2/4 54 | // e.g. 4x^2 + 2x + y with `symbolName=x` would return y 55 | Symbols.getLastNonSymbolTerm = function(node, symbolName) { 56 | if (isPolynomialTermWithSymbol(node, symbolName)) { 57 | return new Node.PolynomialTerm(node).getCoeffNode(); 58 | } 59 | else if (hasDenominatorSymbol(node, symbolName)) { 60 | return null; 61 | } 62 | else if (Node.Type.isOperator(node)) { 63 | for (let i = node.args.length - 1; i >= 0 ; i--) { 64 | const child = node.args[i]; 65 | if (Node.Type.isOperator(child, '+')) { 66 | return Symbols.getLastNonSymbolTerm(child, symbolName); 67 | } 68 | else if (!isSymbolTerm(child, symbolName)) { 69 | return child; 70 | } 71 | } 72 | } 73 | 74 | return null; 75 | }; 76 | 77 | // Iterates through a node and returns the denominator if it has a 78 | // symbolName in its denominator 79 | // e.g. 1/(2x) with `symbolName=x` would return 2x 80 | // e.g. 1/(x+2) with `symbolName=x` would return x+2 81 | // e.g. 1/(x+2) + (x+1)/(2x+3) with `symbolName=x` would return (2x+3) 82 | Symbols.getLastDenominatorWithSymbolTerm = function(node, symbolName) { 83 | // First check if the node itself has a symbol in the denominator 84 | if (hasDenominatorSymbol(node, symbolName)) { 85 | return node.args[1]; 86 | } 87 | // Otherwise, it's a sum of terms. e.g. 1/x + 1(2+x) 88 | // Look through the operands for a 89 | // denominator term with `symbolName` 90 | else if (Node.Type.isOperator(node, '+')) { 91 | for (let i = node.args.length - 1; i >= 0 ; i--) { 92 | const child = node.args[i]; 93 | if (Node.Type.isOperator(child, '+')) { 94 | return Symbols.getLastDenominatorWithSymbolTerm(child, symbolName); 95 | } 96 | else if (hasDenominatorSymbol(child, symbolName)) { 97 | return child.args[1]; 98 | } 99 | } 100 | } 101 | return null; 102 | }; 103 | 104 | // Returns if `node` is a term with symbol `symbolName` 105 | function isSymbolTerm(node, symbolName) { 106 | return isPolynomialTermWithSymbol(node, symbolName) || 107 | hasDenominatorSymbol(node, symbolName); 108 | } 109 | 110 | function isPolynomialTermWithSymbol(node, symbolName) { 111 | if (Node.PolynomialTerm.isPolynomialTerm(node)) { 112 | const polyTerm = new Node.PolynomialTerm(node); 113 | if (polyTerm.getSymbolName() === symbolName) { 114 | return true; 115 | } 116 | } 117 | 118 | return false; 119 | } 120 | 121 | // Return if `node` has a symbol in its denominator 122 | // e.g. true for 1/(2x) 123 | // e.g. false for 5x 124 | function hasDenominatorSymbol(node, symbolName) { 125 | if (Node.Type.isOperator(node) && node.op === '/') { 126 | const allSymbols = Symbols.getSymbolsInExpression(node.args[1]); 127 | return allSymbols.has(symbolName); 128 | } 129 | 130 | return false; 131 | } 132 | 133 | module.exports = Symbols; 134 | -------------------------------------------------------------------------------- /lib/TreeSearch.js: -------------------------------------------------------------------------------- 1 | const Node = require('./node'); 2 | 3 | const TreeSearch = {}; 4 | 5 | // Returns a function that performs a preorder search on the tree for the given 6 | // simplification function 7 | TreeSearch.preOrder = function(simplificationFunction) { 8 | return function (node) { 9 | return search(simplificationFunction, node, true); 10 | }; 11 | }; 12 | 13 | // Returns a function that performs a postorder search on the tree for the given 14 | // simplification function 15 | TreeSearch.postOrder = function(simplificationFunction) { 16 | return function (node) { 17 | return search(simplificationFunction, node, false); 18 | }; 19 | }; 20 | 21 | // A helper function for performing a tree search with a function 22 | function search(simplificationFunction, node, preOrder) { 23 | let status; 24 | 25 | if (preOrder) { 26 | status = simplificationFunction(node); 27 | if (status.hasChanged()) { 28 | return status; 29 | } 30 | } 31 | 32 | if (Node.Type.isConstant(node) || Node.Type.isSymbol(node)) { 33 | return Node.Status.noChange(node); 34 | } 35 | else if (Node.Type.isUnaryMinus(node)) { 36 | status = search(simplificationFunction, node.args[0], preOrder); 37 | if (status.hasChanged()) { 38 | return Node.Status.childChanged(node, status); 39 | } 40 | } 41 | else if (Node.Type.isOperator(node) || Node.Type.isFunction(node)) { 42 | for (let i = 0; i < node.args.length; i++) { 43 | const child = node.args[i]; 44 | const childNodeStatus = search(simplificationFunction, child, preOrder); 45 | if (childNodeStatus.hasChanged()) { 46 | return Node.Status.childChanged(node, childNodeStatus, i); 47 | } 48 | } 49 | } 50 | else if (Node.Type.isParenthesis(node)) { 51 | status = search(simplificationFunction, node.content, preOrder); 52 | if (status.hasChanged()) { 53 | return Node.Status.childChanged(node, status); 54 | } 55 | } 56 | else { 57 | throw Error('Unsupported node type: ' + node); 58 | } 59 | 60 | if (!preOrder) { 61 | return simplificationFunction(node); 62 | } 63 | else { 64 | return Node.Status.noChange(node); 65 | } 66 | } 67 | 68 | 69 | module.exports = TreeSearch; 70 | -------------------------------------------------------------------------------- /lib/checks/canAddLikeTerms.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | 3 | // Returns true if the nodes are terms that can be added together. 4 | // The nodes need to have the same base and exponent 5 | // e.g. 2x + 5x, 6x^2 + x^2, nthRoot(4,2) + nthRoot(4,2) 6 | function canAddLikeTermNodes(node, termSubclass) { 7 | if (!Node.Type.isOperator(node, '+')) { 8 | return false; 9 | } 10 | const args = node.args; 11 | if (!args.every(n => Node.Term.isTerm(n, termSubclass.baseNodeFunc))) { 12 | return false; 13 | } 14 | if (args.length === 1) { 15 | return false; 16 | } 17 | 18 | const termList = args.map(n => new termSubclass(n)); 19 | 20 | // to add terms, they must have the same base *and* exponent 21 | const firstTerm = termList[0]; 22 | const sharedBase = firstTerm.getBaseNode(); 23 | const sharedExponentNode = firstTerm.getExponentNode(true); 24 | 25 | const restTerms = termList.slice(1); 26 | return restTerms.every(term => { 27 | const haveSameBase = sharedBase.equals(term.getBaseNode()); 28 | const exponentNode = term.getExponentNode(true); 29 | const haveSameExponent = exponentNode.equals(sharedExponentNode); 30 | return haveSameBase && haveSameExponent; 31 | }); 32 | } 33 | 34 | // Returns true if the nodes are nth roots that can be added together 35 | function canAddLikeTermNthRootNodes(node) { 36 | return canAddLikeTermNodes(node, Node.NthRootTerm); 37 | } 38 | 39 | // Returns true if the nodes are polynomial terms that can be added together. 40 | function canAddLikeTermPolynomialNodes(node) { 41 | return canAddLikeTermNodes(node, Node.PolynomialTerm); 42 | } 43 | 44 | module.exports = { 45 | canAddLikeTermNodes, 46 | canAddLikeTermNthRootNodes, 47 | canAddLikeTermPolynomialNodes, 48 | }; 49 | -------------------------------------------------------------------------------- /lib/checks/canFindRoots.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | const resolvesToConstant = require('./resolvesToConstant.js'); 3 | /* 4 | Return true if the equation is of the form factor * factor = 0 or factor^power = 0 5 | // e.g (x - 2)^2 = 0, x(x + 2)(x - 2) = 0 6 | */ 7 | function canFindRoots(equation) { 8 | const left = equation.leftNode; 9 | const right = equation.rightNode; 10 | 11 | const zeroRightSide = Node.Type.isConstant(right) 12 | && parseFloat(right.value) === 0; 13 | 14 | const isMulOrPower = Node.Type.isOperator(left, '*') || Node.Type.isOperator(left, '^'); 15 | 16 | if (!(zeroRightSide && isMulOrPower)) { 17 | return false; 18 | } 19 | 20 | // If the left side of the equation is multiplication, filter out all the factors 21 | // that do evaluate to constants because they do not have roots. If the 22 | // resulting array is empty, there is no roots to be found. Do a similiar check 23 | // for when the left side is a power node. 24 | // e.g 2^7 and (33 + 89) do not have solutions when set equal to 0 25 | 26 | if (Node.Type.isOperator(left, '*')) { 27 | const factors = left.args.filter(arg => !resolvesToConstant(arg)); 28 | return factors.length >= 1; 29 | } 30 | else if (Node.Type.isOperator(left, '^')) { 31 | return !resolvesToConstant(left); 32 | } 33 | } 34 | 35 | module.exports = canFindRoots; 36 | -------------------------------------------------------------------------------- /lib/checks/canMultiplyLikeTermConstantNodes.js: -------------------------------------------------------------------------------- 1 | const ConstantOrPowerTerm = require('../simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower'); 2 | const Node = require('../node'); 3 | 4 | // Returns true if node is a multiplication of constant power nodes 5 | // where you can combine their exponents, e.g. 10^2 * 10^4 * 10 can become 10^7. 6 | // The node can either be on form c^n or c, as long as c is the same for all. 7 | function canMultiplyLikeTermConstantNodes(node) { 8 | if (!Node.Type.isOperator(node) || node.op !== '*') { 9 | return false; 10 | } 11 | const args = node.args; 12 | if (!args.every(n => ConstantOrPowerTerm.isConstantOrConstantPower(n))) { 13 | return false; 14 | } 15 | 16 | // if none of the terms have exponents, return false here, 17 | // else e.g. 6*6 will become 6^1 * 6^1 => 6^2 18 | if (args.every(arg => !Node.Type.isOperator(arg, '^'))) { 19 | return false; 20 | } 21 | 22 | const constantTermBaseList = args.map(n => ConstantOrPowerTerm.getBaseNode(n)); 23 | const firstTerm = constantTermBaseList[0]; 24 | const restTerms = constantTermBaseList.slice(1); 25 | // they're considered like terms if they have the same base value 26 | return restTerms.every(term => firstTerm.value === term.value); 27 | } 28 | 29 | module.exports = canMultiplyLikeTermConstantNodes; 30 | -------------------------------------------------------------------------------- /lib/checks/canMultiplyLikeTermPolynomialNodes.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | 3 | // Returns true if the nodes are symbolic terms with the same symbol and no 4 | // coefficients. 5 | function canMultiplyLikeTermPolynomialNodes(node) { 6 | if (!Node.Type.isOperator(node) || node.op !== '*') { 7 | return false; 8 | } 9 | const args = node.args; 10 | if (!args.every(n => Node.PolynomialTerm.isPolynomialTerm(n))) { 11 | return false; 12 | } 13 | if (args.length === 1) { 14 | return false; 15 | } 16 | 17 | const polynomialTermList = node.args.map(n => new Node.PolynomialTerm(n)); 18 | if (!polynomialTermList.every(polyTerm => !polyTerm.hasCoeff())) { 19 | return false; 20 | } 21 | 22 | const firstTerm = polynomialTermList[0]; 23 | const restTerms = polynomialTermList.slice(1); 24 | // they're considered like terms if they have the same symbol name 25 | return restTerms.every(term => firstTerm.getSymbolName() === term.getSymbolName()); 26 | } 27 | 28 | module.exports = canMultiplyLikeTermPolynomialNodes; 29 | -------------------------------------------------------------------------------- /lib/checks/canMultiplyLikeTermsNthRoots.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | const NthRoot = require('../simplifyExpression/functionsSearch/nthRoot'); 3 | 4 | // Function to check if nthRoot nodes can be multiplied 5 | // e.g. nthRoot(x, 2) * nthRoot(x, 2) -> true 6 | // e.g. nthRoot(x, 2) * nthRoot(x, 3) -> false 7 | function canMultiplyLikeTermsNthRoots(node) { 8 | // checks if node is a multiplication of nthRoot nodes 9 | // all the terms has to have the same root node to be multiplied 10 | 11 | if (!Node.Type.isOperator(node, '*') 12 | || !(node.args.every(term => Node.Type.isFunction(term, 'nthRoot')))){ 13 | return false; 14 | } 15 | 16 | // Take arbitrary root node 17 | const firstTerm = node.args[0]; 18 | const rootNode = NthRoot.getRootNode(firstTerm); 19 | 20 | return node.args.every( 21 | term => NthRoot.getRootNode(term).equals(rootNode)); 22 | } 23 | 24 | module.exports = canMultiplyLikeTermsNthRoots; 25 | -------------------------------------------------------------------------------- /lib/checks/canRearrangeCoefficient.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | 3 | // Returns true if the expression is a multiplication between a constant 4 | // and polynomial without a coefficient. 5 | function canRearrangeCoefficient(node) { 6 | // implicit multiplication doesn't count as multiplication here, since it 7 | // represents a single term. 8 | if (node.op !== '*' || node.implicit) { 9 | return false; 10 | } 11 | if (node.args.length !== 2) { 12 | return false; 13 | } 14 | if (!Node.Type.isConstantOrConstantFraction(node.args[1])) { 15 | return false; 16 | } 17 | if (!Node.PolynomialTerm.isPolynomialTerm(node.args[0])) { 18 | return false; 19 | } 20 | 21 | const polyNode = new Node.PolynomialTerm(node.args[0]); 22 | return !polyNode.hasCoeff(); 23 | } 24 | 25 | module.exports = canRearrangeCoefficient; 26 | -------------------------------------------------------------------------------- /lib/checks/canSimplifyPolynomialTerms.js: -------------------------------------------------------------------------------- 1 | const canAddLikeTerms = require('./canAddLikeTerms'); 2 | const canMultiplyLikeTermPolynomialNodes = require('./canMultiplyLikeTermPolynomialNodes'); 3 | const canRearrangeCoefficient = require('./canRearrangeCoefficient'); 4 | 5 | // Returns true if the node is an operation node with parameters that are 6 | // polynomial terms that can be combined in some way. 7 | function canSimplifyPolynomialTerms(node) { 8 | return (canAddLikeTerms.canAddLikeTermPolynomialNodes(node) || 9 | canMultiplyLikeTermPolynomialNodes(node) || 10 | canRearrangeCoefficient(node)); 11 | } 12 | 13 | module.exports = canSimplifyPolynomialTerms; 14 | -------------------------------------------------------------------------------- /lib/checks/hasUnsupportedNodes.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | const resolvesToConstant = require('./resolvesToConstant'); 3 | 4 | function hasUnsupportedNodes(node) { 5 | if (Node.Type.isParenthesis(node)) { 6 | return hasUnsupportedNodes(node.content); 7 | } 8 | else if (Node.Type.isUnaryMinus(node)) { 9 | return hasUnsupportedNodes(node.args[0]); 10 | } 11 | else if (Node.Type.isOperator(node)) { 12 | return node.args.some(hasUnsupportedNodes); 13 | } 14 | else if (Node.Type.isSymbol(node) || Node.Type.isConstant(node)) { 15 | return false; 16 | } 17 | else if (Node.Type.isFunction(node, 'abs')) { 18 | if (node.args.length !== 1) { 19 | return true; 20 | } 21 | if (node.args.some(hasUnsupportedNodes)) { 22 | return true; 23 | } 24 | return !resolvesToConstant(node.args[0]); 25 | } 26 | else if (Node.Type.isFunction(node, 'nthRoot')) { 27 | return node.args.some(hasUnsupportedNodes) || node.args.length < 1; 28 | } 29 | else { 30 | return true; 31 | } 32 | } 33 | 34 | module.exports = hasUnsupportedNodes; 35 | -------------------------------------------------------------------------------- /lib/checks/index.js: -------------------------------------------------------------------------------- 1 | const canAddLikeTerms = require('./canAddLikeTerms'); 2 | const canFindRoots = require('./canFindRoots'); 3 | const canMultiplyLikeTermConstantNodes = require('./canMultiplyLikeTermConstantNodes'); 4 | const canMultiplyLikeTermPolynomialNodes = require('./canMultiplyLikeTermPolynomialNodes'); 5 | const canMultiplyLikeTermsNthRoots = require('./canMultiplyLikeTermsNthRoots'); 6 | const canRearrangeCoefficient = require('./canRearrangeCoefficient'); 7 | const canSimplifyPolynomialTerms = require('./canSimplifyPolynomialTerms'); 8 | const hasUnsupportedNodes = require('./hasUnsupportedNodes'); 9 | const isQuadratic = require('./isQuadratic'); 10 | const resolvesToConstant = require('./resolvesToConstant'); 11 | 12 | module.exports = { 13 | canFindRoots, 14 | canAddLikeTerms, 15 | canMultiplyLikeTermConstantNodes, 16 | canMultiplyLikeTermPolynomialNodes, 17 | canMultiplyLikeTermsNthRoots, 18 | canRearrangeCoefficient, 19 | canSimplifyPolynomialTerms, 20 | hasUnsupportedNodes, 21 | isQuadratic, 22 | resolvesToConstant, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/checks/isQuadratic.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | const Symbols = require('../Symbols'); 3 | 4 | // Given a node, will determine if the expression is in the form of a quadratic 5 | // e.g. `x^2 + 2x + 1` OR `x^2 - 1` but not `x^3 + x^2 + x + 1` 6 | function isQuadratic(node) { 7 | if (!Node.Type.isOperator(node, '+')) { 8 | return false; 9 | } 10 | 11 | if (node.args.length > 3) { 12 | return false; 13 | } 14 | 15 | // make sure only one symbol appears in the expression 16 | const symbolSet = Symbols.getSymbolsInExpression(node); 17 | if (symbolSet.size !== 1) { 18 | return false; 19 | } 20 | 21 | const secondDegreeTerms = node.args.filter(isPolynomialTermOfDegree(2)); 22 | const firstDegreeTerms = node.args.filter(isPolynomialTermOfDegree(1)); 23 | const constantTerms = node.args.filter(Node.Type.isConstant); 24 | 25 | // Check that there is one second degree term and at most one first degree 26 | // term and at most one constant term 27 | if (secondDegreeTerms.length !== 1 || firstDegreeTerms.length > 1 || 28 | constantTerms.length > 1) { 29 | return false; 30 | } 31 | 32 | // check that there are no terms that don't fall into these groups 33 | if ((secondDegreeTerms.length + firstDegreeTerms.length + 34 | constantTerms.length) !== node.args.length) { 35 | return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | // Given a degree, returns a function that checks if a node 42 | // is a polynomial term of the given degree. 43 | function isPolynomialTermOfDegree(degree) { 44 | return function(node) { 45 | if (Node.PolynomialTerm.isPolynomialTerm(node)) { 46 | const polyTerm = new Node.PolynomialTerm(node); 47 | const exponent = polyTerm.getExponentNode(true); 48 | return exponent && parseFloat(exponent.value) === degree; 49 | } 50 | return false; 51 | }; 52 | } 53 | 54 | module.exports = isQuadratic; 55 | -------------------------------------------------------------------------------- /lib/checks/resolvesToConstant.js: -------------------------------------------------------------------------------- 1 | const Node = require('../node'); 2 | 3 | // Returns true if the node is a constant or can eventually be resolved to 4 | // a constant. 5 | // e.g. 2, 2+4, (2+4)^2 would all return true. x + 4 would return false 6 | function resolvesToConstant(node) { 7 | if (Node.Type.isOperator(node) || Node.Type.isFunction(node)) { 8 | return node.args.every( 9 | (child) => resolvesToConstant(child)); 10 | } 11 | else if (Node.Type.isParenthesis(node)) { 12 | return resolvesToConstant(node.content); 13 | } 14 | else if (Node.Type.isConstant(node, true)) { 15 | return true; 16 | } 17 | else if (Node.Type.isSymbol(node)) { 18 | return false; 19 | } 20 | else if (Node.Type.isUnaryMinus(node)) { 21 | return resolvesToConstant(node.args[0]); 22 | } 23 | else { 24 | throw Error('Unsupported node type: ' + node.type); 25 | } 26 | } 27 | 28 | module.exports = resolvesToConstant; 29 | -------------------------------------------------------------------------------- /lib/equation/Equation.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const printNode = require('../util/print'); 4 | 5 | // This represents an equation, made up of the leftNode (LHS), the 6 | // rightNode (RHS) and a comparator (=, <, >, <=, or >=) 7 | class Equation { 8 | constructor(leftNode, rightNode, comparator) { 9 | this.leftNode = leftNode; 10 | this.rightNode = rightNode; 11 | this.comparator = comparator; 12 | } 13 | 14 | // Prints an Equation properly using the print module 15 | ascii(showPlusMinus=false) { 16 | const leftSide = printNode.ascii(this.leftNode, showPlusMinus); 17 | const rightSide = printNode.ascii(this.rightNode, showPlusMinus); 18 | const comparator = this.comparator; 19 | 20 | return `${leftSide} ${comparator} ${rightSide}`; 21 | } 22 | 23 | // Prints an Equation properly using LaTeX 24 | latex(showPlusMinus=false) { 25 | const leftSide = printNode.latex(this.leftNode, showPlusMinus); 26 | const rightSide = printNode.latex(this.rightNode, showPlusMinus); 27 | const comparator = this.comparator; 28 | 29 | return `${leftSide} ${comparator} ${rightSide}`; 30 | } 31 | 32 | clone() { 33 | const newLeft = this.leftNode.cloneDeep(); 34 | const newRight = this.rightNode.cloneDeep(); 35 | return new Equation(newLeft, newRight, this.comparator); 36 | } 37 | } 38 | 39 | // Splits a string on the given comparator and returns a new Equation object 40 | // from the left and right hand sides 41 | Equation.createEquationFromString = function(str, comparator) { 42 | const sides = str.split(comparator); 43 | if (sides.length !== 2) { 44 | throw Error('Expected two sides of an equation using comparator: ' + 45 | comparator); 46 | } 47 | const leftNode = math.parse(sides[0]); 48 | const rightNode = math.parse(sides[1]); 49 | 50 | return new Equation(leftNode, rightNode, comparator); 51 | }; 52 | 53 | module.exports = Equation; 54 | -------------------------------------------------------------------------------- /lib/equation/Status.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../ChangeTypes'); 2 | const Equation = require('./Equation'); 3 | const Node = require('../node'); 4 | 5 | // This represents the current equation we're solving. 6 | // As we move step by step, an equation might be updated. Functions return this 7 | // status object to pass on the updated equation and information on if/how it was 8 | // changed. 9 | class Status { 10 | constructor(changeType, oldEquation, newEquation, substeps=[]) { 11 | if (!newEquation) { 12 | throw Error('new equation isn\'t defined'); 13 | } 14 | if (changeType === undefined || typeof(changeType) !== 'string') { 15 | throw Error('changetype isn\'t valid'); 16 | } 17 | 18 | this.changeType = changeType; 19 | this.oldEquation = oldEquation; 20 | this.newEquation = newEquation; 21 | this.substeps = substeps; 22 | } 23 | 24 | hasChanged() { 25 | return this.changeType !== ChangeTypes.NO_CHANGE; 26 | } 27 | } 28 | 29 | // A wrapper around the Status constructor for the case where equation 30 | // hasn't been changed. 31 | Status.noChange = function(equation) { 32 | return new Status(ChangeTypes.NO_CHANGE, null, equation); 33 | }; 34 | 35 | Status.addLeftStep = function(equation, leftStep) { 36 | const substeps = []; 37 | leftStep.substeps.forEach(substep => { 38 | substeps.push(Status.addLeftStep(equation, substep)); 39 | }); 40 | let oldEquation = null; 41 | if (leftStep.oldNode) { 42 | oldEquation = equation.clone(); 43 | oldEquation.leftNode = leftStep.oldNode; 44 | } 45 | const newEquation = equation.clone(); 46 | newEquation.leftNode = leftStep.newNode; 47 | return new Status( 48 | leftStep.changeType, oldEquation, newEquation, substeps); 49 | }; 50 | 51 | Status.addRightStep = function(equation, rightStep) { 52 | const substeps = []; 53 | rightStep.substeps.forEach(substep => { 54 | substeps.push(Status.addRightStep(equation, substep)); 55 | }); 56 | let oldEquation = null; 57 | if (rightStep.oldNode) { 58 | oldEquation = equation.clone(); 59 | oldEquation.rightNode = rightStep.oldNode; 60 | } 61 | const newEquation = equation.clone(); 62 | newEquation.rightNode = rightStep.newNode; 63 | return new Status( 64 | rightStep.changeType, oldEquation, newEquation, substeps); 65 | }; 66 | 67 | Status.resetChangeGroups = function(equation) { 68 | const leftNode = Node.Status.resetChangeGroups(equation.leftNode); 69 | const rightNode = Node.Status.resetChangeGroups(equation.rightNode); 70 | return new Equation(leftNode, rightNode, equation.comparator); 71 | }; 72 | 73 | module.exports = Status; 74 | -------------------------------------------------------------------------------- /lib/equation/index.js: -------------------------------------------------------------------------------- 1 | const Equation = require('./Equation'); 2 | const Status = require('./Status'); 3 | 4 | module.exports = { 5 | Equation, 6 | Status, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/factor/ConstantFactors.js: -------------------------------------------------------------------------------- 1 | // This module deals with getting constant factors, including prime factors 2 | // and factor pairs of a number 3 | 4 | const ConstantFactors = {}; 5 | 6 | // Given a number, will return all the prime factors of that number as a list 7 | // sorted from smallest to largest 8 | ConstantFactors.getPrimeFactors = function(number){ 9 | let factors = []; 10 | if (number < 0) { 11 | factors = [-1]; 12 | factors = factors.concat(ConstantFactors.getPrimeFactors(-1 * number)); 13 | return factors; 14 | } 15 | 16 | const root = Math.sqrt(number); 17 | let candidate = 2; 18 | if (number % 2) { 19 | candidate = 3; // assign first odd 20 | while (number % candidate && candidate <= root) { 21 | candidate = candidate + 2; 22 | } 23 | } 24 | 25 | // if no factor found then the number is prime 26 | if (candidate > root) { 27 | factors.push(number); 28 | } 29 | // if we find a factor, make a recursive call on the quotient of the number and 30 | // our newly found prime factor in order to find more factors 31 | else { 32 | factors.push(candidate); 33 | factors = factors.concat(ConstantFactors.getPrimeFactors(number/candidate)); 34 | } 35 | 36 | return factors; 37 | }; 38 | 39 | // Given a number, will return all the factor pairs for that number as a list 40 | // of 2-item lists 41 | ConstantFactors.getFactorPairs = function(number){ 42 | const factors = []; 43 | 44 | const bound = Math.floor(Math.sqrt(Math.abs(number))); 45 | for (var divisor = -bound; divisor <= bound; divisor++) { 46 | if (divisor === 0) { 47 | continue; 48 | } 49 | if (number % divisor === 0) { 50 | const quotient = number / divisor; 51 | factors.push([divisor, quotient]); 52 | } 53 | } 54 | 55 | return factors; 56 | }; 57 | 58 | module.exports = ConstantFactors; 59 | -------------------------------------------------------------------------------- /lib/factor/index.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | const stepThrough = require('./stepThrough'); 3 | 4 | function factorString(expressionString, debug=false) { 5 | let node; 6 | try { 7 | node = math.parse(expressionString); 8 | } 9 | catch (err) { 10 | return []; 11 | } 12 | 13 | if (node) { 14 | return stepThrough(node, debug); 15 | } 16 | return []; 17 | } 18 | 19 | module.exports = factorString; 20 | -------------------------------------------------------------------------------- /lib/factor/stepThrough.js: -------------------------------------------------------------------------------- 1 | const checks = require('../checks'); 2 | 3 | const factorQuadratic = require('./factorQuadratic'); 4 | 5 | const flattenOperands = require('../util/flattenOperands'); 6 | const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); 7 | 8 | // Given a mathjs expression node, steps through factoring the expression. 9 | // Currently only supports factoring quadratics. 10 | // Returns a list of details about each step. 11 | function stepThrough(node, debug=false) { 12 | if (debug) { 13 | // eslint-disable-next-line 14 | console.log('\n\nFactoring: ' + print.ascii(node, false, true)); 15 | } 16 | 17 | if (checks.hasUnsupportedNodes(node)) { 18 | return []; 19 | } 20 | 21 | let nodeStatus; 22 | const steps = []; 23 | 24 | node = flattenOperands(node); 25 | node = removeUnnecessaryParens(node, true); 26 | if (checks.isQuadratic(node)) { 27 | nodeStatus = factorQuadratic(node); 28 | if (nodeStatus.hasChanged()) { 29 | steps.push(nodeStatus); 30 | } 31 | } 32 | // Add factoring higher order polynomials... 33 | 34 | return steps; 35 | } 36 | 37 | module.exports = stepThrough; 38 | -------------------------------------------------------------------------------- /lib/node/Creator.js: -------------------------------------------------------------------------------- 1 | /* 2 | Functions to generate any mathJS node supported by the stepper 3 | see http://mathjs.org/docs/expressions/expression_trees.html#nodes for more 4 | information on nodes in mathJS 5 | */ 6 | 7 | const math = require('mathjs'); 8 | const NodeType = require('./Type'); 9 | 10 | const NodeCreator = { 11 | operator (op, args, implicit=false) { 12 | switch (op) { 13 | case '+': 14 | return new math.expression.node.OperatorNode('+', 'add', args); 15 | case '-': 16 | return new math.expression.node.OperatorNode('-', 'subtract', args); 17 | case '/': 18 | return new math.expression.node.OperatorNode('/', 'divide', args); 19 | case '*': 20 | return new math.expression.node.OperatorNode( 21 | '*', 'multiply', args, implicit); 22 | case '^': 23 | return new math.expression.node.OperatorNode('^', 'pow', args); 24 | default: 25 | throw Error('Unsupported operation: ' + op); 26 | } 27 | }, 28 | 29 | // In almost all cases, use Negative.negate (with naive = true) to add a 30 | // unary minus to your node, rather than calling this constructor directly 31 | unaryMinus (content) { 32 | return new math.expression.node.OperatorNode( 33 | '-', 'unaryMinus', [content]); 34 | }, 35 | 36 | constant (val) { 37 | return new math.expression.node.ConstantNode(val); 38 | }, 39 | 40 | symbol (name) { 41 | return new math.expression.node.SymbolNode(name); 42 | }, 43 | 44 | parenthesis (content) { 45 | return new math.expression.node.ParenthesisNode(content); 46 | }, 47 | 48 | list (content) { 49 | return new math.expression.node.ArrayNode(content); 50 | }, 51 | 52 | // exponent might be null, which means there's no exponent node. 53 | // similarly, coefficient might be null, which means there's no coefficient 54 | // the base node can never be null. 55 | term (base, exponent, coeff, explicitCoeff=false) { 56 | let term = base; 57 | if (exponent) { 58 | term = this.operator('^', [term, exponent]); 59 | } 60 | if (coeff && (explicitCoeff || parseFloat(coeff.value) !== 1)) { 61 | if (NodeType.isConstant(coeff) && 62 | parseFloat(coeff.value) === -1 && 63 | !explicitCoeff) { 64 | // if you actually want -1 as the coefficient, set explicitCoeff to true 65 | term = this.unaryMinus(term); 66 | } 67 | else { 68 | term = this.operator('*', [coeff, term], true); 69 | } 70 | } 71 | return term; 72 | }, 73 | 74 | polynomialTerm (symbol, exponent, coeff, explicitCoeff=false) { 75 | return this.term(symbol, exponent, coeff, explicitCoeff); 76 | }, 77 | 78 | // Given a root value and a radicand (what is under the radical) 79 | nthRoot (radicandNode, rootNode) { 80 | const symbol = NodeCreator.symbol('nthRoot'); 81 | return new math.expression.node.FunctionNode(symbol, [radicandNode, rootNode]); 82 | } 83 | }; 84 | 85 | module.exports = NodeCreator; 86 | -------------------------------------------------------------------------------- /lib/node/CustomType.js: -------------------------------------------------------------------------------- 1 | const Negative = require('../Negative'); 2 | const NodeCreator = require('./Creator'); 3 | const NodeType = require('./Type'); 4 | 5 | const NodeCustomType = {}; 6 | 7 | // Returns true if `node` belongs to the type specified by boolean `isTypeFunc`. 8 | // If `allowUnaryMinus/allowParens` is true, we allow for the node to be nested. 9 | NodeCustomType.isType = function(node, isTypeFunc, allowUnaryMinus=true, allowParens=true) { 10 | if (isTypeFunc(node)) { 11 | return true; 12 | } 13 | else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { 14 | return NodeCustomType.isType(node.args[0], isTypeFunc, allowUnaryMinus, allowParens); 15 | } 16 | else if (allowParens && NodeType.isParenthesis(node)) { 17 | return NodeCustomType.isType(node.content, isTypeFunc, allowUnaryMinus, allowParens); 18 | } 19 | 20 | return false; 21 | }; 22 | 23 | // Returns `node` if `node` belongs to the type specified by boolean `isTypeFunc`. 24 | // If `allowUnaryMinus/allowParens` is true, we check for an inner node of this type. 25 | // `moveUnaryMinus` should be defined if `allowUnaryMinus` is true, and should 26 | // move the unaryMinus into the inside of the type 27 | // e.g. for fractions, this function will negate the numerator 28 | NodeCustomType.getType = function( 29 | node, isTypeFunc, allowUnaryMinus=true, allowParens=true, moveUnaryMinus=undefined) { 30 | if (allowUnaryMinus === true && moveUnaryMinus === undefined) { 31 | throw Error('Error in `getType`: moveUnaryMinus is undefined'); 32 | } 33 | 34 | if (isTypeFunc(node)) { 35 | return node; 36 | } 37 | else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { 38 | return moveUnaryMinus( 39 | NodeCustomType.getType( 40 | node.args[0], isTypeFunc, allowUnaryMinus, allowParens, moveUnaryMinus)); 41 | } 42 | else if (allowParens && NodeType.isParenthesis(node)) { 43 | return NodeCustomType.getType( 44 | node.content, isTypeFunc, allowUnaryMinus, allowParens, moveUnaryMinus); 45 | } 46 | 47 | throw Error('`getType` called on a node that does not belong to specified type'); 48 | }; 49 | 50 | NodeCustomType.isFraction = function(node, allowUnaryMinus=true, allowParens=true) { 51 | return NodeCustomType.isType( 52 | node, 53 | (node) => NodeType.isOperator(node, '/'), 54 | allowUnaryMinus, 55 | allowParens); 56 | }; 57 | 58 | NodeCustomType.getFraction = function(node, allowUnaryMinus=true, allowParens=true) { 59 | const moveUnaryMinus = function(node) { 60 | if (!(NodeType.isOperator(node, '/'))) { 61 | throw Error('Expected a fraction'); 62 | } 63 | 64 | const numerator = node.args[0]; 65 | const denominator = node.args[1]; 66 | const newNumerator = Negative.negate(numerator); 67 | return NodeCreator.operator('/', [newNumerator, denominator]); 68 | }; 69 | 70 | return NodeCustomType.getType( 71 | node, 72 | (node) => NodeType.isOperator(node, '/'), 73 | allowParens, 74 | allowUnaryMinus, 75 | moveUnaryMinus); 76 | }; 77 | 78 | module.exports = NodeCustomType; 79 | -------------------------------------------------------------------------------- /lib/node/MixedNumber.js: -------------------------------------------------------------------------------- 1 | const Negative = require('../Negative'); 2 | const NodeType = require('./Type'); 3 | 4 | // Returns true if `node` is a mixed number 5 | // e.g. 2 1/2, 19 2/3 6 | // Right now mathjs cannot parse the above examples; 7 | // instead it expects the input to look like e.g. 2(1)/(2), 8 | // which is division with implicit multiplication in the numerator 9 | // TODO: Add better support for mixed numbers in the future 10 | function isMixedNumber(node) { 11 | if (!NodeType.isOperator(node, '/')) { 12 | return false; 13 | } 14 | 15 | if (node.args.length !== 2) { 16 | return false; 17 | } 18 | 19 | const numerator = node.args[0]; 20 | const denominator = node.args[1]; 21 | 22 | // check for implicit multiplication between two constants in the numerator 23 | // first can be wrapped in unary minus 24 | // second one can be optionally wrapped in parenthesis 25 | if (!(NodeType.isOperator(numerator, '*') && numerator.implicit)) { 26 | return false; 27 | } 28 | 29 | const numeratorFirstArg = NodeType.isUnaryMinus(numerator.args[0]) ? 30 | Negative.negate(numerator.args[0].args[0]) 31 | : numerator.args[0]; 32 | 33 | const numeratorSecondArg = NodeType.isParenthesis(numerator.args[1]) ? 34 | numerator.args[1].content 35 | : numerator.args[1]; 36 | 37 | if (!(NodeType.isConstant(numeratorFirstArg) && 38 | NodeType.isConstant(numeratorSecondArg))) { 39 | return false; 40 | } 41 | 42 | // check for a constant in the denominator, 43 | // optionally wrapped in parenthesis 44 | const denominatorValue = NodeType.isParenthesis(denominator) ? 45 | denominator.content 46 | : denominator; 47 | 48 | if (!NodeType.isConstant(denominatorValue)) { 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | // Returns true if the mixed number is negative, 56 | // in which case we have to ignore the negative while converting to an 57 | // improper fraction, and instead we negate the whole thing at the end 58 | // e.g. -1 2/3 !== ((-1 * 3) + 2)/3 = -1/3 59 | // -1 2/3 == -((1 * 3) + 2)/3 = -5/2 60 | function isNegativeMixedNumber(node) { 61 | if (!isMixedNumber(node)) { 62 | throw Error('Expected a mixed number'); 63 | } 64 | 65 | return NodeType.isUnaryMinus(node.args[0].args[0]); 66 | } 67 | 68 | // Get the whole number part of a mixed number 69 | // e.g. 1 2/3 -> 1 70 | // Negatives are ignored; e.g. -1 2/3 -> 1 71 | function getWholeNumberValue(node) { 72 | if (!isMixedNumber(node)) { 73 | throw Error('Expected a mixed number'); 74 | } 75 | 76 | const wholeNumberNode = NodeType.isUnaryMinus(node.args[0].args[0]) ? 77 | node.args[0].args[0].args[0] 78 | : node.args[0].args[0]; 79 | 80 | return parseInt(wholeNumberNode.value); 81 | } 82 | 83 | // Get the numerator part of a mixed number 84 | // e.g. 1 2/3 -> 2 85 | function getNumeratorValue(node) { 86 | if (!isMixedNumber(node)) { 87 | throw Error('Expected a mixed number'); 88 | } 89 | 90 | const numeratorNode = NodeType.isParenthesis(node.args[0].args[1]) ? 91 | node.args[0].args[1].content 92 | : node.args[0].args[1]; 93 | 94 | return parseInt(numeratorNode.value); 95 | } 96 | 97 | // Get the denominator part of a mixed number 98 | // e.g. 1 2/3 -> 3 99 | function getDenominatorValue(node) { 100 | if (!isMixedNumber(node)) { 101 | throw Error('Expected a mixed number'); 102 | } 103 | 104 | const denominatorNode = NodeType.isParenthesis(node.args[1]) ? 105 | node.args[1].content 106 | : node.args[1]; 107 | 108 | return parseInt(denominatorNode.value); 109 | } 110 | 111 | module.exports = { 112 | isMixedNumber, 113 | isNegativeMixedNumber, 114 | getWholeNumberValue, 115 | getNumeratorValue, 116 | getDenominatorValue 117 | }; 118 | -------------------------------------------------------------------------------- /lib/node/NthRootTerm.js: -------------------------------------------------------------------------------- 1 | const NodeType = require('./Type'); 2 | const Term = require('./Term'); 3 | 4 | // For storing nth root terms, which are a subclass of Term 5 | // where the base node is an nth root 6 | class NthRootTerm extends Term { 7 | constructor(node, onlyImplicitMultiplication=false) { 8 | super(node, NthRootTerm.baseNodeFunc, onlyImplicitMultiplication); 9 | } 10 | } 11 | 12 | // Returns true if the term has a base node that makes it an nth root term 13 | // e.g. 4x^2 has a base of x, so it is not an nth root term 14 | // 4*sqrt(x)^2 has a base of sqrt(x), so it is an nth root term 15 | NthRootTerm.baseNodeFunc = function(node) { 16 | return NodeType.isFunction(node, 'nthRoot'); 17 | }; 18 | 19 | // Returns true if the node represents an nth root term. 20 | // e.g. nthRoot(4), nthRoot(x^2), 4*nthRoot(10)^2 21 | NthRootTerm.isNthRootTerm = function( 22 | node, onlyImplicitMultiplication=false) { 23 | return Term.isTerm( 24 | node, NthRootTerm.baseNodeFunc, onlyImplicitMultiplication); 25 | }; 26 | 27 | module.exports = NthRootTerm; 28 | -------------------------------------------------------------------------------- /lib/node/PolynomialTerm.js: -------------------------------------------------------------------------------- 1 | const NodeType = require('./Type'); 2 | const Term = require('./Term'); 3 | 4 | // For storing polynomial terms, which are a subclass of Term 5 | // where the base node is a symbol 6 | class PolynomialTerm extends Term { 7 | constructor(node, onlyImplicitMultiplication=false) { 8 | super(node, PolynomialTerm.baseNodeFunc, onlyImplicitMultiplication); 9 | } 10 | 11 | getSymbolNode() { 12 | return this.base; 13 | } 14 | 15 | getSymbolName() { 16 | return this.base.name; 17 | } 18 | } 19 | 20 | // Returns true if the term has a base node that makes it a polynomial term 21 | // e.g. 4x^2 has a base of x, so it is a polynomial 22 | // 4*sqrt(x)^2 has a base of sqrt(x), so it is not 23 | PolynomialTerm.baseNodeFunc = function(node) { 24 | return NodeType.isSymbol(node); 25 | }; 26 | 27 | // Returns true if the node is a polynomial term. 28 | // e.g. x^2, 2y, z, 3x/5 are all polynomial terms. 29 | // 4, 2+x, 3*7, x-z are all not polynomial terms. 30 | // See the tests for some more thorough examples. 31 | PolynomialTerm.isPolynomialTerm = function( 32 | node, onlyImplicitMultiplication=false) { 33 | return Term.isTerm( 34 | node, PolynomialTerm.baseNodeFunc, onlyImplicitMultiplication); 35 | }; 36 | 37 | module.exports = PolynomialTerm; 38 | -------------------------------------------------------------------------------- /lib/node/Status.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../ChangeTypes'); 2 | const Type = require('./Type'); 3 | 4 | // This represents the current (sub)expression we're simplifying. 5 | // As we move step by step, a node might be updated. Functions return this 6 | // status object to pass on the updated node and information on if/how it was 7 | // changed. 8 | // Status(node) creates a Status object that signals no change 9 | class Status { 10 | constructor(changeType, oldNode, newNode, substeps=[]) { 11 | if (!newNode) { 12 | throw Error('node is not defined'); 13 | } 14 | if (changeType === undefined || typeof(changeType) !== 'string') { 15 | throw Error('changetype isn\'t valid'); 16 | } 17 | 18 | this.changeType = changeType; 19 | this.oldNode = oldNode; 20 | this.newNode = newNode; 21 | this.substeps = substeps; 22 | } 23 | 24 | hasChanged() { 25 | return this.changeType !== ChangeTypes.NO_CHANGE; 26 | } 27 | } 28 | 29 | Status.resetChangeGroups = function(node) { 30 | node = node.cloneDeep(); 31 | node.filter(node => node.changeGroup).forEach(change => { 32 | delete change.changeGroup; 33 | }); 34 | return node; 35 | }; 36 | 37 | // A wrapper around the Status constructor for the case where node hasn't 38 | // been changed. 39 | Status.noChange = function(node) { 40 | return new Status(ChangeTypes.NO_CHANGE, null, node); 41 | }; 42 | 43 | // A wrapper around the Status constructor for the case of a change 44 | // that is happening at the level of oldNode + newNode 45 | // e.g. 2 + 2 --> 4 (an addition node becomes a constant node) 46 | Status.nodeChanged = function( 47 | changeType, oldNode, newNode, defaultChangeGroup=true, steps=[]) { 48 | if (defaultChangeGroup) { 49 | oldNode.changeGroup = 1; 50 | newNode.changeGroup = 1; 51 | } 52 | 53 | return new Status(changeType, oldNode, newNode, steps); 54 | }; 55 | 56 | // A wrapper around the Status constructor for the case where there was 57 | // a change that happened deeper `node`'s tree, and `node`'s children must be 58 | // updated to have the newNode/oldNode metadata (changeGroups) 59 | // e.g. (2 + 2) + x --> 4 + x has to update the left argument 60 | Status.childChanged = function(node, childStatus, childArgIndex=null) { 61 | const oldNode = node.cloneDeep(); 62 | const newNode = node.cloneDeep(); 63 | let substeps = childStatus.substeps; 64 | 65 | if (!childStatus.oldNode) { 66 | throw Error ('Expected old node for changeType: ' + childStatus.changeType); 67 | } 68 | 69 | function updateSubsteps(substeps, fn) { 70 | substeps.map((step) => { 71 | step = fn(step); 72 | step.substeps = updateSubsteps(step.substeps, fn); 73 | }); 74 | return substeps; 75 | } 76 | 77 | if (Type.isParenthesis(node)) { 78 | oldNode.content = childStatus.oldNode; 79 | newNode.content = childStatus.newNode; 80 | substeps = updateSubsteps(substeps, (step) => { 81 | const oldNode = node.cloneDeep(); 82 | const newNode = node.cloneDeep(); 83 | oldNode.content = step.oldNode; 84 | newNode.content = step.newNode; 85 | step.oldNode = oldNode; 86 | step.newNode = newNode; 87 | return step; 88 | }); 89 | } 90 | else if ((Type.isOperator(node) || Type.isFunction(node) && 91 | childArgIndex !== null)) { 92 | oldNode.args[childArgIndex] = childStatus.oldNode; 93 | newNode.args[childArgIndex] = childStatus.newNode; 94 | substeps = updateSubsteps(substeps, (step) => { 95 | const oldNode = node.cloneDeep(); 96 | const newNode = node.cloneDeep(); 97 | oldNode.args[childArgIndex] = step.oldNode; 98 | newNode.args[childArgIndex] = step.newNode; 99 | step.oldNode = oldNode; 100 | step.newNode = newNode; 101 | return step; 102 | }); 103 | } 104 | else if (Type.isUnaryMinus(node)) { 105 | oldNode.args[0] = childStatus.oldNode; 106 | newNode.args[0] = childStatus.newNode; 107 | substeps = updateSubsteps(substeps, (step) => { 108 | const oldNode = node.cloneDeep(); 109 | const newNode = node.cloneDeep(); 110 | oldNode.args[0] = step.oldNode; 111 | newNode.args[0] = step.newNode; 112 | step.oldNode = oldNode; 113 | step.newNode = newNode; 114 | return step; 115 | }); 116 | } 117 | else { 118 | throw Error('Unexpected node type: ' + node.type); 119 | } 120 | 121 | return new Status(childStatus.changeType, oldNode, newNode, substeps); 122 | }; 123 | 124 | module.exports = Status; 125 | -------------------------------------------------------------------------------- /lib/node/Type.js: -------------------------------------------------------------------------------- 1 | /* 2 | For determining the type of a mathJS node. 3 | */ 4 | 5 | const NodeType = {}; 6 | 7 | NodeType.isOperator = function(node, operator=null) { 8 | return node.type === 'OperatorNode' && 9 | node.fn !== 'unaryMinus' && 10 | '*+-/^'.includes(node.op) && 11 | (operator ? node.op === operator : true); 12 | }; 13 | 14 | NodeType.isParenthesis = function(node) { 15 | return node.type === 'ParenthesisNode'; 16 | }; 17 | 18 | NodeType.isUnaryMinus = function(node) { 19 | return node.type === 'OperatorNode' && node.fn === 'unaryMinus'; 20 | }; 21 | 22 | NodeType.isFunction = function(node, functionName=null) { 23 | if (node.type !== 'FunctionNode') { 24 | return false; 25 | } 26 | if (functionName && node.fn.name !== functionName) { 27 | return false; 28 | } 29 | return true; 30 | }; 31 | 32 | NodeType.isSymbol = function(node, allowUnaryMinus=false) { 33 | if (node.type === 'SymbolNode') { 34 | return true; 35 | } 36 | else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { 37 | return NodeType.isSymbol(node.args[0], false); 38 | } 39 | else { 40 | return false; 41 | } 42 | }; 43 | 44 | NodeType.isConstant = function(node, allowUnaryMinus=false) { 45 | if (node.type === 'ConstantNode') { 46 | return true; 47 | } 48 | else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { 49 | if (NodeType.isConstant(node.args[0], false)) { 50 | const value = parseFloat(node.args[0].value); 51 | return value >= 0; 52 | } 53 | else { 54 | return false; 55 | } 56 | } 57 | else { 58 | return false; 59 | } 60 | }; 61 | 62 | NodeType.isConstantFraction = function(node, allowUnaryMinus=false) { 63 | if (NodeType.isOperator(node, '/')) { 64 | return node.args.every(n => NodeType.isConstant(n, allowUnaryMinus)); 65 | } 66 | else { 67 | return false; 68 | } 69 | }; 70 | 71 | NodeType.isConstantOrConstantFraction = function(node, allowUnaryMinus=false) { 72 | if (NodeType.isConstant(node, allowUnaryMinus) || 73 | NodeType.isConstantFraction(node, allowUnaryMinus)) { 74 | return true; 75 | } 76 | else { 77 | return false; 78 | } 79 | }; 80 | 81 | NodeType.isIntegerFraction = function(node, allowUnaryMinus=false) { 82 | if (!NodeType.isConstantFraction(node, allowUnaryMinus)) { 83 | return false; 84 | } 85 | let numerator = node.args[0]; 86 | let denominator = node.args[1]; 87 | if (allowUnaryMinus) { 88 | if (NodeType.isUnaryMinus(numerator)) { 89 | numerator = numerator.args[0]; 90 | } 91 | if (NodeType.isUnaryMinus(denominator)) { 92 | denominator = denominator.args[0]; 93 | } 94 | } 95 | return (Number.isInteger(parseFloat(numerator.value)) && 96 | Number.isInteger(parseFloat(denominator.value))); 97 | }; 98 | 99 | module.exports = NodeType; 100 | -------------------------------------------------------------------------------- /lib/node/index.js: -------------------------------------------------------------------------------- 1 | const Creator = require('./Creator'); 2 | const CustomType = require('./CustomType'); 3 | const MixedNumber = require('./MixedNumber'); 4 | const NthRootTerm = require('./NthRootTerm'); 5 | const PolynomialTerm = require('./PolynomialTerm'); 6 | const Status = require('./Status'); 7 | const Term = require('./Term'); 8 | const Type = require('./Type'); 9 | 10 | module.exports = { 11 | Creator, 12 | CustomType, 13 | MixedNumber, 14 | NthRootTerm, 15 | PolynomialTerm, 16 | Status, 17 | Term, 18 | Type, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/simplifyExpression/arithmeticSearch/index.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const evaluate = require('../../util/evaluate'); 3 | const Node = require('../../node'); 4 | const TreeSearch = require('../../TreeSearch'); 5 | 6 | // Searches through the tree, prioritizing deeper nodes, and evaluates 7 | // arithmetic (e.g. 2+2 or 3*5*2) on an operation node if possible. 8 | // Returns a Node.Status object. 9 | const search = TreeSearch.postOrder(arithmetic); 10 | 11 | // evaluates arithmetic (e.g. 2+2 or 3*5*2) on an operation node. 12 | // Returns a Node.Status object. 13 | function arithmetic(node) { 14 | if (!Node.Type.isOperator(node)) { 15 | return Node.Status.noChange(node); 16 | } 17 | if (!node.args.every(child => Node.Type.isConstant(child, true))) { 18 | return Node.Status.noChange(node); 19 | } 20 | 21 | // we want to eval each arg so unary minuses around constant nodes become 22 | // constant nodes with negative values 23 | node.args.forEach((arg, i) => { 24 | node.args[i] = Node.Creator.constant(evaluate(arg)); 25 | }); 26 | 27 | // Only resolve division of integers if we get an integer result. 28 | // Note that a fraction of decimals will be divided out. 29 | if (Node.Type.isIntegerFraction(node)) { 30 | const numeratorValue = parseInt(node.args[0]); 31 | const denominatorValue = parseInt(node.args[1]); 32 | if (numeratorValue % denominatorValue === 0) { 33 | const newNode = Node.Creator.constant(numeratorValue/denominatorValue); 34 | return Node.Status.nodeChanged( 35 | ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode); 36 | } 37 | else { 38 | return Node.Status.noChange(node); 39 | } 40 | } 41 | else { 42 | const evaluatedValue = evaluateAndRound(node); 43 | const newNode = Node.Creator.constant(evaluatedValue); 44 | return Node.Status.nodeChanged(ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode); 45 | } 46 | } 47 | 48 | // Evaluates a math expression to a constant, e.g. 3+4 -> 7 and rounds if 49 | // necessary 50 | function evaluateAndRound(node) { 51 | let result = evaluate(node); 52 | if (Math.abs(result) < 1) { 53 | result = parseFloat(result.toPrecision(4)); 54 | } 55 | else { 56 | result = parseFloat(result.toFixed(4)); 57 | } 58 | return result; 59 | } 60 | 61 | module.exports = search; 62 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // Converts a mixed number to an improper fraction 5 | // e.g. 1 2/3 -> 5/3 6 | // All comments in the function are based on this example 7 | function convertMixedNumberToImproperFraction(node) { 8 | if (!Node.MixedNumber.isMixedNumber(node)) { 9 | return Node.Status.noChange(node); 10 | } 11 | 12 | const substeps = []; 13 | let newNode = node.cloneDeep(); 14 | 15 | // e.g. 1 2/3 16 | const wholeNumber = Node.MixedNumber.getWholeNumberValue(node); // 1 17 | const numerator = Node.MixedNumber.getNumeratorValue(node); // 2 18 | const denominator = Node.MixedNumber.getDenominatorValue(node); // 3 19 | const isNegativeMixedNumber = Node.MixedNumber.isNegativeMixedNumber(node); 20 | 21 | // STEP 1: Convert to unsimplified improper fraction 22 | // e.g. 1 2/3 -> ((1 * 3) + 2) / 3 23 | let status = convertToUnsimplifiedImproperFraction( 24 | newNode, wholeNumber, numerator, denominator, isNegativeMixedNumber); 25 | substeps.push(status); 26 | newNode = Node.Status.resetChangeGroups(status.newNode); 27 | 28 | // STEP 2: Simplify multiplication in numerator 29 | // e.g. ((1 * 3) + 2) / 3 -> (3 + 2) / 3 30 | status = simplifyMultiplicationInImproperFraction( 31 | newNode, wholeNumber, numerator, denominator, isNegativeMixedNumber); 32 | substeps.push(status); 33 | newNode = Node.Status.resetChangeGroups(status.newNode); 34 | 35 | // STEP 3: Simplify addition in numerator 36 | // e.g. (3 + 2) / 3 -> 5/3 37 | status = simplifyAdditionInImproperFraction( 38 | newNode, wholeNumber, numerator, denominator, isNegativeMixedNumber); 39 | substeps.push(status); 40 | newNode = Node.Status.resetChangeGroups(status.newNode); 41 | 42 | return Node.Status.nodeChanged( 43 | ChangeTypes.CONVERT_MIXED_NUMBER_TO_IMPROPER_FRACTION, 44 | node, newNode, true, substeps); 45 | } 46 | 47 | // Convert a mixed number to an unsimplified proper fraction 48 | // e.g. 1 2/3 -> ((1 * 3) + 2) / 3 49 | function convertToUnsimplifiedImproperFraction( 50 | oldNode, wholeNumber, numerator, denominator, isNegativeMixedNumber) { 51 | // (wholeNumber * denominator) 52 | // e.g. (1 * 3) 53 | const newNumeratorMultiplication = Node.Creator.parenthesis( 54 | Node.Creator.operator( 55 | '*', 56 | [Node.Creator.constant(wholeNumber), 57 | Node.Creator.constant(denominator)])); 58 | 59 | // (wholeNumber * denominator) + numerator 60 | // e.g. (1 * 3) + 2 61 | const newNumerator = Node.Creator.operator( 62 | '+', 63 | [newNumeratorMultiplication, Node.Creator.constant(numerator)]); 64 | oldNode.args[0].args[0].changeGroup = 1; 65 | newNumerator.changeGroup = 1; 66 | 67 | // e.g. 3 68 | const newDenominator = Node.Creator.constant(denominator); 69 | 70 | let newNode = Node.Creator.operator( 71 | '/', [newNumerator, newDenominator]); 72 | 73 | if (isNegativeMixedNumber) { 74 | newNode = Node.Creator.unaryMinus(newNode); 75 | } 76 | 77 | return Node.Status.nodeChanged( 78 | ChangeTypes.IMPROPER_FRACTION_NUMERATOR, oldNode, newNode, false); 79 | } 80 | 81 | // Simplify multiplication in the numerator of an improper fraction 82 | // e.g. ((1 * 3) + 2) / 3 -> (3 + 2) / 3 83 | function simplifyMultiplicationInImproperFraction( 84 | oldNode, wholeNumber, numerator, denominator, isNegativeMixedNumber) { 85 | // (wholeNumber * denominator) + numerator 86 | // e.g. 3 + 2 87 | const newNumerator = Node.Creator.operator( 88 | '+', 89 | [Node.Creator.constant(wholeNumber * denominator), 90 | Node.Creator.constant(numerator)]); 91 | oldNode.args[0].changeGroup = 1; 92 | newNumerator.changeGroup = 1; 93 | 94 | // e.g. 3 95 | const newDenominator = Node.Creator.constant(denominator); 96 | 97 | let newNode = Node.Creator.operator( 98 | '/', [newNumerator, newDenominator]); 99 | 100 | if (isNegativeMixedNumber) { 101 | newNode = Node.Creator.unaryMinus(newNode); 102 | } 103 | 104 | return Node.Status.nodeChanged( 105 | ChangeTypes.SIMPLIFY_ARITHMETIC, oldNode, newNode, false); 106 | } 107 | 108 | // Simplify addition in the numerator of an improper fraction 109 | // e.g. (3 + 2) / 3 -> 5/3 110 | function simplifyAdditionInImproperFraction( 111 | oldNode, wholeNumber, numerator, denominator, isNegativeMixedNumber) { 112 | // (wholeNumber * denominator) + numerator 113 | // e.g. 5 114 | const newNumerator = Node.Creator.constant( 115 | wholeNumber * denominator + numerator); 116 | oldNode.args[0].changeGroup = 1; 117 | newNumerator.changeGroup = 1; 118 | 119 | // e.g. 3 120 | const newDenominator = Node.Creator.constant(denominator); 121 | 122 | let newNode = Node.Creator.operator( 123 | '/', [newNumerator, newDenominator]); 124 | 125 | if (isNegativeMixedNumber) { 126 | newNode = Node.Creator.unaryMinus(newNode); 127 | } 128 | 129 | return Node.Status.nodeChanged( 130 | ChangeTypes.SIMPLIFY_ARITHMETIC, oldNode, newNode, false); 131 | } 132 | 133 | module.exports = convertMixedNumberToImproperFraction; 134 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Performs simpifications that are more basic and overaching like (...)^0 => 1 3 | * These are always the first simplifications that are attempted. 4 | */ 5 | 6 | const Node = require('../../node'); 7 | const TreeSearch = require('../../TreeSearch'); 8 | 9 | const convertMixedNumberToImproperFraction = require('./convertMixedNumberToImproperFraction'); 10 | const rearrangeCoefficient = require('./rearrangeCoefficient'); 11 | const reduceExponentByZero = require('./reduceExponentByZero'); 12 | const reduceMultiplicationByZero = require('./reduceMultiplicationByZero'); 13 | const reduceZeroDividedByAnything = require('./reduceZeroDividedByAnything'); 14 | const removeAdditionOfZero = require('./removeAdditionOfZero'); 15 | const removeDivisionByOne = require('./removeDivisionByOne'); 16 | const removeExponentBaseOne = require('./removeExponentBaseOne'); 17 | const removeExponentByOne = require('./removeExponentByOne'); 18 | const removeMultiplicationByNegativeOne = require('./removeMultiplicationByNegativeOne'); 19 | const removeMultiplicationByOne = require('./removeMultiplicationByOne'); 20 | const simplifyDoubleUnaryMinus = require('./simplifyDoubleUnaryMinus'); 21 | 22 | const SIMPLIFICATION_FUNCTIONS = [ 23 | // convert mixed numbers to improper fractions 24 | convertMixedNumberToImproperFraction, 25 | // multiplication by 0 yields 0 26 | reduceMultiplicationByZero, 27 | // division of 0 by something yields 0 28 | reduceZeroDividedByAnything, 29 | // ____^0 --> 1 30 | reduceExponentByZero, 31 | // Check for x^1 which should be reduced to x 32 | removeExponentByOne, 33 | // Check for 1^x which should be reduced to 1 34 | // if x can be simplified to a constant 35 | removeExponentBaseOne, 36 | // - - becomes + 37 | simplifyDoubleUnaryMinus, 38 | // If this is a + node and one of the operands is 0, get rid of the 0 39 | removeAdditionOfZero, 40 | // If this is a * node and one of the operands is 1, get rid of the 1 41 | removeMultiplicationByOne, 42 | // In some cases, remove multiplying by -1 43 | removeMultiplicationByNegativeOne, 44 | // If this is a / node and the denominator is 1 or -1, get rid of it 45 | removeDivisionByOne, 46 | // e.g. x*5 -> 5x 47 | rearrangeCoefficient, 48 | ]; 49 | 50 | const search = TreeSearch.preOrder(basics); 51 | 52 | // Look for basic step(s) to perform on a node. Returns a Node.Status object. 53 | function basics(node) { 54 | for (let i = 0; i < SIMPLIFICATION_FUNCTIONS.length; i++) { 55 | const nodeStatus = SIMPLIFICATION_FUNCTIONS[i](node); 56 | if (nodeStatus.hasChanged()) { 57 | return nodeStatus; 58 | } 59 | else { 60 | node = nodeStatus.newNode; 61 | } 62 | } 63 | return Node.Status.noChange(node); 64 | } 65 | 66 | module.exports = search; 67 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/rearrangeCoefficient.js: -------------------------------------------------------------------------------- 1 | const checks = require('../../checks'); 2 | 3 | const ChangeTypes = require('../../ChangeTypes'); 4 | const Node = require('../../node'); 5 | 6 | // Rearranges something of the form x * 5 to be 5x, ie putting the coefficient 7 | // in the right place. 8 | // Returns a Node.Status object 9 | function rearrangeCoefficient(node) { 10 | if (!checks.canRearrangeCoefficient(node)) { 11 | return Node.Status.noChange(node); 12 | } 13 | 14 | let newNode = node.cloneDeep(); 15 | 16 | const polyNode = new Node.PolynomialTerm(newNode.args[0]); 17 | const constNode = newNode.args[1]; 18 | const exponentNode = polyNode.getExponentNode(); 19 | newNode = Node.Creator.polynomialTerm( 20 | polyNode.getSymbolNode(), exponentNode, constNode); 21 | 22 | return Node.Status.nodeChanged( 23 | ChangeTypes.REARRANGE_COEFF, node, newNode); 24 | } 25 | 26 | module.exports = rearrangeCoefficient; 27 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/reduceExponentByZero.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // If `node` is an exponent of something to 0, we can reduce that to just 1. 5 | // Returns a Node.Status object. 6 | function reduceExponentByZero(node) { 7 | if (node.op !== '^') { 8 | return Node.Status.noChange(node); 9 | } 10 | const exponent = node.args[1]; 11 | if (Node.Type.isConstant(exponent) && exponent.value === '0') { 12 | const newNode = Node.Creator.constant(1); 13 | return Node.Status.nodeChanged( 14 | ChangeTypes.REDUCE_EXPONENT_BY_ZERO, node, newNode); 15 | } 16 | else { 17 | return Node.Status.noChange(node); 18 | } 19 | } 20 | 21 | module.exports = reduceExponentByZero; 22 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // If `node` is a multiplication node with 0 as one of its operands, 5 | // reduce the node to 0. Returns a Node.Status object. 6 | function reduceMultiplicationByZero(node) { 7 | if (node.op !== '*') { 8 | return Node.Status.noChange(node); 9 | } 10 | const zeroIndex = node.args.findIndex(arg => { 11 | if (Node.Type.isConstant(arg) && arg.value === '0') { 12 | return true; 13 | } 14 | if (Node.PolynomialTerm.isPolynomialTerm(arg)) { 15 | const polyTerm = new Node.PolynomialTerm(arg); 16 | return polyTerm.getCoeffValue() === 0; 17 | } 18 | return false; 19 | }); 20 | if (zeroIndex >= 0) { 21 | // reduce to just the 0 node 22 | const newNode = Node.Creator.constant(0); 23 | return Node.Status.nodeChanged( 24 | ChangeTypes.MULTIPLY_BY_ZERO, node, newNode); 25 | } 26 | else { 27 | return Node.Status.noChange(node); 28 | } 29 | } 30 | 31 | module.exports = reduceMultiplicationByZero; 32 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // If `node` is a fraction with 0 as the numerator, reduce the node to 0. 5 | // Returns a Node.Status object. 6 | function reduceZeroDividedByAnything(node) { 7 | if (node.op !== '/') { 8 | return Node.Status.noChange(node); 9 | } 10 | if (node.args[0].value === '0') { 11 | const newNode = Node.Creator.constant(0); 12 | return Node.Status.nodeChanged( 13 | ChangeTypes.REDUCE_ZERO_NUMERATOR, node, newNode); 14 | } 15 | else { 16 | return Node.Status.noChange(node); 17 | } 18 | } 19 | 20 | module.exports = reduceZeroDividedByAnything; 21 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/removeAdditionOfZero.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // If `node` is an addition node with 0 as one of its operands, 5 | // remove 0 from the operands list. Returns a Node.Status object. 6 | function removeAdditionOfZero(node) { 7 | if (node.op !== '+') { 8 | return Node.Status.noChange(node); 9 | } 10 | const zeroIndex = node.args.findIndex(arg => { 11 | return Node.Type.isConstant(arg) && arg.value === '0'; 12 | }); 13 | let newNode = node.cloneDeep(); 14 | if (zeroIndex >= 0) { 15 | // remove the 0 node 16 | newNode.args.splice(zeroIndex, 1); 17 | // if there's only one operand left, there's nothing left to add it to, 18 | // so move it up the tree 19 | if (newNode.args.length === 1) { 20 | newNode = newNode.args[0]; 21 | } 22 | return Node.Status.nodeChanged( 23 | ChangeTypes.REMOVE_ADDING_ZERO, node, newNode); 24 | } 25 | return Node.Status.noChange(node); 26 | } 27 | 28 | module.exports = removeAdditionOfZero; 29 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/removeDivisionByOne.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Negative = require('../../Negative'); 3 | const Node = require('../../node'); 4 | 5 | // If `node` is a division operation of something by 1 or -1, we can remove the 6 | // denominator. Returns a Node.Status object. 7 | function removeDivisionByOne(node) { 8 | if (node.op !== '/') { 9 | return Node.Status.noChange(node); 10 | } 11 | const denominator = node.args[1]; 12 | if (!Node.Type.isConstant(denominator)) { 13 | return Node.Status.noChange(node); 14 | } 15 | // It's taken 40ms on average to pass distribution test, 16 | // TODO: see if we should keep using utils/clone here 17 | let numerator = node.args[0].cloneDeep(); 18 | 19 | // if denominator is -1, we make the numerator negative 20 | if (parseFloat(denominator.value) === -1) { 21 | // If the numerator was an operation, wrap it in parens before adding - 22 | // to the front. 23 | // e.g. 2+3 / -1 ---> -(2+3) 24 | if (Node.Type.isOperator(numerator)) { 25 | numerator = Node.Creator.parenthesis(numerator); 26 | } 27 | const changeType = Negative.isNegative(numerator) ? 28 | ChangeTypes.RESOLVE_DOUBLE_MINUS : 29 | ChangeTypes.DIVISION_BY_NEGATIVE_ONE; 30 | numerator = Negative.negate(numerator); 31 | return Node.Status.nodeChanged(changeType, node, numerator); 32 | } 33 | else if (parseFloat(denominator.value) === 1) { 34 | return Node.Status.nodeChanged( 35 | ChangeTypes.DIVISION_BY_ONE, node, numerator); 36 | } 37 | else { 38 | return Node.Status.noChange(node); 39 | } 40 | } 41 | 42 | module.exports = removeDivisionByOne; 43 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/removeExponentBaseOne.js: -------------------------------------------------------------------------------- 1 | const checks = require('../../checks'); 2 | 3 | const ChangeTypes = require('../../ChangeTypes'); 4 | const Node = require('../../node'); 5 | 6 | // If `node` is of the form 1^x, reduces it to a node of the form 1. 7 | // Returns a Node.Status object. 8 | function removeExponentBaseOne(node) { 9 | if (node.op === '^' && // an exponent with 10 | checks.resolvesToConstant(node.args[1]) && // a power not a symbol and 11 | Node.Type.isConstant(node.args[0]) && // a constant base 12 | node.args[0].value === '1') { // of value 1 13 | const newNode = node.args[0].cloneDeep(); 14 | return Node.Status.nodeChanged( 15 | ChangeTypes.REMOVE_EXPONENT_BASE_ONE, node, newNode); 16 | } 17 | return Node.Status.noChange(node); 18 | } 19 | 20 | module.exports = removeExponentBaseOne; 21 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/removeExponentByOne.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // If `node` is of the form x^1, reduces it to a node of the form x. 5 | // Returns a Node.Status object. 6 | function removeExponentByOne(node) { 7 | if (node.op === '^' && // exponent of anything 8 | Node.Type.isConstant(node.args[1]) && // to a constant 9 | node.args[1].value === '1') { // of value 1 10 | const newNode = node.args[0].cloneDeep(); 11 | return Node.Status.nodeChanged( 12 | ChangeTypes.REMOVE_EXPONENT_BY_ONE, node, newNode); 13 | } 14 | return Node.Status.noChange(node); 15 | } 16 | 17 | module.exports = removeExponentByOne; 18 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Negative = require('../../Negative'); 3 | const Node = require('../../node'); 4 | 5 | // If `node` is a multiplication node with -1 as one of its operands, 6 | // and a non constant as the next operand, remove -1 from the operands 7 | // list and make the next term have a unary minus. 8 | // Returns a Node.Status object. 9 | function removeMultiplicationByNegativeOne(node) { 10 | if (node.op !== '*') { 11 | return Node.Status.noChange(node); 12 | } 13 | const minusOneIndex = node.args.findIndex(arg => { 14 | return Node.Type.isConstant(arg) && arg.value === '-1'; 15 | }); 16 | if (minusOneIndex < 0) { 17 | return Node.Status.noChange(node); 18 | } 19 | 20 | // We might merge/combine the negative one into another node. This stores 21 | // the index of that other node in the arg list. 22 | let nodeToCombineIndex; 23 | // If minus one is the last term, maybe combine with the term before 24 | if (minusOneIndex + 1 === node.args.length) { 25 | nodeToCombineIndex = minusOneIndex - 1; 26 | } 27 | else { 28 | nodeToCombineIndex = minusOneIndex + 1; 29 | } 30 | 31 | let nodeToCombine = node.args[nodeToCombineIndex]; 32 | // If it's a constant, the combining of those terms is handled elsewhere. 33 | if (Node.Type.isConstant(nodeToCombine)) { 34 | return Node.Status.noChange(node); 35 | } 36 | 37 | let newNode = node.cloneDeep(); 38 | 39 | // Get rid of the -1 40 | nodeToCombine = Negative.negate(nodeToCombine.cloneDeep()); 41 | 42 | // replace the node next to -1 and remove -1 43 | newNode.args[nodeToCombineIndex] = nodeToCombine; 44 | newNode.args.splice(minusOneIndex, 1); 45 | 46 | // if there's only one operand left, move it up the tree 47 | if (newNode.args.length === 1) { 48 | newNode = newNode.args[0]; 49 | } 50 | return Node.Status.nodeChanged( 51 | ChangeTypes.REMOVE_MULTIPLYING_BY_NEGATIVE_ONE, node, newNode); 52 | } 53 | 54 | module.exports = removeMultiplicationByNegativeOne; 55 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/removeMultiplicationByOne.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // If `node` is a multiplication node with 1 as one of its operands, 5 | // remove 1 from the operands list. Returns a Node.Status object. 6 | function removeMultiplicationByOne(node) { 7 | if (node.op !== '*') { 8 | return Node.Status.noChange(node); 9 | } 10 | const oneIndex = node.args.findIndex(arg => { 11 | return Node.Type.isConstant(arg) && arg.value === '1'; 12 | }); 13 | if (oneIndex >= 0) { 14 | let newNode = node.cloneDeep(); 15 | // remove the 1 node 16 | newNode.args.splice(oneIndex, 1); 17 | // if there's only one operand left, there's nothing left to multiply it 18 | // to, so move it up the tree 19 | if (newNode.args.length === 1) { 20 | newNode = newNode.args[0]; 21 | } 22 | return Node.Status.nodeChanged( 23 | ChangeTypes.REMOVE_MULTIPLYING_BY_ONE, node, newNode); 24 | } 25 | return Node.Status.noChange(node); 26 | } 27 | 28 | module.exports = removeMultiplicationByOne; 29 | -------------------------------------------------------------------------------- /lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | 4 | // Simplifies two unary minuses in a row by removing both of them. 5 | // e.g. -(- 4) --> 4 6 | function simplifyDoubleUnaryMinus(node) { 7 | if (!Node.Type.isUnaryMinus(node)) { 8 | return Node.Status.noChange(node); 9 | } 10 | const unaryArg = node.args[0]; 11 | // e.g. in - -x, -x is the unary arg, and we'd want to reduce to just x 12 | if (Node.Type.isUnaryMinus(unaryArg)) { 13 | const newNode = unaryArg.args[0].cloneDeep(); 14 | return Node.Status.nodeChanged( 15 | ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); 16 | } 17 | // e.g. - -4, -4 could be a constant with negative value 18 | else if (Node.Type.isConstant(unaryArg) && parseFloat(unaryArg.value) < 0) { 19 | const newNode = Node.Creator.constant(parseFloat(unaryArg.value) * -1); 20 | return Node.Status.nodeChanged( 21 | ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); 22 | } 23 | // e.g. -(-(5+2)) 24 | else if (Node.Type.isParenthesis(unaryArg)) { 25 | const parenthesisNode = unaryArg; 26 | const parenthesisContent = parenthesisNode.content; 27 | if (Node.Type.isUnaryMinus(parenthesisContent)) { 28 | const newNode = Node.Creator.parenthesis(parenthesisContent.args[0]); 29 | return Node.Status.nodeChanged( 30 | ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); 31 | } 32 | } 33 | return Node.Status.noChange(node); 34 | } 35 | 36 | module.exports = simplifyDoubleUnaryMinus; 37 | -------------------------------------------------------------------------------- /lib/simplifyExpression/breakUpNumeratorSearch/index.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | const TreeSearch = require('../../TreeSearch'); 4 | 5 | // Breaks up any fraction (deeper nodes getting priority) that has a numerator 6 | // that is a sum. e.g. (2+x)/5 -> (2/5 + x/5) 7 | // This step must happen after things have been collected and combined, or 8 | // else things will infinite loop, so it's a tree search of its own. 9 | // Returns a Node.Status object 10 | const search = TreeSearch.postOrder(breakUpNumerator); 11 | 12 | // If `node` is a fraction with a numerator that is a sum, breaks up the 13 | // fraction e.g. (2+x)/5 -> (2/5 + x/5) 14 | // Returns a Node.Status object 15 | function breakUpNumerator(node) { 16 | if (!Node.Type.isOperator(node) || node.op !== '/') { 17 | return Node.Status.noChange(node); 18 | } 19 | let numerator = node.args[0]; 20 | if (Node.Type.isParenthesis(numerator)) { 21 | numerator = numerator.content; 22 | } 23 | if (!Node.Type.isOperator(numerator) || numerator.op !== '+') { 24 | return Node.Status.noChange(node); 25 | } 26 | 27 | // At this point, we know that node is a fraction and its numerator is a sum 28 | // of terms that can't be collected or combined, so we should break it up. 29 | const fractionList = []; 30 | const denominator = node.args[1]; 31 | numerator.args.forEach(arg => { 32 | const newFraction = Node.Creator.operator('/', [arg, denominator]); 33 | newFraction.changeGroup = 1; 34 | fractionList.push(newFraction); 35 | }); 36 | 37 | let newNode = Node.Creator.operator('+', fractionList); 38 | // Wrap in parens for cases like 2*(2+3)/5 => 2*(2/5 + 3/5) 39 | newNode = Node.Creator.parenthesis(newNode); 40 | node.changeGroup = 1; 41 | return Node.Status.nodeChanged( 42 | ChangeTypes.BREAK_UP_FRACTION, node, newNode, false); 43 | } 44 | 45 | module.exports = search; 46 | -------------------------------------------------------------------------------- /lib/simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower.js: -------------------------------------------------------------------------------- 1 | const NodeCreator = require('../../node/Creator'); 2 | const NodeType = require('../../node/Type'); 3 | 4 | // This module is needed when simplifying multiplication of constant powers 5 | // as it contains functions to get different parts of the node instead of 6 | // creating a new class, like polynomialTerm. The functions can return the base 7 | // and the exponent of the power, it can also check if the node is constant or 8 | // constant power. 9 | // e.g 2^10 is an constant power, while x^10 is not 10 | // e.g 2 is an constant, while x is not 11 | 12 | // Returns the base if the node is on power form 13 | // else returns the node as it is constant. 14 | // e.g 2^4 returns 2 15 | // e.g 3 returns 3, since 3 is equal to 3^1 which has a base of 3 16 | function getBaseNode(node) { 17 | if (node.args) { 18 | return node.args[0]; 19 | } 20 | else { 21 | return node; 22 | } 23 | } 24 | 25 | // Returns the node that is an exponent to a constant, or a constant node with 26 | // value 1 if there's no exponent. 27 | // e.g. on the node representing 2^3, returns a constant node with value 3 28 | // e.g 3 returns 1, since 3 is equal to 3^1 which has an exponent of 1 29 | function getExponentNode(node) { 30 | if (NodeType.isConstant(node)) { 31 | return NodeCreator.constant(1); 32 | } 33 | else { 34 | return node.args[1]; 35 | } 36 | } 37 | 38 | // Checks if the node is an constant or a power with constant base. 39 | // e.g. 2^3 is a constant power node, 5 is a constant node, x and x^2 are not 40 | function isConstantOrConstantPower(node) { 41 | return ((NodeType.isOperator(node, '^') && 42 | NodeType.isConstant(node.args[0])) || 43 | NodeType.isConstant(node)); 44 | } 45 | 46 | module.exports = { 47 | getBaseNode, 48 | getExponentNode, 49 | isConstantOrConstantPower 50 | }; 51 | -------------------------------------------------------------------------------- /lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.js: -------------------------------------------------------------------------------- 1 | const addConstantAndFraction = require('../fractionsSearch/addConstantAndFraction'); 2 | const addConstantFractions = require('../fractionsSearch/addConstantFractions'); 3 | const arithmeticSearch = require('../arithmeticSearch'); 4 | 5 | const ChangeTypes = require('../../ChangeTypes'); 6 | const Node = require('../../node'); 7 | 8 | // Evaluates a sum of constant numbers and integer fractions to a single 9 | // constant number or integer fraction. e.g. e.g. 2/3 + 5 + 5/2 => 49/6 10 | // Returns a Node.Status object. 11 | function evaluateConstantSum(node) { 12 | if (Node.Type.isParenthesis(node)) { 13 | node = node.content; 14 | } 15 | if (!Node.Type.isOperator(node) || node.op !== '+') { 16 | return Node.Status.noChange(node); 17 | } 18 | if (node.args.some(node => !Node.Type.isConstantOrConstantFraction(node))) { 19 | return Node.Status.noChange(node); 20 | } 21 | 22 | // functions needed to evaluate the sum 23 | const summingFunctions = [ 24 | arithmeticSearch, 25 | addConstantFractions, 26 | addConstantAndFraction, 27 | ]; 28 | for (let i = 0; i < summingFunctions.length; i++) { 29 | const status = summingFunctions[i](node); 30 | if (status.hasChanged()) { 31 | if (Node.Type.isConstantOrConstantFraction(status.newNode)) { 32 | return status; 33 | } 34 | } 35 | } 36 | 37 | let newNode = node.cloneDeep(); 38 | const substeps = []; 39 | let status; 40 | 41 | // STEP 1: group fractions and constants separately 42 | status = groupConstantsAndFractions(newNode); 43 | substeps.push(status); 44 | newNode = Node.Status.resetChangeGroups(status.newNode); 45 | 46 | const constants = newNode.args[0]; 47 | const fractions = newNode.args[1]; 48 | 49 | // STEP 2A: evaluate arithmetic IF there's > 1 constant 50 | // (which is the case if it's a list surrounded by parenthesis) 51 | if (Node.Type.isParenthesis(constants)) { 52 | const constantList = constants.content; 53 | const evaluateStatus = arithmeticSearch(constantList); 54 | status = Node.Status.childChanged(newNode, evaluateStatus, 0); 55 | substeps.push(status); 56 | newNode = Node.Status.resetChangeGroups(status.newNode); 57 | } 58 | 59 | // STEP 2B: add fractions IF there's > 1 fraction 60 | // (which is the case if it's a list surrounded by parenthesis) 61 | if (Node.Type.isParenthesis(fractions)) { 62 | const fractionList = fractions.content; 63 | const evaluateStatus = addConstantFractions(fractionList); 64 | status = Node.Status.childChanged(newNode, evaluateStatus, 1); 65 | substeps.push(status); 66 | newNode = Node.Status.resetChangeGroups(status.newNode); 67 | } 68 | 69 | // STEP 3: combine the evaluated constant and fraction 70 | // the fraction might have simplified to a constant (e.g. 1/3 + 2/3 -> 2) 71 | // so we just call evaluateConstantSum again to cycle through 72 | status = evaluateConstantSum(newNode); 73 | substeps.push(status); 74 | newNode = Node.Status.resetChangeGroups(status.newNode); 75 | 76 | return Node.Status.nodeChanged( 77 | ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode, true, substeps); 78 | } 79 | 80 | // If we can't combine using one of those functions, there's a mix of > 2 81 | // fractions and constants. So we need to group them together so we can later 82 | // add them. 83 | // Expects a node that is a sum of integer fractions and constants. 84 | // Returns a Node.Status object. 85 | // e.g. 2/3 + 5 + 5/2 => (2/3 + 5/2) + 5 86 | function groupConstantsAndFractions(node) { 87 | let fractions = node.args.filter(Node.Type.isIntegerFraction); 88 | let constants = node.args.filter(Node.Type.isConstant); 89 | 90 | if (fractions.length === 0 || constants.length === 0) { 91 | throw Error('expected both integer fractions and constants, got ' + node); 92 | } 93 | 94 | if (fractions.length + constants.length !== node.args.length) { 95 | throw Error('can only evaluate integer fractions and constants'); 96 | } 97 | 98 | constants = constants.map(node => { 99 | // set the changeGroup - this affects both the old and new node 100 | node.changeGroup = 1; 101 | // clone so that node and newNode aren't stored in the same memory 102 | return node.cloneDeep(); 103 | }); 104 | // wrap in parenthesis if there's more than one, to group them 105 | if (constants.length > 1) { 106 | constants = Node.Creator.parenthesis(Node.Creator.operator('+', constants)); 107 | } 108 | else { 109 | constants = constants[0]; 110 | } 111 | 112 | fractions = fractions.map(node => { 113 | // set the changeGroup - this affects both the old and new node 114 | node.changeGroup = 2; 115 | // clone so that node and newNode aren't stored in the same memory 116 | return node.cloneDeep(); 117 | }); 118 | // wrap in parenthesis if there's more than one, to group them 119 | if (fractions.length > 1) { 120 | fractions = Node.Creator.parenthesis(Node.Creator.operator('+', fractions)); 121 | } 122 | else { 123 | fractions = fractions[0]; 124 | } 125 | 126 | const newNode = Node.Creator.operator('+', [constants, fractions]); 127 | return Node.Status.nodeChanged( 128 | ChangeTypes.COLLECT_LIKE_TERMS, node, newNode); 129 | } 130 | 131 | module.exports = evaluateConstantSum; 132 | -------------------------------------------------------------------------------- /lib/simplifyExpression/collectAndCombineSearch/index.js: -------------------------------------------------------------------------------- 1 | // Collects and combines like terms 2 | 3 | const addLikeTerms = require('./addLikeTerms'); 4 | const checks = require('../../checks'); 5 | const multiplyLikeTerms = require('./multiplyLikeTerms'); 6 | 7 | const ChangeTypes = require('../../ChangeTypes'); 8 | const LikeTermCollector = require('./LikeTermCollector'); 9 | const Node = require('../../node'); 10 | const TreeSearch = require('../../TreeSearch'); 11 | 12 | const termCollectorFunctions = { 13 | '+': addLikeTerms, 14 | '*': multiplyLikeTerms 15 | }; 16 | 17 | // Iterates through the tree looking for like terms to collect and combine. 18 | // Will prioritize deeper expressions. Returns a Node.Status object. 19 | const search = TreeSearch.postOrder(collectAndCombineLikeTerms); 20 | 21 | // Given an operator node, maybe collects and then combines if possible 22 | // e.g. 2x + 4x + y => 6x + y 23 | // e.g. 2x * x^2 * 5x => 10 x^4 24 | function collectAndCombineLikeTerms(node) { 25 | if (node.op === '+') { 26 | const status = collectAndCombineOperation(node); 27 | if (status.hasChanged()) { 28 | return status; 29 | } 30 | // we might also be able to just combine if they're all the same term 31 | // e.g. 2x + 4x + x (doesn't need collecting) 32 | return addLikeTerms(node, true); 33 | } 34 | else if (node.op === '*') { 35 | // collect and combine involves there being coefficients pulled the front 36 | // e.g. 2x * x^2 * 5x => (2*5) * (x * x^2 * x) => ... => 10 x^4 37 | if (checks.canMultiplyLikeTermConstantNodes(node)) { 38 | return multiplyLikeTerms(node, true); 39 | } 40 | const status = collectAndCombineOperation(node); 41 | if (status.hasChanged()) { 42 | // make sure there's no * between the coefficient and the symbol part 43 | status.newNode.implicit = true; 44 | return status; 45 | } 46 | // we might also be able to just combine polynomial terms 47 | // e.g. x * x^2 * x => ... => x^4 48 | return multiplyLikeTerms(node, true); 49 | } 50 | else { 51 | return Node.Status.noChange(node); 52 | } 53 | } 54 | 55 | // Collects and combines (if possible) the arguments of an addition or 56 | // multiplication 57 | function collectAndCombineOperation(node) { 58 | let substeps = []; 59 | 60 | const status = LikeTermCollector.collectLikeTerms(node.cloneDeep()); 61 | if (!status.hasChanged()) { 62 | return status; 63 | } 64 | 65 | // STEP 1: collect like terms, e.g. 2x + 4x^2 + 5x => 4x^2 + (2x + 5x) 66 | substeps.push(status); 67 | let newNode = Node.Status.resetChangeGroups(status.newNode); 68 | 69 | // STEP 2 onwards: combine like terms for each group that can be combined 70 | // e.g. (x + 3x) + (2 + 2) has two groups 71 | const combineSteps = combineLikeTerms(newNode); 72 | if (combineSteps.length > 0) { 73 | substeps = substeps.concat(combineSteps); 74 | const lastStep = combineSteps[combineSteps.length - 1]; 75 | newNode = Node.Status.resetChangeGroups(lastStep.newNode); 76 | } 77 | 78 | return Node.Status.nodeChanged( 79 | ChangeTypes.COLLECT_AND_COMBINE_LIKE_TERMS, 80 | node, newNode, true, substeps); 81 | } 82 | 83 | // step 2 onwards for collectAndCombineOperation 84 | // combine like terms for each group that can be combined 85 | // e.g. (x + 3x) + (2 + 2) has two groups 86 | // returns a list of combine steps 87 | function combineLikeTerms(node) { 88 | const steps = []; 89 | let newNode = node.cloneDeep(); 90 | 91 | for (let i = 0; i < node.args.length; i++) { 92 | let child = node.args[i]; 93 | // All groups of terms will be surrounded by parenthesis 94 | if (!Node.Type.isParenthesis(child)) { 95 | continue; 96 | } 97 | child = child.content; 98 | const childStatus = termCollectorFunctions[newNode.op](child); 99 | if (childStatus.hasChanged()) { 100 | const status = Node.Status.childChanged(newNode, childStatus, i); 101 | steps.push(status); 102 | newNode = Node.Status.resetChangeGroups(status.newNode); 103 | } 104 | } 105 | 106 | return steps; 107 | } 108 | 109 | module.exports = search; 110 | -------------------------------------------------------------------------------- /lib/simplifyExpression/divisionSearch/index.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | const TreeSearch = require('../../TreeSearch'); 4 | 5 | // Searches for and simplifies any chains of division or nested division. 6 | // Returns a Node.Status object 7 | const search = TreeSearch.preOrder(division); 8 | 9 | function division(node) { 10 | if (!Node.Type.isOperator(node) || node.op !== '/') { 11 | return Node.Status.noChange(node); 12 | } 13 | // e.g. 2/(x/6) => 2 * 6/x 14 | let nodeStatus = multiplyByInverse(node); 15 | if (nodeStatus.hasChanged()) { 16 | return nodeStatus; 17 | } 18 | // e.g. 2/x/6 -> 2/(x*6) 19 | nodeStatus = simplifyDivisionChain(node); 20 | if (nodeStatus.hasChanged()) { 21 | return nodeStatus; 22 | } 23 | return Node.Status.noChange(node); 24 | } 25 | 26 | // If `node` is a fraction with a denominator that is also a fraction, multiply 27 | // by the inverse. 28 | // e.g. x/(2/3) -> x * 3/2 29 | function multiplyByInverse(node) { 30 | let denominator = node.args[1]; 31 | if (Node.Type.isParenthesis(denominator)) { 32 | denominator = denominator.content; 33 | } 34 | if (!Node.Type.isOperator(denominator) || denominator.op !== '/') { 35 | return Node.Status.noChange(node); 36 | } 37 | // At this point, we know that node is a fraction and denonimator is the 38 | // fraction we need to inverse. 39 | const inverseNumerator = denominator.args[1]; 40 | const inverseDenominator = denominator.args[0]; 41 | const inverseFraction = Node.Creator.operator( 42 | '/', [inverseNumerator, inverseDenominator]); 43 | 44 | const newNode = Node.Creator.operator('*', [node.args[0], inverseFraction]); 45 | return Node.Status.nodeChanged( 46 | ChangeTypes.MULTIPLY_BY_INVERSE, node, newNode); 47 | } 48 | 49 | // Simplifies any chains of division into a single division operation. 50 | // e.g. 2/x/6 -> 2/(x*6) 51 | // Returns a Node.Status object 52 | function simplifyDivisionChain(node) { 53 | // check for a chain of division 54 | const denominatorList = getDenominatorList(node); 55 | // one for the numerator, and at least two terms in the denominator 56 | if (denominatorList.length > 2) { 57 | const numerator = denominatorList.shift(); 58 | // the new single denominator is all the chained denominators 59 | // multiplied together, in parentheses. 60 | const denominator = Node.Creator.parenthesis( 61 | Node.Creator.operator('*', denominatorList)); 62 | const newNode = Node.Creator.operator('/', [numerator, denominator]); 63 | return Node.Status.nodeChanged( 64 | ChangeTypes.SIMPLIFY_DIVISION, node, newNode); 65 | } 66 | return Node.Status.noChange(node); 67 | } 68 | 69 | // Given a the denominator of a division node, returns all the nested 70 | // denominator nodess. e.g. 2/3/4/5 would return [2,3,4,5] 71 | // (note: all the numbers in the example are actually constant nodes) 72 | function getDenominatorList(denominator) { 73 | let node = denominator; 74 | const denominatorList = []; 75 | while (node.op === '/') { 76 | // unshift the denominator to the front of the list, and recurse on 77 | // the numerator 78 | denominatorList.unshift(node.args[1]); 79 | node = node.args[0]; 80 | } 81 | // unshift the final node, which wasn't a / node 82 | denominatorList.unshift(node); 83 | return denominatorList; 84 | } 85 | 86 | module.exports = search; 87 | -------------------------------------------------------------------------------- /lib/simplifyExpression/fractionsSearch/addConstantAndFraction.js: -------------------------------------------------------------------------------- 1 | const addConstantFractions = require('./addConstantFractions'); 2 | 3 | const ChangeTypes = require('../../ChangeTypes'); 4 | const evaluate = require('../../util/evaluate'); 5 | const Node = require('../../node'); 6 | 7 | // Adds a constant to a fraction by: 8 | // - collapsing the fraction to decimal if the constant is not an integer 9 | // e.g. 5.3 + 1/2 -> 5.3 + 0.2 10 | // - turning the constant into a fraction with the same denominator if it is 11 | // an integer, e.g. 5 + 1/2 -> 10/2 + 1/2 12 | function addConstantAndFraction(node) { 13 | if (!Node.Type.isOperator(node) || node.op !== '+' || node.args.length !== 2) { 14 | return Node.Status.noChange(node); 15 | } 16 | 17 | const firstArg = node.args[0]; 18 | const secondArg = node.args[1]; 19 | let constNode, fractionNode; 20 | if (Node.Type.isConstant(firstArg)) { 21 | if (Node.Type.isIntegerFraction(secondArg)) { 22 | constNode = firstArg; 23 | fractionNode = secondArg; 24 | } 25 | else { 26 | return Node.Status.noChange(node); 27 | } 28 | } 29 | else if (Node.Type.isConstant(secondArg)) { 30 | if (Node.Type.isIntegerFraction(firstArg)) { 31 | constNode = secondArg; 32 | fractionNode = firstArg; 33 | } 34 | else { 35 | return Node.Status.noChange(node); 36 | } 37 | } 38 | else { 39 | return Node.Status.noChange(node); 40 | } 41 | 42 | let newNode = node.cloneDeep(); 43 | let substeps = []; 44 | // newConstNode and newFractionNode will end up both constants, or both 45 | // fractions. I'm naming them based on their original form so we can keep 46 | // track of which is which. 47 | let newConstNode, newFractionNode; 48 | let changeType; 49 | if (Number.isInteger(parseFloat(constNode.value))) { 50 | const denominatorNode = fractionNode.args[1]; 51 | const denominatorValue = parseInt(denominatorNode); 52 | const constNodeValue = parseInt(constNode.value); 53 | const newNumeratorNode = Node.Creator.constant( 54 | constNodeValue * denominatorValue); 55 | newConstNode = Node.Creator.operator( 56 | '/', [newNumeratorNode, denominatorNode]); 57 | newFractionNode = fractionNode; 58 | changeType = ChangeTypes.CONVERT_INTEGER_TO_FRACTION; 59 | } 60 | else { 61 | // round to 4 decimal places 62 | let dividedValue = evaluate(fractionNode); 63 | if (dividedValue < 1) { 64 | dividedValue = parseFloat(dividedValue.toPrecision(4)); 65 | } 66 | else { 67 | dividedValue = parseFloat(dividedValue.toFixed(4)); 68 | } 69 | newFractionNode = Node.Creator.constant(dividedValue); 70 | newConstNode = constNode; 71 | changeType = ChangeTypes.DIVIDE_FRACTION_FOR_ADDITION; 72 | } 73 | 74 | if (Node.Type.isConstant(firstArg)) { 75 | newNode.args[0] = newConstNode; 76 | newNode.args[1] = newFractionNode; 77 | } 78 | else { 79 | newNode.args[0] = newFractionNode; 80 | newNode.args[1] = newConstNode; 81 | } 82 | 83 | substeps.push(Node.Status.nodeChanged(changeType, node, newNode)); 84 | newNode = Node.Status.resetChangeGroups(newNode); 85 | 86 | // If we changed an integer to a fraction, we need to add the steps for 87 | // adding the fractions. 88 | if (changeType === ChangeTypes.CONVERT_INTEGER_TO_FRACTION) { 89 | const addFractionStatus = addConstantFractions(newNode); 90 | substeps = substeps.concat(addFractionStatus.substeps); 91 | } 92 | // Otherwise, add the two constants 93 | else { 94 | const evalNode = Node.Creator.constant(evaluate(newNode)); 95 | substeps.push(Node.Status.nodeChanged( 96 | ChangeTypes.SIMPLIFY_ARITHMETIC, newNode, evalNode)); 97 | } 98 | 99 | const lastStep = substeps[substeps.length - 1]; 100 | newNode = Node.Status.resetChangeGroups(lastStep.newNode); 101 | 102 | return Node.Status.nodeChanged( 103 | ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode, true, substeps); 104 | } 105 | 106 | module.exports = addConstantAndFraction; 107 | -------------------------------------------------------------------------------- /lib/simplifyExpression/fractionsSearch/divideByGCD.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const ChangeTypes = require('../../ChangeTypes'); 4 | const evaluate = require('../../util/evaluate'); 5 | const Node = require('../../node'); 6 | 7 | // Simplifies a fraction (with constant numerator and denominator) by dividing 8 | // the top and bottom by the GCD, if possible. 9 | // e.g. 2/4 --> 1/2 10/5 --> 2x 10 | // Also simplified negative signs 11 | // e.g. -1/-3 --> 1/3 4/-5 --> -4/5 12 | // Note that -4/5 doesn't need to be simplified. 13 | // Note that our goal is for the denominator to always be positive. If it 14 | // isn't, we can simplify signs. 15 | // Returns a Node.Status object 16 | function divideByGCD(fraction) { 17 | if (!Node.Type.isOperator(fraction) || fraction.op !== '/') { 18 | return Node.Status.noChange(fraction); 19 | } 20 | // If it's not an integer fraction, all we can do is simplify signs 21 | if (!Node.Type.isIntegerFraction(fraction, true)) { 22 | return Node.Status.noChange(fraction); 23 | } 24 | 25 | const substeps = []; 26 | let newNode = fraction.cloneDeep(); 27 | 28 | const numeratorValue = parseInt(evaluate(fraction.args[0])); 29 | const denominatorValue = parseInt(evaluate(fraction.args[1])); 30 | 31 | // The gcd is what we're dividing the numerator and denominator by. 32 | let gcd = math.gcd(numeratorValue, denominatorValue); 33 | // A greatest common denominator is technically defined as always positive, 34 | // but since our goal is to reduce negative signs or move them to the 35 | // numerator, a negative denominator always means we want to flip signs 36 | // of both numerator and denominator. 37 | // e.g. -1/-3 --> 1/3 4/-5 --> -4/5 38 | if (denominatorValue < 0) { 39 | gcd *= -1; 40 | } 41 | 42 | if (gcd === 1) { 43 | return Node.Status.noChange(fraction); 44 | } 45 | 46 | // STEP 1: Find GCD 47 | // e.g. 15/6 -> (5*3)/(2*3) 48 | let status = findGCD(newNode, gcd, numeratorValue, denominatorValue); 49 | substeps.push(status); 50 | newNode = Node.Status.resetChangeGroups(status.newNode); 51 | 52 | // STEP 2: Cancel GCD 53 | // (5*3)/(2*3) -> 5/2 54 | status = cancelGCD(newNode, gcd, numeratorValue, denominatorValue); 55 | substeps.push(status); 56 | newNode = Node.Status.resetChangeGroups(status.newNode); 57 | 58 | return Node.Status.nodeChanged( 59 | ChangeTypes.SIMPLIFY_FRACTION, fraction, newNode, true, substeps); 60 | } 61 | 62 | // Returns a substep where the GCD is factored out of numerator and denominator 63 | // e.g. 15/6 -> (5*3)/(2*3) 64 | function findGCD(node, gcd, numeratorValue, denominatorValue) { 65 | let newNode = node.cloneDeep(); 66 | 67 | // manually set change group of the GCD nodes to be the same 68 | const gcdNode = Node.Creator.constant(gcd); 69 | gcdNode.changeGroup = 1; 70 | 71 | const intermediateNumerator = Node.Creator.parenthesis(Node.Creator.operator( 72 | '*', [Node.Creator.constant(numeratorValue/gcd), gcdNode])); 73 | const intermediateDenominator = Node.Creator.parenthesis(Node.Creator.operator( 74 | '*', [Node.Creator.constant(denominatorValue/gcd), gcdNode])); 75 | newNode = Node.Creator.operator( 76 | '/', [intermediateNumerator, intermediateDenominator]); 77 | 78 | return Node.Status.nodeChanged( 79 | ChangeTypes.FIND_GCD, node, newNode, false); 80 | } 81 | 82 | // Returns a substep where the GCD is cancelled out of numerator and denominator 83 | // e.g. (5*3)/(2*3) -> 5/2 84 | function cancelGCD(node, gcd, numeratorValue, denominatorValue) { 85 | let newNode; 86 | const newNumeratorNode = Node.Creator.constant(numeratorValue/gcd); 87 | const newDenominatorNode = Node.Creator.constant(denominatorValue/gcd); 88 | 89 | if (parseFloat(newDenominatorNode.value) === 1) { 90 | newNode = newNumeratorNode; 91 | } 92 | else { 93 | newNode = Node.Creator.operator( 94 | '/', [newNumeratorNode, newDenominatorNode]); 95 | } 96 | 97 | return Node.Status.nodeChanged( 98 | ChangeTypes.CANCEL_GCD, node, newNode, false); 99 | } 100 | 101 | module.exports = divideByGCD; 102 | -------------------------------------------------------------------------------- /lib/simplifyExpression/fractionsSearch/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Performs simpifications on fractions: adding and cancelling out. 3 | * 4 | * Note: division is represented in mathjs as an operator node with op '/' 5 | * and two args, where arg[0] is the numerator and arg[1] is the denominator 6 | 7 | // This module manipulates fractions with constants in the numerator and 8 | // denominator. For more complex/general fractions, see Fraction.js 9 | 10 | */ 11 | 12 | const addConstantAndFraction = require('./addConstantAndFraction'); 13 | const addConstantFractions = require('./addConstantFractions'); 14 | const cancelLikeTerms = require('./cancelLikeTerms'); 15 | const divideByGCD = require('./divideByGCD'); 16 | const simplifyFractionSigns = require('./simplifyFractionSigns'); 17 | const simplifyPolynomialFraction = require('./simplifyPolynomialFraction'); 18 | 19 | const Node = require('../../node'); 20 | const TreeSearch = require('../../TreeSearch'); 21 | 22 | const SIMPLIFICATION_FUNCTIONS = [ 23 | // e.g. 2/3 + 5/6 24 | addConstantFractions, 25 | // e.g. 4 + 5/6 or 4.5 + 6/8 26 | addConstantAndFraction, 27 | // e.g. 2/-9 -> -2/9 e.g. -2/-9 -> 2/9 28 | simplifyFractionSigns, 29 | // e.g. 8/12 -> 2/3 (divide by GCD 4) 30 | divideByGCD, 31 | // e.g. 2x/4 -> x/2 (divideByGCD but for coefficients of polynomial terms) 32 | simplifyPolynomialFraction, 33 | // e.g. (2x * 5) / 2x -> 5 34 | cancelLikeTerms, 35 | ]; 36 | 37 | const search = TreeSearch.preOrder(simplifyFractions); 38 | 39 | // Look for step(s) to perform on a node. Returns a Node.Status object. 40 | function simplifyFractions(node) { 41 | for (let i = 0; i < SIMPLIFICATION_FUNCTIONS.length; i++) { 42 | const nodeStatus = SIMPLIFICATION_FUNCTIONS[i](node); 43 | if (nodeStatus.hasChanged()) { 44 | return nodeStatus; 45 | } 46 | else { 47 | node = nodeStatus.newNode; 48 | } 49 | } 50 | return Node.Status.noChange(node); 51 | } 52 | 53 | 54 | module.exports = search; 55 | -------------------------------------------------------------------------------- /lib/simplifyExpression/fractionsSearch/simplifyFractionSigns.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Negative = require('../../Negative'); 3 | const Node = require('../../node'); 4 | 5 | // Simplifies negative signs if possible 6 | // e.g. -1/-3 --> 1/3 4/-5 --> -4/5 7 | // Note that -4/5 doesn't need to be simplified. 8 | // Note that our goal is for the denominator to always be positive. If it 9 | // isn't, we can simplify signs. 10 | // Returns a Node.Status object 11 | function simplifySigns(fraction) { 12 | if (!Node.Type.isOperator(fraction) || fraction.op !== '/') { 13 | return Node.Status.noChange(fraction); 14 | } 15 | const oldFraction = fraction.cloneDeep(); 16 | let numerator = fraction.args[0]; 17 | let denominator = fraction.args[1]; 18 | // The denominator should never be negative. 19 | if (Negative.isNegative(denominator)) { 20 | denominator = Negative.negate(denominator); 21 | const changeType = Negative.isNegative(numerator) ? 22 | ChangeTypes.CANCEL_MINUSES : 23 | ChangeTypes.SIMPLIFY_SIGNS; 24 | numerator = Negative.negate(numerator); 25 | const newFraction = Node.Creator.operator('/', [numerator, denominator]); 26 | return Node.Status.nodeChanged(changeType, oldFraction, newFraction); 27 | } 28 | else { 29 | return Node.Status.noChange(fraction); 30 | } 31 | } 32 | 33 | module.exports = simplifySigns; 34 | -------------------------------------------------------------------------------- /lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.js: -------------------------------------------------------------------------------- 1 | const arithmeticSearch = require('../arithmeticSearch'); 2 | const divideByGCD = require('./divideByGCD'); 3 | const Node = require('../../node'); 4 | 5 | // Simplifies a polynomial term with a fraction as its coefficients. 6 | // e.g. 2x/4 --> x/2 10x/5 --> 2x 7 | // Also simplified negative signs 8 | // e.g. -y/-3 --> y/3 4x/-5 --> -4x/5 9 | // returns the new simplified node in a Node.Status object 10 | function simplifyPolynomialFraction(node) { 11 | if (!Node.PolynomialTerm.isPolynomialTerm(node)) { 12 | return Node.Status.noChange(node); 13 | } 14 | 15 | const polyNode = new Node.PolynomialTerm(node.cloneDeep()); 16 | if (!polyNode.hasFractionCoeff()) { 17 | return Node.Status.noChange(node); 18 | } 19 | 20 | const coefficientSimplifications = [ 21 | divideByGCD, // for integer fractions 22 | arithmeticSearch, // for decimal fractions 23 | ]; 24 | 25 | for (let i = 0; i < coefficientSimplifications.length; i++) { 26 | const coefficientFraction = polyNode.getCoeffNode(); // a division node 27 | const newCoeffStatus = coefficientSimplifications[i](coefficientFraction); 28 | if (newCoeffStatus.hasChanged()) { 29 | // we need to reset change groups because we're creating a new node 30 | let newCoeff = Node.Status.resetChangeGroups(newCoeffStatus.newNode); 31 | if (newCoeff.value === '1') { 32 | newCoeff = null; 33 | } 34 | const exponentNode = polyNode.getExponentNode(); 35 | const newNode = Node.Creator.polynomialTerm( 36 | polyNode.getSymbolNode(), exponentNode, newCoeff); 37 | return Node.Status.nodeChanged(newCoeffStatus.changeType, node, newNode); 38 | } 39 | } 40 | 41 | return Node.Status.noChange(node); 42 | } 43 | 44 | module.exports = simplifyPolynomialFraction; 45 | -------------------------------------------------------------------------------- /lib/simplifyExpression/functionsSearch/absoluteValue.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const ChangeTypes = require('../../ChangeTypes'); 4 | const evaluate = require('../../util/evaluate'); 5 | const Node = require('../../node'); 6 | 7 | // Evaluates abs() function if it's on a single constant value. 8 | // Returns a Node.Status object. 9 | function absoluteValue(node) { 10 | if (!Node.Type.isFunction(node, 'abs')) { 11 | return Node.Status.noChange(node); 12 | } 13 | if (node.args.length > 1) { 14 | return Node.Status.noChange(node); 15 | } 16 | let newNode = node.cloneDeep(); 17 | const argument = newNode.args[0]; 18 | if (Node.Type.isConstant(argument, true)) { 19 | newNode = Node.Creator.constant(math.abs(evaluate(argument))); 20 | return Node.Status.nodeChanged( 21 | ChangeTypes.ABSOLUTE_VALUE, node, newNode); 22 | } 23 | else if (Node.Type.isConstantFraction(argument, true)) { 24 | const newNumerator = Node.Creator.constant( 25 | math.abs(evaluate(argument.args[0]))); 26 | const newDenominator = Node.Creator.constant( 27 | math.abs(evaluate(argument.args[1]))); 28 | newNode = Node.Creator.operator('/', [newNumerator, newDenominator]); 29 | return Node.Status.nodeChanged( 30 | ChangeTypes.ABSOLUTE_VALUE, node, newNode); 31 | } 32 | else { 33 | return Node.Status.noChange(node); 34 | } 35 | } 36 | 37 | module.exports = absoluteValue; 38 | -------------------------------------------------------------------------------- /lib/simplifyExpression/functionsSearch/index.js: -------------------------------------------------------------------------------- 1 | const absoluteValue = require('./absoluteValue'); 2 | 3 | const Node = require('../../node'); 4 | const NthRoot = require('./nthRoot'); 5 | const TreeSearch = require('../../TreeSearch'); 6 | 7 | const FUNCTIONS = [ 8 | NthRoot.nthRoot, 9 | absoluteValue 10 | ]; 11 | 12 | // Searches through the tree, prioritizing deeper nodes, and evaluates 13 | // functions (e.g. abs(-4)) if possible. 14 | // Returns a Node.Status object. 15 | const search = TreeSearch.postOrder(functions); 16 | 17 | // Evaluates a function call if possible. Returns a Node.Status object. 18 | function functions(node) { 19 | if (!Node.Type.isFunction(node)) { 20 | return Node.Status.noChange(node); 21 | } 22 | 23 | for (let i = 0; i < FUNCTIONS.length; i++) { 24 | const nodeStatus = FUNCTIONS[i](node); 25 | if (nodeStatus.hasChanged()) { 26 | return nodeStatus; 27 | } 28 | } 29 | return Node.Status.noChange(node); 30 | } 31 | 32 | module.exports = search; 33 | -------------------------------------------------------------------------------- /lib/simplifyExpression/index.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | const stepThrough = require('./stepThrough'); 3 | 4 | function simplifyExpressionString(expressionString, debug=false) { 5 | let exprNode; 6 | try { 7 | exprNode = math.parse(expressionString); 8 | } 9 | catch (err) { 10 | return []; 11 | } 12 | if (exprNode) { 13 | return stepThrough(exprNode, debug); 14 | } 15 | return []; 16 | } 17 | 18 | module.exports = simplifyExpressionString; 19 | -------------------------------------------------------------------------------- /lib/simplifyExpression/multiplyFractionsSearch/index.js: -------------------------------------------------------------------------------- 1 | const ChangeTypes = require('../../ChangeTypes'); 2 | const Node = require('../../node'); 3 | const TreeSearch = require('../../TreeSearch'); 4 | 5 | // If `node` is a product of terms where: 6 | // 1) at least one is a fraction 7 | // 2) either none are polynomial terms, OR 8 | // at least one has a symbol in the denominator 9 | // then multiply them together. 10 | // e.g. 2 * 5/x -> (2*5)/x 11 | // e.g. 3 * 1/5 * 5/9 = (3*1*5)/(5*9) 12 | // e.g. 2x * 1/x -> (2x*1) / x 13 | // NOTE: The reason we exclude the case of polynomial terms is because 14 | // we do not want to combine 9/2 * x -> 9x / 2 (which is less readable). 15 | // Cases like 5/2 * x * y/5 will be handled in collect and combine. 16 | // TODO: add a step somewhere to remove common terms in numerator and 17 | // denominator (so the 5s would cancel out on the next step after this) 18 | // This step must happen after things have been distributed, or else the answer 19 | // will be formatted badly, so it's a tree search of its own. 20 | // Returns a Node.Status object. 21 | const search = TreeSearch.postOrder(multiplyFractions); 22 | 23 | function multiplyFractions(node) { 24 | if (!Node.Type.isOperator(node) || node.op !== '*') { 25 | return Node.Status.noChange(node); 26 | } 27 | 28 | // we need to use the verbose syntax for `some` here because isFraction 29 | // can take more than one parameter 30 | const atLeastOneFraction = node.args.some( 31 | arg => Node.CustomType.isFraction(arg)); 32 | const hasPolynomialTerms = node.args.some(Node.PolynomialTerm.isPolynomialTerm); 33 | const hasPolynomialInDenominatorTerms = node.args.some(hasPolynomialInDenominator); 34 | 35 | if (!atLeastOneFraction || (hasPolynomialTerms && !hasPolynomialInDenominatorTerms)) { 36 | return Node.Status.noChange(node); 37 | } 38 | 39 | const numeratorArgs = []; 40 | const denominatorArgs = []; 41 | node.args.forEach(operand => { 42 | if (Node.CustomType.isFraction(operand)) { 43 | const fraction = Node.CustomType.getFraction(operand); 44 | numeratorArgs.push(fraction.args[0]); 45 | denominatorArgs.push(fraction.args[1]); 46 | } 47 | else { 48 | numeratorArgs.push(operand); 49 | } 50 | }); 51 | 52 | const newNumerator = Node.Creator.parenthesis( 53 | Node.Creator.operator('*', numeratorArgs)); 54 | const newDenominator = denominatorArgs.length === 1 55 | ? denominatorArgs[0] 56 | : Node.Creator.parenthesis(Node.Creator.operator('*', denominatorArgs)); 57 | 58 | const newNode = Node.Creator.operator('/', [newNumerator, newDenominator]); 59 | return Node.Status.nodeChanged( 60 | ChangeTypes.MULTIPLY_FRACTIONS, node, newNode); 61 | } 62 | 63 | // Returns true if `node` has a polynomial in the denominator, 64 | // e.g. 5/x or 1/2x^2 65 | function hasPolynomialInDenominator(node) { 66 | if (!(Node.CustomType.isFraction(node))) { 67 | return false; 68 | } 69 | 70 | const fraction = Node.CustomType.getFraction(node); 71 | const denominator = fraction.args[1]; 72 | return Node.PolynomialTerm.isPolynomialTerm(denominator); 73 | } 74 | 75 | module.exports = search; 76 | -------------------------------------------------------------------------------- /lib/simplifyExpression/simplify.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const checks = require('../checks'); 4 | const flattenOperands = require('../util/flattenOperands'); 5 | const print = require('../util/print'); 6 | const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); 7 | const stepThrough = require('./stepThrough'); 8 | 9 | 10 | // Given a mathjs expression node, steps through simplifying the expression. 11 | // Returns the simplified expression node. 12 | function simplify(node, debug=false) { 13 | if (checks.hasUnsupportedNodes(node)) { 14 | return node; 15 | } 16 | 17 | const steps = stepThrough(node, debug); 18 | let simplifiedNode; 19 | if (steps.length > 0) { 20 | simplifiedNode = steps.pop().newNode; 21 | } 22 | else { 23 | // removing parens isn't counted as a step, so try it here 24 | simplifiedNode = removeUnnecessaryParens(flattenOperands(node), true); 25 | } 26 | // unflatten the node. 27 | return unflatten(simplifiedNode); 28 | } 29 | 30 | // Unflattens a node so it is in the math.js style, by printing and parsing it 31 | // again 32 | function unflatten(node) { 33 | return math.parse(print.ascii(node)); 34 | } 35 | 36 | 37 | module.exports = simplify; 38 | -------------------------------------------------------------------------------- /lib/simplifyExpression/stepThrough.js: -------------------------------------------------------------------------------- 1 | const checks = require('../checks'); 2 | const Node = require('../node'); 3 | const Status = require('../node/Status'); 4 | 5 | const arithmeticSearch = require('./arithmeticSearch'); 6 | const basicsSearch = require('./basicsSearch'); 7 | const breakUpNumeratorSearch = require('./breakUpNumeratorSearch'); 8 | const collectAndCombineSearch = require('./collectAndCombineSearch'); 9 | const distributeSearch = require('./distributeSearch'); 10 | const divisionSearch = require('./divisionSearch'); 11 | const fractionsSearch = require('./fractionsSearch'); 12 | const functionsSearch = require('./functionsSearch'); 13 | const multiplyFractionsSearch = require('./multiplyFractionsSearch'); 14 | 15 | const flattenOperands = require('../util/flattenOperands'); 16 | const print = require('../util/print'); 17 | const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); 18 | 19 | // Given a mathjs expression node, steps through simplifying the expression. 20 | // Returns a list of details about each step. 21 | function stepThrough(node, debug=false) { 22 | if (debug) { 23 | // eslint-disable-next-line 24 | console.log('\n\nSimplifying: ' + print.ascii(node, false, true)); 25 | } 26 | 27 | if (checks.hasUnsupportedNodes(node)) { 28 | return []; 29 | } 30 | 31 | let nodeStatus; 32 | const steps = []; 33 | 34 | const originalExpressionStr = print.ascii(node); 35 | const MAX_STEP_COUNT = 20; 36 | let iters = 0; 37 | 38 | // Now, step through the math expression until nothing changes 39 | nodeStatus = step(node); 40 | while (nodeStatus.hasChanged()) { 41 | if (debug) { 42 | logSteps(nodeStatus); 43 | } 44 | steps.push(removeUnnecessaryParensInStep(nodeStatus)); 45 | 46 | node = Status.resetChangeGroups(nodeStatus.newNode); 47 | nodeStatus = step(node); 48 | 49 | if (iters++ === MAX_STEP_COUNT) { 50 | // eslint-disable-next-line 51 | console.error('Math error: Potential infinite loop for expression: ' + 52 | originalExpressionStr + ', returning no steps'); 53 | return []; 54 | } 55 | } 56 | 57 | return steps; 58 | } 59 | 60 | // Given a mathjs expression node, performs a single step to simplify the 61 | // expression. Returns a Node.Status object. 62 | function step(node) { 63 | let nodeStatus; 64 | 65 | node = flattenOperands(node); 66 | node = removeUnnecessaryParens(node, true); 67 | 68 | const simplificationTreeSearches = [ 69 | // Basic simplifications that we always try first e.g. (...)^0 => 1 70 | basicsSearch, 71 | // Simplify any division chains so there's at most one division operation. 72 | // e.g. 2/x/6 -> 2/(x*6) e.g. 2/(x/6) => 2 * 6/x 73 | divisionSearch, 74 | // Adding fractions, cancelling out things in fractions 75 | fractionsSearch, 76 | // e.g. addition of polynomial terms: 2x + 4x^2 + x => 4x^2 + 3x 77 | // e.g. multiplication of polynomial terms: 2x * x * x^2 => 2x^3 78 | // e.g. multiplication of constants: 10^3 * 10^2 => 10^5 79 | collectAndCombineSearch, 80 | // e.g. 2 + 2 => 4 81 | arithmeticSearch, 82 | // e.g. (2 + x) / 4 => 2/4 + x/4 83 | breakUpNumeratorSearch, 84 | // e.g. 3/x * 2x/5 => (3 * 2x) / (x * 5) 85 | multiplyFractionsSearch, 86 | // e.g. (2x + 3)(x + 4) => 2x^2 + 11x + 12 87 | distributeSearch, 88 | // e.g. abs(-4) => 4 89 | functionsSearch, 90 | ]; 91 | 92 | for (let i = 0; i < simplificationTreeSearches.length; i++) { 93 | nodeStatus = simplificationTreeSearches[i](node); 94 | // Always update node, since there might be changes that didn't count as 95 | // a step. Remove unnecessary parens, in case one a step results in more 96 | // parens than needed. 97 | node = removeUnnecessaryParens(nodeStatus.newNode, true); 98 | if (nodeStatus.hasChanged()) { 99 | node = flattenOperands(node); 100 | nodeStatus.newNode = node.cloneDeep(); 101 | return nodeStatus; 102 | } 103 | else { 104 | node = flattenOperands(node); 105 | } 106 | } 107 | return Node.Status.noChange(node); 108 | } 109 | 110 | // Removes unnecessary parens throughout the steps. 111 | // TODO: Ideally this would happen in NodeStatus instead. 112 | function removeUnnecessaryParensInStep(nodeStatus) { 113 | if (nodeStatus.substeps.length > 0) { 114 | nodeStatus.substeps.map(removeUnnecessaryParensInStep); 115 | } 116 | 117 | nodeStatus.oldNode = removeUnnecessaryParens(nodeStatus.oldNode, true); 118 | nodeStatus.newNode = removeUnnecessaryParens(nodeStatus.newNode, true); 119 | return nodeStatus; 120 | } 121 | 122 | function logSteps(nodeStatus) { 123 | // eslint-disable-next-line 124 | console.log(nodeStatus.changeType); 125 | // eslint-disable-next-line 126 | console.log(print.ascii(nodeStatus.newNode) + '\n'); 127 | 128 | if (nodeStatus.substeps.length > 0) { 129 | // eslint-disable-next-line 130 | console.log('\nsubsteps: '); 131 | nodeStatus.substeps.forEach(substep => substep); 132 | } 133 | } 134 | 135 | module.exports = stepThrough; 136 | -------------------------------------------------------------------------------- /lib/solveEquation/index.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const stepThrough = require('./stepThrough'); 4 | 5 | function solveEquationString(equationString, debug=false) { 6 | const comparators = ['<=', '>=', '=', '<', '>']; 7 | 8 | for (let i = 0; i < comparators.length; i++) { 9 | const comparator = comparators[i]; 10 | const sides = equationString.split(comparator); 11 | if (sides.length !== 2) { 12 | continue; 13 | } 14 | let leftNode, rightNode; 15 | const leftSide = sides[0].trim(); 16 | const rightSide = sides[1].trim(); 17 | 18 | if (!leftSide || !rightSide) { 19 | return []; 20 | } 21 | 22 | try { 23 | leftNode = math.parse(leftSide); 24 | rightNode = math.parse(rightSide); 25 | } 26 | catch (err) { 27 | return []; 28 | } 29 | if (leftNode && rightNode) { 30 | return stepThrough(leftNode, rightNode, comparator, debug); 31 | } 32 | } 33 | 34 | return []; 35 | } 36 | 37 | module.exports = solveEquationString; 38 | -------------------------------------------------------------------------------- /lib/util/Util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Various utility functions used in the math stepper 3 | */ 4 | const Util = {}; 5 | 6 | // Adds `value` to a list in `dict`, creating a new list if the key isn't in 7 | // the dictionary yet. Returns the updated dictionary. 8 | Util.appendToArrayInObject = function(dict, key, value) { 9 | if (dict[key]) { 10 | dict[key].push(value); 11 | } 12 | else { 13 | dict[key] = [value]; 14 | } 15 | return dict; 16 | }; 17 | 18 | module.exports = Util; 19 | -------------------------------------------------------------------------------- /lib/util/evaluate.js: -------------------------------------------------------------------------------- 1 | // Evaluates a node to a numerical value 2 | // e.g. the tree representing (2 + 2) * 5 would be evaluated to the number 20 3 | // it's important that `node` does not contain any symbol nodes 4 | 5 | function evaluate(node) { 6 | // TODO: once we swap in math-parser, call its evaluate function instead 7 | return node.eval(); 8 | } 9 | 10 | module.exports = evaluate; 11 | -------------------------------------------------------------------------------- /lib/util/print.js: -------------------------------------------------------------------------------- 1 | const flatten = require('./flattenOperands'); 2 | const Node = require('../node'); 3 | 4 | // Prints an expression node in asciimath 5 | // If showPlusMinus is true, print + - (e.g. 2 + -3) 6 | // If it's false (the default) 2 + -3 would print as 2 - 3 7 | // (The + - is needed to support the conversion of subtraction to addition of 8 | // negative terms. See flattenOperands for more details if you're curious.) 9 | function printAscii(node, showPlusMinus=false) { 10 | node = flatten(node.cloneDeep()); 11 | 12 | let string = printTreeTraversal(node); 13 | if (!showPlusMinus) { 14 | string = string.replace(/\s*?\+\s*?\-\s*?/g, ' - '); 15 | } 16 | return string; 17 | } 18 | 19 | function printTreeTraversal(node, parentNode) { 20 | if (Node.PolynomialTerm.isPolynomialTerm(node)) { 21 | const polyTerm = new Node.PolynomialTerm(node); 22 | // This is so we don't print 2/3 x^2 as 2 / 3x^2 23 | // Still print x/2 as x/2 and not 1/2 x though 24 | if (polyTerm.hasFractionCoeff() && node.op !== '/') { 25 | const coeffTerm = polyTerm.getCoeffNode(); 26 | const coeffStr = printTreeTraversal(coeffTerm); 27 | 28 | const nonCoeffTerm = Node.Creator.polynomialTerm( 29 | polyTerm.getSymbolNode(), polyTerm.exponent, null); 30 | const nonCoeffStr = printTreeTraversal(nonCoeffTerm); 31 | 32 | return `${coeffStr} ${nonCoeffStr}`; 33 | } 34 | } 35 | 36 | if (Node.Type.isIntegerFraction(node)) { 37 | return `${node.args[0]}/${node.args[1]}`; 38 | } 39 | 40 | if (Node.Type.isOperator(node)) { 41 | if (node.op === '/' && Node.Type.isOperator(node.args[1])) { 42 | return `${printTreeTraversal(node.args[0])} / (${printTreeTraversal(node.args[1])})`; 43 | } 44 | 45 | let opString = ''; 46 | 47 | switch (node.op) { 48 | case '+': 49 | case '-': 50 | // add space between operator and operands 51 | opString = ` ${node.op} `; 52 | break; 53 | case '*': 54 | if (node.implicit) { 55 | break; 56 | } 57 | opString = ` ${node.op} `; 58 | break; 59 | case '/': 60 | // no space for constant fraction divisions (slightly easier to read) 61 | if (Node.Type.isConstantFraction(node, true)) { 62 | opString = `${node.op}`; 63 | } 64 | else { 65 | opString = ` ${node.op} `; 66 | } 67 | break; 68 | case '^': 69 | // no space for exponents 70 | opString = `${node.op}`; 71 | break; 72 | } 73 | 74 | let str = node.args.map(arg => printTreeTraversal(arg, node)).join(opString); 75 | 76 | // Need to add parens around any [+, -] operation 77 | // nested in [/, *, ^] operation 78 | // Check #120, #126 issues for more details. 79 | // { "/" [{ "+" ["x", "2"] }, "2"] } -> (x + 2) / 2. 80 | if (parentNode && 81 | Node.Type.isOperator(parentNode) && 82 | node.op && parentNode.op && 83 | '*/^'.indexOf(parentNode.op) >= 0 && 84 | '+-'.indexOf(node.op) >= 0) { 85 | str = `(${str})`; 86 | } 87 | 88 | return str; 89 | } 90 | else if (Node.Type.isParenthesis(node)) { 91 | return `(${printTreeTraversal(node.content)})`; 92 | } 93 | else if (Node.Type.isUnaryMinus(node)) { 94 | if (Node.Type.isOperator(node.args[0]) && 95 | '*/^'.indexOf(node.args[0].op) === -1 && 96 | !Node.PolynomialTerm.isPolynomialTerm(node)) { 97 | return `-(${printTreeTraversal(node.args[0])})`; 98 | } 99 | else { 100 | return `-${printTreeTraversal(node.args[0])}`; 101 | } 102 | } 103 | else { 104 | return node.toString(); 105 | } 106 | } 107 | 108 | // Prints an expression node in LaTeX 109 | // (The + - is needed to support the conversion of subtraction to addition of 110 | // negative terms. See flattenOperands for more details if you're curious.) 111 | function printLatex(node, showPlusMinus=false) { 112 | let nodeTex = node.toTex({implicit: 'hide'}); 113 | 114 | if (!showPlusMinus) { 115 | // Replaces '+ -' with '-' 116 | nodeTex = nodeTex.replace(/\s*?\+\s*?\-\s*?/g, ' - '); 117 | } 118 | 119 | return nodeTex; 120 | } 121 | 122 | module.exports = { 123 | ascii: printAscii, 124 | latex: printLatex, 125 | }; 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mathsteps", 3 | "version": "0.2.0", 4 | "description": "Step by step math solutions", 5 | "main": "index.js", 6 | "dependencies": { 7 | "mathjs": "3.11.2" 8 | }, 9 | "engines": { 10 | "node": ">=6.0.0" 11 | }, 12 | "devDependencies": { 13 | "eslint": "^3.10.2", 14 | "eslint-config-google": "^0.7.0", 15 | "eslint-plugin-sort-requires": "^2.1.0", 16 | "mocha": "2.4.5" 17 | }, 18 | "scripts": { 19 | "lint": "node_modules/.bin/eslint .", 20 | "test": "node_modules/.bin/mocha --recursive", 21 | "setup-hooks": "ln -s ../../scripts/git-hooks/pre-commit.sh .git/hooks/pre-commit" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/socraticorg/mathsteps.git" 26 | }, 27 | "keywords": [ 28 | "math", 29 | "steps", 30 | "algebra", 31 | "cas", 32 | "computer", 33 | "algebra", 34 | "system" 35 | ], 36 | "author": "Evy Kassirer", 37 | "license": "Apache-2.0", 38 | "bugs": { 39 | "url": "https://github.com/socraticorg/mathsteps/issues" 40 | }, 41 | "homepage": "https://github.com/socraticorg/mathsteps#readme" 42 | } 43 | -------------------------------------------------------------------------------- /scripts/git-hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm test && 4 | npm run lint 5 | -------------------------------------------------------------------------------- /test/Negative.test.js: -------------------------------------------------------------------------------- 1 | const print = require('../lib/util/print'); 2 | 3 | const Negative = require('../lib/Negative'); 4 | 5 | const TestUtil = require('./TestUtil'); 6 | 7 | function testNegate(exprString, outputStr) { 8 | const inputStr = Negative.negate(TestUtil.parseAndFlatten(exprString)); 9 | TestUtil.testFunctionOutput(print.ascii, inputStr, outputStr); 10 | } 11 | 12 | describe('negate', function() { 13 | const tests = [ 14 | ['1', '-1'], 15 | ['-1', '1'], 16 | ['1/2', '-1/2'], 17 | ['(x+2)', '-(x + 2)'], 18 | ['x', '-x'], 19 | ['x^2', '-x^2'], 20 | ['-y^3', 'y^3'], 21 | ['2/3 x', '-2/3 x'], 22 | ['-5/6 z', '5/6 z'], 23 | ]; 24 | tests.forEach(t => testNegate(t[0], t[1])); 25 | }); 26 | -------------------------------------------------------------------------------- /test/Node/MixedNumber.test.js: -------------------------------------------------------------------------------- 1 | const MixedNumber = require('../../lib/node/MixedNumber'); 2 | const TestUtil = require('../TestUtil'); 3 | 4 | function testIsMixedNumber(input, output) { 5 | TestUtil.testBooleanFunction(MixedNumber.isMixedNumber, input, output); 6 | } 7 | 8 | function testIsNegativeMixedNumber(input, output) { 9 | TestUtil.testBooleanFunction(MixedNumber.isNegativeMixedNumber, input, output); 10 | } 11 | 12 | function testGetWholeNumberValue(input, output) { 13 | input = TestUtil.parseAndFlatten(input); 14 | TestUtil.testFunctionOutput(MixedNumber.getWholeNumberValue, input, output); 15 | } 16 | 17 | function testGetNumeratorValue(input, output) { 18 | input = TestUtil.parseAndFlatten(input); 19 | TestUtil.testFunctionOutput(MixedNumber.getNumeratorValue, input, output); 20 | } 21 | 22 | function testGetDenominatorValue(input, output) { 23 | input = TestUtil.parseAndFlatten(input); 24 | TestUtil.testFunctionOutput(MixedNumber.getDenominatorValue, input, output); 25 | } 26 | 27 | describe('isMixedNumber', function () { 28 | const tests = [ 29 | ['5(1)/(6)', true], 30 | ['19(2)/(3)', true], 31 | ['-1(7)/(8)', true], 32 | ['4*(1/2)', false], 33 | ['(1/2)3', false], 34 | ['3*10/15', false], 35 | ]; 36 | tests.forEach(t => testIsMixedNumber(t[0], t[1])); 37 | }); 38 | 39 | describe('isNegativeMixedNumber', function () { 40 | const tests = [ 41 | ['-1(7)/(8)', true], 42 | ['5(1)/(6)', false], 43 | ['19(2)/(3)', false], 44 | ]; 45 | tests.forEach(t => testIsNegativeMixedNumber(t[0], t[1])); 46 | }); 47 | 48 | describe('getWholeNumber', function () { 49 | const tests = [ 50 | ['5(1)/(6)', 5], 51 | ['19(2)/(3)', 19], 52 | ['-1(7)/(8)', 1], 53 | ]; 54 | tests.forEach(t => testGetWholeNumberValue(t[0], t[1])); 55 | }); 56 | 57 | describe('getNumerator', function () { 58 | const tests = [ 59 | ['5(1)/(6)', 1], 60 | ['19(2)/(3)', 2], 61 | ['-1(7)/(8)', 7], 62 | ]; 63 | tests.forEach(t => testGetNumeratorValue(t[0], t[1])); 64 | }); 65 | 66 | describe('getDenominator', function () { 67 | const tests = [ 68 | ['5(1)/(6)', 6], 69 | ['19(2)/(3)', 3], 70 | ['-1(7)/(8)', 8], 71 | ]; 72 | tests.forEach(t => testGetDenominatorValue(t[0], t[1])); 73 | }); 74 | -------------------------------------------------------------------------------- /test/Node/NthRootTerm.test.js: -------------------------------------------------------------------------------- 1 | const NthRootTerm = require('../../lib/node/NthRootTerm'); 2 | 3 | const TestUtil = require('../TestUtil'); 4 | 5 | function testIsNthRootTerm(exprStr, isTerm) { 6 | TestUtil.testBooleanFunction(NthRootTerm.isNthRootTerm, exprStr, isTerm); 7 | } 8 | 9 | describe('classifies nth root terms correctly', function() { 10 | const tests = [ 11 | ['nthRoot(3)', true], 12 | ['nthRoot(4, 3)', true], 13 | ['nthRoot(x)', true], 14 | ['nthRoot(x)^2', true], 15 | ['nthRoot(4*x^2, 2)', true], 16 | ['4nthRoot(x)', true], 17 | ['2*nthRoot(3,2)', true], 18 | ['-nthRoot(y^2)', true], 19 | ['nthRoot(x) * nthRoot(x)', false], 20 | ['nthRoot(2) + nthRoot(5)', false], 21 | ['3', false], 22 | ['x', false], 23 | ['y^5', false], 24 | ]; 25 | tests.forEach(t => testIsNthRootTerm(t[0], t[1])); 26 | }); 27 | -------------------------------------------------------------------------------- /test/Node/PolynomialTerm.test.js: -------------------------------------------------------------------------------- 1 | const PolynomialTerm = require('../../lib/node/PolynomialTerm'); 2 | 3 | const TestUtil = require('../TestUtil'); 4 | 5 | function testIsPolynomialTerm(exprStr, isTerm) { 6 | TestUtil.testBooleanFunction(PolynomialTerm.isPolynomialTerm, exprStr, isTerm); 7 | } 8 | 9 | describe('classifies symbol terms correctly', function() { 10 | const tests = [ 11 | ['x', true], 12 | ['x^2', true], 13 | ['y^55', true], 14 | ['y^4/4', true], 15 | ['5y/3', true], 16 | ['x^y', true], 17 | ['3', false], 18 | ['2^5', false], 19 | ['x*y^5', false], 20 | ['-12y^5/-3', true], 21 | ]; 22 | tests.forEach(t => testIsPolynomialTerm(t[0], t[1])); 23 | }); 24 | -------------------------------------------------------------------------------- /test/Node/Type.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const math = require('mathjs'); 3 | 4 | const Negative = require('../../lib/Negative'); 5 | const Node = require('../../lib/node'); 6 | const TestUtil = require('../TestUtil'); 7 | 8 | const constNode = Node.Creator.constant; 9 | 10 | describe('Node.Type works', function () { 11 | it('(2+2) parenthesis', function () { 12 | assert.deepEqual( 13 | Node.Type.isParenthesis(math.parse('(2+2)')), 14 | true); 15 | }); 16 | it('10 constant', function () { 17 | assert.deepEqual( 18 | Node.Type.isConstant(math.parse(10)), 19 | true); 20 | }); 21 | it('-2 constant', function () { 22 | assert.deepEqual( 23 | Node.Type.isConstant(constNode(-2)), 24 | true); 25 | }); 26 | it('2+2 operator without operator param', function () { 27 | assert.deepEqual( 28 | Node.Type.isOperator(math.parse('2+2')), 29 | true); 30 | }); 31 | it('2+2 operator with correct operator param', function () { 32 | assert.deepEqual( 33 | Node.Type.isOperator(math.parse('2+2'), '+'), 34 | true); 35 | }); 36 | it('2+2 operator with incorrect operator param', function () { 37 | assert.deepEqual( 38 | Node.Type.isOperator(math.parse('2+2'), '-'), 39 | false); 40 | }); 41 | it('-x not operator', function () { 42 | assert.deepEqual( 43 | Node.Type.isOperator(math.parse('-x')), 44 | false); 45 | }); 46 | it('-x not symbol', function () { 47 | assert.deepEqual( 48 | Node.Type.isSymbol(math.parse('-x')), 49 | false); 50 | }); 51 | it('y symbol', function () { 52 | assert.deepEqual( 53 | Node.Type.isSymbol(math.parse('y')), 54 | true); 55 | }); 56 | it('abs(5) is abs function', function () { 57 | assert.deepEqual( 58 | Node.Type.isFunction(math.parse('abs(5)'), 'abs'), 59 | true); 60 | }); 61 | it('sqrt(5) is not abs function', function () { 62 | assert.deepEqual( 63 | Node.Type.isFunction(math.parse('sqrt(5)'), 'abs'), 64 | false); 65 | }); 66 | // it('nthRoot(4) is an nthRoot function', function () { 67 | // assert.deepEqual( 68 | // Node.Type.isFunction(math.parse('nthRoot(5)'), 'nthRoot'), 69 | // true); 70 | // }); 71 | }); 72 | 73 | describe('isConstantOrConstantFraction', function () { 74 | it('2 true', function () { 75 | assert.deepEqual( 76 | Node.Type.isConstantOrConstantFraction(math.parse('2')), 77 | true); 78 | }); 79 | it('2/9 true', function () { 80 | assert.deepEqual( 81 | Node.Type.isConstantOrConstantFraction(math.parse('4/9')), 82 | true); 83 | }); 84 | it('x/2 false', function () { 85 | assert.deepEqual( 86 | Node.Type.isConstantOrConstantFraction(math.parse('x/2')), 87 | false); 88 | }); 89 | }); 90 | 91 | describe('isIntegerFraction', function () { 92 | it('4/5 true', function () { 93 | assert.deepEqual( 94 | Node.Type.isIntegerFraction(math.parse('4/5')), 95 | true); 96 | }); 97 | it('4.3/5 false', function () { 98 | assert.deepEqual( 99 | Node.Type.isIntegerFraction(math.parse('4.3/5')), 100 | false); 101 | }); 102 | it('4x/5 false', function () { 103 | assert.deepEqual( 104 | Node.Type.isIntegerFraction(math.parse('4x/5')), 105 | false); 106 | }); 107 | it('5 false', function () { 108 | assert.deepEqual( 109 | Node.Type.isIntegerFraction(math.parse('5')), 110 | false); 111 | }); 112 | }); 113 | 114 | describe('isFraction', function () { 115 | it('2/3 true', function () { 116 | assert.deepEqual( 117 | Node.CustomType.isFraction(math.parse('2/3')), 118 | true); 119 | }); 120 | it('-2/3 true', function () { 121 | assert.deepEqual( 122 | Node.CustomType.isFraction(math.parse('-2/3')), 123 | true); 124 | }); 125 | it('-(2/3) true', function () { 126 | assert.deepEqual( 127 | Node.CustomType.isFraction(math.parse('-(2/3)')), 128 | true); 129 | }); 130 | it('(2/3) true', function () { 131 | assert.deepEqual( 132 | Node.CustomType.isFraction(math.parse('(2/3)')), 133 | true); 134 | }); 135 | }); 136 | 137 | describe('getFraction', function () { 138 | it('2/3 2/3', function () { 139 | assert.deepEqual( 140 | Node.CustomType.getFraction(math.parse('2/3')), 141 | math.parse('2/3')); 142 | }); 143 | 144 | const expectedFraction = math.parse('2/3'); 145 | TestUtil.removeComments(expectedFraction); 146 | 147 | it('(2/3) 2/3', function () { 148 | assert.deepEqual( 149 | Node.CustomType.getFraction(math.parse('(2/3)')), 150 | expectedFraction); 151 | }); 152 | 153 | // we can't just parse -2/3 to get the expected fraction, 154 | // because that will put a unary minus on the 2, 155 | // instead of using a constant node of value -2 as our code does 156 | const negativeExpectedFraction = math.parse('2/3'); 157 | TestUtil.removeComments(negativeExpectedFraction); 158 | Negative.negate(negativeExpectedFraction); 159 | 160 | it('-(2/3) -2/3', function () { 161 | assert.deepEqual( 162 | Node.CustomType.getFraction(math.parse('-(2/3)')), 163 | negativeExpectedFraction); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/Symbols.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const math = require('mathjs'); 3 | 4 | const print = require('../lib/util/print'); 5 | const Symbols = require('../lib/Symbols'); 6 | 7 | function runTest(functionToTest, exprString, expectedOutput, symbolName) { 8 | it(exprString + ' -> ' + expectedOutput, function () { 9 | const expression = math.parse(exprString); 10 | const foundSymbol = functionToTest(expression, symbolName); 11 | assert.deepEqual( 12 | print.ascii(foundSymbol), 13 | expectedOutput 14 | ); 15 | }); 16 | } 17 | 18 | describe('getLastSymbolTerm', function() { 19 | const tests = [ 20 | ['1/x', '1 / x', 'x'], 21 | ['1/(3x)', '1 / (3x)', 'x'], 22 | ['3x', '3x', 'x'], 23 | ['x + 3x + 2', '3x', 'x'], 24 | ['x/(x+3)', 'x / (x + 3)', 'x'], 25 | ['x/(x+3) + y', 'x / (x + 3)', 'x'], 26 | ['x/(x+3) + y + 3x', 'y', 'y'], 27 | ['x/(x+3) + y + 3x + 1/2y', '1/2 y', 'y'], 28 | ]; 29 | 30 | tests.forEach(t => runTest(Symbols.getLastSymbolTerm, t[0], t[1], t[2])); 31 | }); 32 | 33 | describe('getLastNonSymbolTerm', function() { 34 | const tests = [ 35 | ['4x^2 + 2x + 2/4', '2/4', 'x'], 36 | ['4x^2 + 2/4 + x', '2/4', 'x'], 37 | ['4x^2 + 2x + y', 'y', 'x'], 38 | ['4x^2', '4', 'x'], 39 | ]; 40 | 41 | tests.forEach(t => runTest(Symbols.getLastNonSymbolTerm, t[0], t[1], t[2])); 42 | }); 43 | 44 | describe('getLastDenominatorWithSymbolTerm', function() { 45 | const tests = [ 46 | ['1/x', 'x', 'x'], 47 | ['1/(x+2)', '(x + 2)', 'x'], 48 | ['1/(x+2) + 3x', '(x + 2)', 'x'], 49 | ['1/(x+2) + 3x/(1+x)', '(1 + x)', 'x'], 50 | ['1/(x+2) + (x+1)/(2x+3)', '(2x + 3)', 'x'], 51 | ['1/x + x/5', 'x', 'x'], 52 | ['2 + 2/x + x/2', 'x', 'x'], 53 | ['2 + 2/y + x/2', 'y', 'y'], 54 | ['2y + 2/x + 3/(2y) + x/2', '(2y)', 'y'], 55 | ]; 56 | 57 | tests.forEach(t => runTest(Symbols.getLastDenominatorWithSymbolTerm, t[0], t[1], t[2])); 58 | }); 59 | -------------------------------------------------------------------------------- /test/TestUtil.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const math = require('mathjs'); 3 | 4 | const flatten = require('../lib/util/flattenOperands'); 5 | const print = require('../lib/util/print'); 6 | 7 | // TestUtil contains helper methods to share code across tests 8 | const TestUtil = {}; 9 | 10 | // Takes in an input string and returns a flattened and parsed node 11 | TestUtil.parseAndFlatten = function (exprString) { 12 | return flatten(math.parse(exprString)); 13 | }; 14 | 15 | // Tests a function that takes an input string and check its output 16 | TestUtil.testFunctionOutput = function (fn, input, output) { 17 | it(input + ' -> ' + output, () => { 18 | assert.deepEqual(fn(input),output); 19 | }); 20 | }; 21 | 22 | // tests a function that takes in a node and returns a boolean value 23 | TestUtil.testBooleanFunction = function (simplifier, exprString, expectedBooleanValue) { 24 | it(exprString + ' ' + expectedBooleanValue, () => { 25 | const inputNode = flatten(math.parse(exprString)); 26 | assert.equal(simplifier(inputNode),expectedBooleanValue); 27 | }); 28 | }; 29 | 30 | // Tests a simplification function 31 | TestUtil.testSimplification = function (simplifyingFunction, exprString, 32 | expectedOutputString) { 33 | it (exprString + ' -> ' + expectedOutputString, () => { 34 | assert.deepEqual( 35 | print.ascii(simplifyingFunction(flatten(math.parse(exprString))).newNode), 36 | expectedOutputString); 37 | }); 38 | }; 39 | 40 | // Test the substeps in the expression 41 | TestUtil.testSubsteps = function (fn, exprString, outputList, 42 | outputStr) { 43 | it(exprString + ' -> ' + outputStr, () => { 44 | const status = fn(flatten(math.parse(exprString))); 45 | const substeps = status.substeps; 46 | 47 | assert.deepEqual(substeps.length, outputList.length); 48 | substeps.forEach((step, i) => { 49 | assert.deepEqual( 50 | print.ascii(step.newNode), 51 | outputList[i]); 52 | }); 53 | if (outputStr) { 54 | assert.deepEqual( 55 | print.ascii(status.newNode), 56 | outputStr); 57 | } 58 | }); 59 | }; 60 | 61 | // Remove some property used in mathjs that we don't need and prevents node 62 | // equality checks from passing 63 | TestUtil.removeComments = function(node) { 64 | node.filter(node => node.comment !== undefined).forEach( 65 | node => delete node.comment); 66 | }; 67 | 68 | module.exports = TestUtil; 69 | -------------------------------------------------------------------------------- /test/canAddLikeTermPolynomialNodes.test.js: -------------------------------------------------------------------------------- 1 | const canAddLikeTerms = require('../lib/checks/canAddLikeTerms'); 2 | 3 | const TestUtil = require('./TestUtil'); 4 | 5 | function testCanBeAdded(expr, addable) { 6 | TestUtil.testBooleanFunction( 7 | canAddLikeTerms.canAddLikeTermPolynomialNodes, expr, addable); 8 | } 9 | 10 | describe('can add like term polynomials', () => { 11 | const tests = [ 12 | ['x^2 + x^2', true], 13 | ['x + x', true], 14 | ['x^3 + x', false], 15 | ]; 16 | tests.forEach(t => testCanBeAdded(t[0], t[1])); 17 | }); 18 | -------------------------------------------------------------------------------- /test/canMultiplyLikeTermConstantNodes.test.js: -------------------------------------------------------------------------------- 1 | const canMultiplyLikeTermConstantNodes = require('../lib/checks/canMultiplyLikeTermConstantNodes'); 2 | 3 | const TestUtil = require('./TestUtil'); 4 | 5 | function testCanBeMultipliedConstants(expr, multipliable) { 6 | TestUtil.testBooleanFunction(canMultiplyLikeTermConstantNodes, expr, multipliable); 7 | } 8 | 9 | describe('can multiply like term constants', () => { 10 | const tests = [ 11 | ['3^2 * 3^5', true], 12 | ['2^3 * 3^2', false], 13 | ['10^3 * 10^2', true], 14 | ['10^2 * 10 * 10^4', true] 15 | ]; 16 | tests.forEach(t => testCanBeMultipliedConstants(t[0], t[1])); 17 | }); 18 | -------------------------------------------------------------------------------- /test/canMultiplyLikeTermPolynomialNodes.test.js: -------------------------------------------------------------------------------- 1 | const canMultiplyLikeTermPolynomialNodes = require('../lib/checks/canMultiplyLikeTermPolynomialNodes'); 2 | 3 | const TestUtil = require('./TestUtil'); 4 | 5 | function testCanBeMultiplied(expr, multipliable) { 6 | TestUtil.testBooleanFunction(canMultiplyLikeTermPolynomialNodes, expr, multipliable); 7 | } 8 | 9 | describe('can multiply like term polynomials', () => { 10 | const tests = [ 11 | ['x^2 * x * x', true], 12 | ['x^2 * 3x * x', false], 13 | ['y * y^3', true], 14 | ['x^3 * x^2', true] 15 | ]; 16 | tests.forEach(t => testCanBeMultiplied(t[0], t[1])); 17 | }); 18 | -------------------------------------------------------------------------------- /test/canRearrangeCoefficient.test.js: -------------------------------------------------------------------------------- 1 | const canRearrangeCoefficient = require('../lib/checks/canRearrangeCoefficient'); 2 | 3 | const TestUtil = require('./TestUtil'); 4 | 5 | function testCanBeRearranged(expr, arrangeable) { 6 | TestUtil.testBooleanFunction(canRearrangeCoefficient, expr, arrangeable); 7 | } 8 | 9 | describe('can rearrange coefficient', () => { 10 | const tests = [ 11 | ['x*2', true], 12 | ['y^3 * 7', true] 13 | ]; 14 | tests.forEach(t => testCanBeRearranged(t[0], t[1])); 15 | }); 16 | -------------------------------------------------------------------------------- /test/checks/checks.test.js: -------------------------------------------------------------------------------- 1 | const checks = require('../../lib/checks'); 2 | 3 | const TestUtil = require('../TestUtil'); 4 | 5 | function testCanCombine(exprStr, canCombine) { 6 | TestUtil.testBooleanFunction(checks.canSimplifyPolynomialTerms, exprStr, canCombine); 7 | } 8 | 9 | describe('canSimplifyPolynomialTerms multiplication', function() { 10 | const tests = [ 11 | ['x^2 * x * x', true], 12 | // false b/c coefficient 13 | ['x^2 * 3x * x', false], 14 | ['y * y^3', true], 15 | ['5 * y^3', false], // just needs flattening 16 | ['5/7 * x', false], // just needs flattening 17 | ['5/7 * 9 * x', false], 18 | ]; 19 | tests.forEach(t => testCanCombine(t[0], t[1])); 20 | }); 21 | 22 | 23 | describe('canSimplifyPolynomialTerms addition', function() { 24 | const tests = [ 25 | ['x + x', true], 26 | ['4y^2 + 7y^2 + y^2', true], 27 | ['4y^2 + 7y^2 + y^2 + y', false], 28 | ['y', false], 29 | ]; 30 | tests.forEach(t => testCanCombine(t[0], t[1])); 31 | }); 32 | -------------------------------------------------------------------------------- /test/checks/hasUnsupportedNodes.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const math = require('mathjs'); 3 | 4 | const checks = require('../../lib/checks'); 5 | 6 | describe('arithmetic stepping', function () { 7 | it('4 + sqrt(16) no support for sqrt', function () { 8 | assert.deepEqual( 9 | checks.hasUnsupportedNodes(math.parse('4 + sqrt(4)')), 10 | true); 11 | }); 12 | 13 | it('x = 5 no support for assignment', function () { 14 | assert.deepEqual( 15 | checks.hasUnsupportedNodes(math.parse('x = 5')), 16 | true); 17 | }); 18 | 19 | it('x + (-5)^2 - 8*y/2 is fine', function () { 20 | assert.deepEqual( 21 | checks.hasUnsupportedNodes(math.parse('x + (-5)^2 - 8*y/2')), 22 | false); 23 | }); 24 | 25 | it('nthRoot() with no args has no support', function () { 26 | assert.deepEqual( 27 | checks.hasUnsupportedNodes(math.parse('nthRoot()')), 28 | true); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/checks/isQuadratic.test.js: -------------------------------------------------------------------------------- 1 | const checks = require('../../lib/checks'); 2 | const TestUtil = require('../TestUtil'); 3 | 4 | function testIsQuadratic(input, output) { 5 | TestUtil.testBooleanFunction(checks.isQuadratic, input, output); 6 | } 7 | 8 | describe('isQuadratic', function () { 9 | const tests = [ 10 | ['2 + 2', false], 11 | ['x', false], 12 | ['x^2 - 4', true], 13 | ['x^2 + 2x + 1', true], 14 | ['x^2 - 2x + 1', true], 15 | ['x^2 + 3x + 2', true], 16 | ['x^2 - 3x + 2', true], 17 | ['x^2 + x - 2', true], 18 | ['x^2 + x', true], 19 | ['x^2 + 4', true], 20 | ['x^2 + 4x + 1', true], 21 | ['x^2', false], 22 | ['x^3 + x^2 + x + 1', false], 23 | ['x^2 + 4 + 2^x', false], 24 | ['x^2 + 4 + 2y', false], 25 | ['y^2 + 4 + 2x', false], 26 | ]; 27 | tests.forEach(t => testIsQuadratic(t[0], t[1])); 28 | }); 29 | -------------------------------------------------------------------------------- /test/checks/resolvesToConstant.test.js: -------------------------------------------------------------------------------- 1 | const checks = require('../../lib/checks'); 2 | 3 | const TestUtil = require('../TestUtil'); 4 | 5 | function testResolvesToConstant(exprString, resolves) { 6 | TestUtil.testBooleanFunction(checks.resolvesToConstant, exprString, resolves); 7 | } 8 | 9 | describe('resolvesToConstant', function () { 10 | const tests = [ 11 | ['(2+2)', true], 12 | ['10', true], 13 | ['((2^2 + 4)) * 7 / 8', true], 14 | ['2 * 3^x', false], 15 | ['-(2) * -3', true], 16 | ]; 17 | tests.forEach(t => testResolvesToConstant(t[0], t[1])); 18 | }); 19 | -------------------------------------------------------------------------------- /test/equation.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const math = require('mathjs'); 3 | 4 | const TestUtil = require('./TestUtil'); 5 | 6 | const Equation = require('../lib/equation/Equation'); 7 | 8 | function constructAndPrintEquation(left, right, comp) { 9 | const leftNode = math.parse(left); 10 | const rightNode = math.parse(right); 11 | const equation = new Equation(leftNode, rightNode, comp); 12 | return equation.ascii(); 13 | } 14 | 15 | function constructAndPrintLatexEquation(left, right, comp) { 16 | const rightNode = TestUtil.parseAndFlatten(right); 17 | const leftNode = TestUtil.parseAndFlatten(left); 18 | const equation = new Equation(leftNode, rightNode, comp); 19 | return equation.latex(); 20 | } 21 | 22 | function testLatexprint(left, right, comp, output) { 23 | it (output, () => { 24 | assert.equal( 25 | constructAndPrintLatexEquation(left, right, comp), output 26 | ); 27 | }); 28 | } 29 | 30 | function testEquationConstructor(left, right, comp, output) { 31 | it (output, () => { 32 | assert.equal( 33 | constructAndPrintEquation(left, right, comp), output 34 | ); 35 | }); 36 | } 37 | 38 | describe('Equation constructor', () => { 39 | const tests = [ 40 | ['2*x^2 + x', '4', '=', '2x^2 + x = 4'], 41 | ['x^2 + 2*x + 2', '0', '>=', 'x^2 + 2x + 2 >= 0'], 42 | ['2*x - 1', '0', '<=', '2x - 1 <= 0'] 43 | ]; 44 | tests.forEach(t => testEquationConstructor(t[0], t[1], t[2], t[3])); 45 | }); 46 | 47 | describe('Latex printer', () => { 48 | const tests = [ 49 | ['2*x^2 + x', '4', '=', '2~{ x}^{2}+ x = 4'], 50 | ['x^2 + 2*y + 2', '0', '>=', '{ x}^{2}+2~ y+2 >= 0'], 51 | ['2x - 1', '0', '<=', '2~ x - 1 <= 0'] 52 | ]; 53 | tests.forEach(t => testLatexprint(t[0], t[1], t[2], t[3])); 54 | }); 55 | -------------------------------------------------------------------------------- /test/factor/ConstantFactors.test.js: -------------------------------------------------------------------------------- 1 | const ConstantFactors = require('../../lib/factor/ConstantFactors'); 2 | 3 | const TestUtil = require('../TestUtil'); 4 | 5 | function testPrimeFactors(input, output) { 6 | TestUtil.testFunctionOutput(ConstantFactors.getPrimeFactors, input, output); 7 | } 8 | 9 | describe('prime factors', function() { 10 | const tests = [ 11 | [1, [1]], 12 | [-1, [-1, 1]], 13 | [-2, [-1, 2]], 14 | [5, [5]], 15 | [12, [2, 2, 3]], 16 | [15, [3, 5]], 17 | [36, [2, 2, 3, 3]], 18 | [49, [7, 7]], 19 | [1260, [2, 2, 3, 3, 5, 7]], 20 | [13195, [5, 7, 13, 29]], 21 | [1234567891, [1234567891]] 22 | ]; 23 | tests.forEach(t => testPrimeFactors(t[0], t[1])); 24 | }); 25 | 26 | function testFactorPairs(input, output) { 27 | TestUtil.testFunctionOutput(ConstantFactors.getFactorPairs, input, output); 28 | } 29 | 30 | describe('factor pairs', function() { 31 | const tests = [ 32 | [1, [[-1, -1], [1, 1]]], 33 | [5, [[-1, -5], [1, 5]]], 34 | [12, [[-3, -4], [-2, -6], [-1, -12], [1, 12], [2, 6], [3, 4]]], 35 | [-12, [[-3, 4], [-2, 6], [-1, 12], [1, -12], [2, -6], [3, -4]]], 36 | [15, [[-3, -5], [-1, -15], [1, 15], [3, 5]]], 37 | [36, [[-6, -6], [-4, -9], [-3, -12], [-2, -18], [-1, -36], [1, 36], [2, 18], [3, 12], [4, 9], [6, 6,]]], 38 | [49, [[-7, -7], [-1, -49], [1, 49], [7, 7]]], 39 | [1260, [[-35, -36], [-30, -42], [-28, -45], [-21, -60], [-20, -63], [-18, -70], [-15, -84], [-14, -90], [-12, -105], [-10, -126], [-9, -140], [-7, -180], [-6, -210], [-5, -252], [-4, -315], [-3, -420], [-2, -630], [-1, -1260], [1, 1260], [2, 630], [3, 420], [4, 315], [5, 252], [6, 210], [7, 180], [9, 140], [10, 126], [12, 105], [14, 90], [15, 84], [18, 70], [20, 63], [21, 60], [28, 45], [30, 42], [35, 36]]], 40 | [13195, [[-91, -145], [-65, -203], [-35, -377], [-29, -455], [-13, -1015], [-7, -1885], [-5, -2639], [-1, -13195], [1, 13195], [5, 2639], [7, 1885], [13, 1015], [29, 455], [35, 377], [65, 203], [91, 145]]], 41 | [1234567891, [[-1, -1234567891], [1, 1234567891]]] 42 | ]; 43 | tests.forEach(t => testFactorPairs(t[0], t[1])); 44 | }); 45 | -------------------------------------------------------------------------------- /test/factor/factor.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const factor = require('../../lib/factor'); 3 | const print = require('../../lib/util/print'); 4 | 5 | const NO_STEPS = 'no-steps'; 6 | 7 | function testFactor(expressionString, outputStr, debug=false) { 8 | const steps = factor(expressionString, debug); 9 | let lastStep; 10 | if (steps.length === 0) { 11 | lastStep = NO_STEPS; 12 | } 13 | else { 14 | lastStep = print.ascii(steps[steps.length -1].newNode); 15 | } 16 | it(expressionString + ' -> ' + outputStr, (done) => { 17 | assert.equal(lastStep, outputStr); 18 | done(); 19 | }); 20 | } 21 | 22 | describe('factor expressions', function () { 23 | const tests = [ 24 | ['x^2', NO_STEPS], 25 | ['x^2 + 2x', 'x * (x + 2)'], 26 | ['x^2 - 4', '(x + 2) * (x - 2)'], 27 | ['x^2 + 4', NO_STEPS], 28 | ['x^2 + 2x + 1', '(x + 1)^2'], 29 | ['x^2 + 3x + 2', '(x + 1) * (x + 2)'], 30 | ['x^3 + x^2 + x + 1', NO_STEPS], 31 | ['1 + 2', NO_STEPS], 32 | ['x + 2', NO_STEPS], 33 | ]; 34 | tests.forEach(t => testFactor(t[0], t[1], t[2])); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /test/factor/factorQuadratic.test.js: -------------------------------------------------------------------------------- 1 | const factorQuadratic = require('../../lib/factor/factorQuadratic'); 2 | const TestUtil = require('../TestUtil'); 3 | 4 | function testFactorQuadratic(input, output) { 5 | TestUtil.testSimplification(factorQuadratic, input, output); 6 | } 7 | 8 | describe('factorQuadratic', function () { 9 | const tests = [ 10 | // no change 11 | ['x^2', 'x^2'], 12 | ['x^2 + x^2', 'x^2 + x^2'], 13 | ['x^2 + 2 - 3', 'x^2 + 2 - 3'], 14 | ['x^2 + 2y + 2x + 3', 'x^2 + 2y + 2x + 3'], 15 | ['x^2 + 4', 'x^2 + 4'], 16 | ['x^2 + 4 + 2^x', 'x^2 + 4 + 2^x'], 17 | ['-x^2 - 1', '-x^2 - 1'], 18 | // factor symbol 19 | ['x^2 + 2x', 'x * (x + 2)'], 20 | ['-x^2 - 2x', '-x * (x + 2)'], 21 | ['x^2 - 3x', 'x * (x - 3)'], 22 | ['2x^2 + 4x', '2x * (x + 2)'], 23 | // difference of squares 24 | ['x^2 - 4', '(x + 2) * (x - 2)'], 25 | ['-x^2 + 1', '-(x + 1) * (x - 1)'], 26 | ['4x^2 - 9', '(2x + 3) * (2x - 3)'], 27 | ['4x^2 - 16', '4 * (x + 2) * (x - 2)'], 28 | ['-4x^2 + 16', '-4 * (x + 2) * (x - 2)'], 29 | // perfect square 30 | ['x^2 + 2x + 1', '(x + 1)^2'], 31 | ['x^2 - 2x + 1', '(x - 1)^2'], 32 | ['-x^2 - 2x - 1', '-(x + 1)^2'], 33 | ['4x^2 + 4x + 1', '(2x + 1)^2'], 34 | ['12x^2 + 12x + 3', '3 * (2x + 1)^2'], 35 | // sum product rule 36 | ['x^2 + 3x + 2', '(x + 1) * (x + 2)'], 37 | ['x^2 - 3x + 2', '(x - 1) * (x - 2)'], 38 | ['x^2 + x - 2', '(x - 1) * (x + 2)'], 39 | ['-x^2 - 3x - 2', '-(x + 1) * (x + 2)'], 40 | ['2x^2 + 5x + 3','(x + 1) * (2x + 3)'], 41 | ['2x^2 - 5x - 3','(2x + 1) * (x - 3)'], 42 | ['2x^2 - 5x + 3','(x - 1) * (2x - 3)'], 43 | // TODO: quadratic equation 44 | ['x^2 + 4x + 1', 'x^2 + 4x + 1'], 45 | ['x^2 - 3x + 1', 'x^2 - 3x + 1'], 46 | ]; 47 | tests.forEach(t => testFactorQuadratic(t[0], t[1])); 48 | }); 49 | 50 | function testFactorSumProductRuleSubsteps(exprString, outputList) { 51 | const lastString = outputList[outputList.length - 1]; 52 | TestUtil.testSubsteps(factorQuadratic, exprString, outputList, lastString); 53 | } 54 | 55 | describe('factorSumProductRule', function() { 56 | const tests = [ 57 | // sum product rule 58 | ['x^2 + 3x + 2', 59 | ['x^2 + x + 2x + 2', 60 | '(x^2 + x) + (2x + 2)', 61 | 'x * (x + 1) + (2x + 2)', 62 | 'x * (x + 1) + 2 * (x + 1)', 63 | '(x + 1) * (x + 2)'] 64 | ], 65 | ['x^2 - 3x + 2', 66 | ['x^2 - x - 2x + 2', 67 | '(x^2 - x) + (-2x + 2)', 68 | 'x * (x - 1) + (-2x + 2)', 69 | 'x * (x - 1) - 2 * (x - 1)', 70 | '(x - 1) * (x - 2)'] 71 | ], 72 | ['x^2 + x - 2', 73 | ['x^2 - x + 2x - 2', 74 | '(x^2 - x) + (2x - 2)', 75 | 'x * (x - 1) + (2x - 2)', 76 | 'x * (x - 1) + 2 * (x - 1)', 77 | '(x - 1) * (x + 2)'] 78 | ], 79 | ['-x^2 - 3x - 2', 80 | ['-(x^2 + 3x + 2)', 81 | '-(x^2 + x + 2x + 2)', 82 | '-((x^2 + x) + (2x + 2))', 83 | '-(x * (x + 1) + (2x + 2))', 84 | '-(x * (x + 1) + 2 * (x + 1))', 85 | '-(x + 1) * (x + 2)'] 86 | ], 87 | ['2x^2 + 5x + 3', 88 | ['2x^2 + 2x + 3x + 3', 89 | '(2x^2 + 2x) + (3x + 3)', 90 | '2x * (x + 1) + (3x + 3)', 91 | '2x * (x + 1) + 3 * (x + 1)', 92 | '(x + 1) * (2x + 3)'] 93 | ], 94 | ['2x^2 - 5x - 3', 95 | ['2x^2 + x - 6x - 3', 96 | '(2x^2 + x) + (-6x - 3)', 97 | 'x * (2x + 1) + (-6x - 3)', 98 | 'x * (2x + 1) - 3 * (2x + 1)', 99 | '(2x + 1) * (x - 3)'] 100 | ], 101 | ['2x^2 - 5x + 3', 102 | ['2x^2 - 2x - 3x + 3', 103 | '(2x^2 - 2x) + (-3x + 3)', 104 | '2x * (x - 1) + (-3x + 3)', 105 | '2x * (x - 1) - 3 * (x - 1)', 106 | '(x - 1) * (2x - 3)'] 107 | ], 108 | ]; 109 | tests.forEach(t => testFactorSumProductRuleSubsteps(t[0], t[1])); 110 | }); 111 | -------------------------------------------------------------------------------- /test/simplifyExpression/arithmeticSearch/arithmeticSearch.test.js: -------------------------------------------------------------------------------- 1 | const arithmeticSearch = require('../../../lib/simplifyExpression/arithmeticSearch'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testArithmeticSearch(exprStr, outputStr) { 6 | TestUtil.testSimplification(arithmeticSearch, exprStr, outputStr); 7 | } 8 | 9 | describe('evaluate arithmeticSearch', function () { 10 | const tests = [ 11 | ['2+2', '4'], 12 | ['2*3*5', '30'], 13 | ['6*6', '36'], 14 | ['9/4', '9/4'], // does not divide 15 | ['16 - 1953125', '-1953109'], // verify large negative number round correctly 16 | ]; 17 | tests.forEach(t => testArithmeticSearch(t[0], t[1])); 18 | }); 19 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.test.js: -------------------------------------------------------------------------------- 1 | const convertMixedNumberToImproperFraction = require( 2 | '../../../lib/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction'); 3 | 4 | const TestUtil = require('../../TestUtil'); 5 | 6 | function testConvertMixedNumberToImproperFraction(exprString, outputList, outputStr) { 7 | TestUtil.testSubsteps(convertMixedNumberToImproperFraction, exprString, outputList, outputStr); 8 | } 9 | 10 | describe('convertMixedNumberToImproperFraction', function() { 11 | const tests = [ 12 | ['1(2)/(3)', 13 | ['((1 * 3) + 2) / 3', 14 | '(3 + 2) / 3', 15 | '5/3'], 16 | '5/3' 17 | ], 18 | ['19(4)/(8)', 19 | ['((19 * 8) + 4) / 8', 20 | '(152 + 4) / 8', 21 | '156/8'], 22 | '156/8' 23 | ], 24 | ['-5(10)/(11)', 25 | ['-((5 * 11) + 10) / 11', 26 | '-(55 + 10) / 11', 27 | '-65/11'], 28 | '-65/11' 29 | ], 30 | ]; 31 | tests.forEach(t => testConvertMixedNumberToImproperFraction(t[0], t[1], t[2])); 32 | }); 33 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/rearrangeCoefficient.test.js: -------------------------------------------------------------------------------- 1 | const rearrangeCoefficient = require('../../../lib/simplifyExpression/basicsSearch/rearrangeCoefficient'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('rearrangeCoefficient', function() { 6 | const tests = [ 7 | ['2 * x^2', '2x^2'], 8 | ['y^3 * 5', '5y^3'], 9 | ]; 10 | tests.forEach(t => testSimplify(t[0], t[1], rearrangeCoefficient)); 11 | }); 12 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/reduceExponentByZero.test.js: -------------------------------------------------------------------------------- 1 | const reduceExponentByZero = require('../../../lib/simplifyExpression/basicsSearch/reduceExponentByZero'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('reduceExponentByZero', function() { 6 | testSimplify('(x+3)^0', '1', reduceExponentByZero); 7 | }); 8 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/reduceMutliplicationByZero.test.js: -------------------------------------------------------------------------------- 1 | const reduceMultiplicationByZero = require('../../../lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('reduce multiplication by 0', function () { 6 | const tests = [ 7 | ['0x', '0'], 8 | ['2*0*z^2','0'], 9 | ]; 10 | tests.forEach(t => testSimplify(t[0], t[1], reduceMultiplicationByZero)); 11 | }); 12 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.test.js: -------------------------------------------------------------------------------- 1 | const reduceZeroDividedByAnything = require('../../../lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('simplify basics', function () { 6 | const tests = [ 7 | ['0/5', '0'], 8 | ['0/(x+6+7+x^2+2^y)', '0'], 9 | ]; 10 | tests.forEach(t => testSimplify(t[0], t[1], reduceZeroDividedByAnything)); 11 | }); 12 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/removeAdditionOfZero.test.js: -------------------------------------------------------------------------------- 1 | const removeAdditionOfZero = require('../../../lib/simplifyExpression/basicsSearch/removeAdditionOfZero'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('removeAdditionOfZero', function() { 6 | var tests = [ 7 | ['2+0+x', '2 + x'], 8 | ['2+x+0', '2 + x'], 9 | ['0+2+x', '2 + x'] 10 | ]; 11 | tests.forEach(t => testSimplify(t[0], t[1], removeAdditionOfZero)); 12 | }); 13 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/removeDivisionByOne.test.js: -------------------------------------------------------------------------------- 1 | const removeDivisionByOne = require('../../../lib/simplifyExpression/basicsSearch/removeDivisionByOne'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('removeDivisionByOne', function() { 6 | testSimplify('x/1', 'x', removeDivisionByOne); 7 | }); 8 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/removeExponentBaseOne.test.js: -------------------------------------------------------------------------------- 1 | const removeExponentBaseOne = require('../../../lib/simplifyExpression/basicsSearch/removeExponentBaseOne'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('removeExponentBaseOne', function() { 6 | const tests = [ 7 | ['1^3', '1'], 8 | ['1^x', '1^x'], 9 | ['1^(2 + 3 + 5/4 + 7 - 6/7)', '1'] 10 | ]; 11 | tests.forEach(t => testSimplify(t[0], t[1], removeExponentBaseOne)); 12 | }); 13 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/removeExponentByOne.test.js: -------------------------------------------------------------------------------- 1 | const removeExponentByOne = require('../../../lib/simplifyExpression/basicsSearch/removeExponentByOne'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('removeExponentByOne', function() { 6 | testSimplify('x^1', 'x', removeExponentByOne); 7 | }); 8 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.test.js: -------------------------------------------------------------------------------- 1 | const removeMultiplicationByNegativeOne = require('../../../lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('removeMultiplicationByNegativeOne', function() { 6 | const tests = [ 7 | ['-1*x', '-x'], 8 | ['x^2*-1', '-x^2'], 9 | ['2x*2*-1', '2x * 2 * -1'], // does not remove multiplication by -1 10 | ]; 11 | tests.forEach(t => testSimplify(t[0], t[1], removeMultiplicationByNegativeOne)); 12 | }); 13 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/removeMultiplicationByOne.test.js: -------------------------------------------------------------------------------- 1 | const removeMultiplicationByOne = require('../../../lib/simplifyExpression/basicsSearch/removeMultiplicationByOne'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | describe('removeMultiplicationByOne', function() { 6 | const tests = [ 7 | ['x*1', 'x'], 8 | ['1x', 'x'], 9 | ['1*z^2', 'z^2'], 10 | ['2*1*z^2', '2 * 1z^2'], 11 | ]; 12 | tests.forEach(t => testSimplify(t[0], t[1], removeMultiplicationByOne)); 13 | }); 14 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.test.js: -------------------------------------------------------------------------------- 1 | const simplifyDoubleUnaryMinus = require('../../../lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus'); 2 | 3 | const testSimplify = require('./testSimplify'); 4 | 5 | 6 | describe('simplifyDoubleUnaryMinus', function() { 7 | var tests = [ 8 | ['--5', '5'], 9 | ['--x', 'x'] 10 | ]; 11 | tests.forEach(t => testSimplify(t[0], t[1], simplifyDoubleUnaryMinus)); 12 | }); 13 | -------------------------------------------------------------------------------- /test/simplifyExpression/basicsSearch/testSimplify.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const print = require('../../../lib/util/print'); 4 | 5 | const TestUtil = require('../../TestUtil'); 6 | 7 | function testSimplify(exprStr, outputStr, simplifyOperation) { 8 | it(exprStr + ' -> ' + outputStr, function () { 9 | const inputNode = TestUtil.parseAndFlatten(exprStr); 10 | const newNode = simplifyOperation(inputNode).newNode; 11 | assert.equal( 12 | print.ascii(newNode), 13 | outputStr); 14 | }); 15 | } 16 | 17 | module.exports = testSimplify; 18 | -------------------------------------------------------------------------------- /test/simplifyExpression/breakUpNumeratorSearch/breakUpNumeratorSearch.test.js: -------------------------------------------------------------------------------- 1 | const breakUpNumeratorSearch = require('../../../lib/simplifyExpression/breakUpNumeratorSearch'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testBreakUpNumeratorSearch(exprStr, outputStr) { 6 | TestUtil.testSimplification(breakUpNumeratorSearch, exprStr, outputStr); 7 | } 8 | 9 | describe('breakUpNumerator', function() { 10 | const tests = [ 11 | ['(x+3+y)/3', '(x / 3 + 3/3 + y / 3)'], 12 | ['(2+x)/4', '(2/4 + x / 4)'], 13 | ['2(x+3)/3', '2 * (x / 3 + 3/3)'], 14 | ]; 15 | tests.forEach(t => testBreakUpNumeratorSearch(t[0], t[1])); 16 | }); 17 | -------------------------------------------------------------------------------- /test/simplifyExpression/collectAndCombineSearch/LikeTermCollector.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const print = require('../../../lib/util/print'); 4 | 5 | const LikeTermCollector = require('../../../lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector'); 6 | 7 | const TestUtil = require('../../TestUtil'); 8 | 9 | function testCollectLikeTerms(exprStr, outputStr, explanation='', debug=false) { 10 | let description = `${exprStr} -> ${outputStr}`; 11 | 12 | if (explanation) { 13 | description += ` (${explanation})`; 14 | } 15 | 16 | it(description, () => { 17 | const exprTree = TestUtil.parseAndFlatten(exprStr); 18 | const collected = print.ascii(LikeTermCollector.collectLikeTerms(exprTree).newNode); 19 | if (debug) { 20 | // eslint-disable-next-line 21 | console.log(collected); 22 | } 23 | assert.equal(collected, outputStr); 24 | }); 25 | } 26 | 27 | function testCanCollectLikeTerms(exprStr, canCollect, explanation) { 28 | let description = `${exprStr} -> ${canCollect}`; 29 | 30 | if (explanation) { 31 | description += ` (${explanation})`; 32 | } 33 | 34 | it(description , () => { 35 | const exprTree = TestUtil.parseAndFlatten(exprStr); 36 | assert.equal( 37 | LikeTermCollector.canCollectLikeTerms(exprTree), 38 | canCollect); 39 | }); 40 | } 41 | 42 | describe('can collect like terms for addition', function () { 43 | const tests = [ 44 | ['2+2', false, 'because only one type'], 45 | ['x^2+x^2', false, 'because only one type'], 46 | ['x+2', false, 'because all types have only one'], 47 | ['(x+2+x)', false, 'because in parenthesis, need to be collected first'], 48 | ['x+2+x', true], 49 | ['x^2 + 5 + x + x^2', true], 50 | ['nthRoot(2) + nthRoot(2)', false], 51 | ['nthRoot(x, 2) + nthRoot(x, 2) + 5', true], 52 | ['7x + nthRoot(3*x, 2) + nthRoot(3*x, 2)', true], 53 | ]; 54 | tests.forEach(t => testCanCollectLikeTerms(t[0], t[1], t[2])); 55 | }); 56 | 57 | describe('can collect like terms for multiplication', function () { 58 | const tests = [ 59 | ['2*2', false, 'because only one type'], 60 | ['x^2 * 2x^2', true], 61 | ['x * 2', false, 'because all types have only one'], 62 | ['((2x^2)) * y * x * y^3', true], 63 | ]; 64 | tests.forEach(t => testCanCollectLikeTerms(t[0], t[1], t[2])); 65 | }); 66 | 67 | describe('basic addition collect like terms, no exponents or coefficients', function() { 68 | const tests = [ 69 | ['2+x+7', 'x + (2 + 7)'], 70 | ['x + 4 + x + 5', '(x + x) + (4 + 5)'], 71 | ['x + 4 + y', 'x + 4 + y'], 72 | ['x + 4 + x + 4/9 + y + 5/7', '(x + x) + y + 4 + (4/9 + 5/7)'], 73 | ['x + 4 + x + 2^x + 5', '(x + x) + (4 + 5) + 2^x', 74 | 'because 2^x is an \'other\''], 75 | ['z + 2*(y + x) + 4 + z', '(z + z) + 4 + 2 * (y + x)', 76 | ' 2*(y + x) is an \'other\' cause it has parens'], 77 | ['nthRoot(2) + 100 + nthRoot(2)', '(nthRoot(2) + nthRoot(2)) + 100'], 78 | ['y + nthRoot(x, 2) + 4y + nthRoot(x, 2)', '(nthRoot(x, 2) + nthRoot(x, 2)) + (y + 4y)'], 79 | ['nthRoot(x, 2) + 2 + nthRoot(x, 2) + 5', '(nthRoot(x, 2) + nthRoot(x, 2)) + (2 + 5)'], 80 | ]; 81 | tests.forEach(t => testCollectLikeTerms(t[0], t[1], t[2])); 82 | }); 83 | 84 | describe('collect like terms with exponents and coefficients', function() { 85 | const tests = [ 86 | ['x^2 + x + x^2 + x', '(x^2 + x^2) + (x + x)'], 87 | ['y^2 + 5 + y^2 + 5', '(y^2 + y^2) + (5 + 5)'], 88 | ['y + 5 + z^2', 'y + 5 + z^2'], 89 | ['2x^2 + x + x^2 + 3x', '(2x^2 + x^2) + (x + 3x)'], 90 | ['nthRoot(2)^3 + nthRoot(2)^3 - 6x', '(nthRoot(2)^3 + nthRoot(2)^3) - 6x'], 91 | ['4x + 7 * nthRoot(11) - x - 2 * nthRoot(11)', '(7 * nthRoot(11) - 2 * nthRoot(11)) + (4x - x)'], 92 | ]; 93 | tests.forEach(t => testCollectLikeTerms(t[0], t[1], t[2])); 94 | }); 95 | 96 | describe('collect like terms for multiplication', function() { 97 | const tests = [ 98 | ['2x^2 * y * x * y^3', '2 * (x^2 * x) * (y * y^3)'], 99 | ['y^2 * 5 * y * 9', '(5 * 9) * (y^2 * y)'], 100 | ['5y^2 * -4y * 9', '(5 * -4 * 9) * (y^2 * y)'], 101 | ['5y^2 * -y * 9', '(5 * -1 * 9) * (y^2 * y)'], 102 | ['y * 5 * (2+x) * y^2 * 1/3', '(5 * 1/3) * (y * y^2) * (2 + x)'], 103 | ]; 104 | tests.forEach(t => testCollectLikeTerms(t[0], t[1], t[2])); 105 | }); 106 | 107 | describe('collect like terms for nthRoot multiplication', function() { 108 | const tests = [ 109 | ['nthRoot(x, 2) * nthRoot(x, 2)', 'nthRoot(x, 2) * nthRoot(x, 2)'], 110 | ['nthRoot(x, 2) * nthRoot(x, 2) * 3', '3 * (nthRoot(x, 2) * nthRoot(x, 2))'], 111 | ['nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3)', '(nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)'], 112 | ['nthRoot(2x, 2) * nthRoot(2x, 2) * nthRoot(y, 4) * nthRoot(y^3, 4)', '(nthRoot(2 x, 2) * nthRoot(2 x, 2)) * (nthRoot(y, 4) * nthRoot(y ^ 3, 4))'], 113 | ]; 114 | tests.forEach(t => testCollectLikeTerms(t[0], t[1])); 115 | }); 116 | -------------------------------------------------------------------------------- /test/simplifyExpression/collectAndCombineSearch/collectAndCombineSearch.test.js: -------------------------------------------------------------------------------- 1 | const collectAndCombineSearch = require('../../../lib/simplifyExpression/collectAndCombineSearch'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testCollectAndCombineSubsteps(exprString, outputList, outputStr) { 6 | TestUtil.testSubsteps(collectAndCombineSearch, exprString, outputList, outputStr); 7 | } 8 | 9 | function testSimpleCollectAndCombineSearch(exprString, outputStr) { 10 | TestUtil.testSimplification(collectAndCombineSearch, exprString, outputStr); 11 | } 12 | 13 | describe('combineNthRoots multiplication', function() { 14 | const tests = [ 15 | ['nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3)', 16 | ['(nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)', 17 | 'nthRoot(x * x, 2) * nthRoot(x, 3)'], 18 | ], 19 | ['nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3) * 3', 20 | ['3 * (nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)', 21 | '3 * nthRoot(x * x, 2) * nthRoot(x, 3)'], 22 | ], 23 | ['nthRoot(2x, 2) * nthRoot(2x, 2) * nthRoot(y, 4) * nthRoot(y^3, 4)', 24 | ['(nthRoot(2 x, 2) * nthRoot(2 x, 2)) * (nthRoot(y, 4) * nthRoot(y ^ 3, 4))', 25 | 'nthRoot(2 x * 2 x, 2) * (nthRoot(y, 4) * nthRoot(y ^ 3, 4))', 26 | 'nthRoot(2 x * 2 x, 2) * nthRoot(y * y ^ 3, 4)'], 27 | ], 28 | ['nthRoot(x) * nthRoot(x)', 29 | [], 30 | 'nthRoot(x * x, 2)' 31 | ], 32 | ['nthRoot(3) * nthRoot(3)', 33 | [], 34 | 'nthRoot(3 * 3, 2)' 35 | ], 36 | ['nthRoot(5) * nthRoot(9x, 2)', 37 | [], 38 | 'nthRoot(5 * 9 x, 2)' 39 | ] 40 | ]; 41 | tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1], t[2])); 42 | }); 43 | 44 | describe('combinePolynomialTerms multiplication', function() { 45 | const tests = [ 46 | ['x^2 * x * x', 47 | ['x^2 * x^1 * x^1', 48 | 'x^(2 + 1 + 1)', 49 | 'x^4'], 50 | ], 51 | ['y * y^3', 52 | ['y^1 * y^3', 53 | 'y^(1 + 3)', 54 | 'y^4'], 55 | ], 56 | ['2x * x^2 * 5x', 57 | ['(2 * 5) * (x * x^2 * x)', 58 | '10 * (x * x^2 * x)', 59 | '10x^4'], 60 | '10x^4' 61 | ], 62 | ]; 63 | tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1], t[2])); 64 | }); 65 | 66 | describe('combinePolynomialTerms addition', function() { 67 | const tests = [ 68 | ['x+x', 69 | ['1x + 1x', 70 | '(1 + 1) * x', 71 | '2x'] 72 | ], 73 | ['4y^2 + 7y^2 + y^2', 74 | ['4y^2 + 7y^2 + 1y^2', 75 | '(4 + 7 + 1) * y^2', 76 | '12y^2'] 77 | ], 78 | ['2x + 4x + y', 79 | ['(2x + 4x) + y', 80 | '6x + y'], 81 | '6x + y' 82 | ], 83 | ]; 84 | tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1])); 85 | }); 86 | 87 | describe('combineNthRootTerms addition', function() { 88 | const tests = [ 89 | ['nthRoot(x) + nthRoot(x)', 90 | ['1 * nthRoot(x) + 1 * nthRoot(x)', 91 | '(1 + 1) * nthRoot(x)', 92 | '2 * nthRoot(x)'] 93 | ], 94 | ['4nthRoot(2)^2 + 7nthRoot(2)^2 + nthRoot(2)^2', 95 | ['4 * nthRoot(2)^2 + 7 * nthRoot(2)^2 + 1 * nthRoot(2)^2', 96 | '(4 + 7 + 1) * nthRoot(2)^2', 97 | '12 * nthRoot(2)^2'] 98 | ], 99 | ['10nthRoot(5y) - 2nthRoot(5y)', 100 | ['(10 - 2) * nthRoot(5 y)', 101 | '8 * nthRoot(5 y)'], 102 | ], 103 | ]; 104 | tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1])); 105 | }); 106 | 107 | describe('combineConstantPowerTerms multiplication', function() { 108 | const tests = [ 109 | ['10^2 * 10', 110 | ['10^2 * 10^1', 111 | '10^(2 + 1)', 112 | '10^3'], 113 | ], 114 | ['2 * 2^3', 115 | ['2^1 * 2^3', 116 | '2^(1 + 3)', 117 | '2^4'], 118 | ], 119 | ['3^3 * 3 * 3', 120 | ['3^3 * 3^1 * 3^1', 121 | '3^(3 + 1 + 1)', 122 | '3^5'], 123 | ], 124 | ]; 125 | tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1], t[2])); 126 | }); 127 | 128 | describe('collectAndCombineSearch with no substeps', function () { 129 | const tests = [ 130 | ['nthRoot(x, 2) * nthRoot(x, 2)', 'nthRoot(x * x, 2)'], 131 | ['-nthRoot(x, 2) * nthRoot(x, 2)', '-1 * nthRoot(x * x, 2)'], 132 | ['-nthRoot(x, 2) * -nthRoot(x, 2)', '1 * nthRoot(x * x, 2)'], 133 | ['2x + 4x + x', '7x'], 134 | ['x * x^2 * x', 'x^4'], 135 | ['3*nthRoot(11) - 2*nthRoot(11)', '1 * nthRoot(11)'], 136 | ['nthRoot(xy) + 2x + nthRoot(xy) + 5x', '2 * nthRoot(xy) + 7x'], 137 | ]; 138 | tests.forEach(t => testSimpleCollectAndCombineSearch(t[0], t[1])); 139 | }); 140 | 141 | describe('collect and multiply like terms', function() { 142 | const tests = [ 143 | ['10^3 * 10^2', '10^5'], 144 | ['2^4 * 2 * 2^4 * 2', '2^10'] 145 | ]; 146 | tests.forEach(t => testSimpleCollectAndCombineSearch(t[0], t[1])); 147 | }); 148 | -------------------------------------------------------------------------------- /test/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.test.js: -------------------------------------------------------------------------------- 1 | const evaluateConstantSum = require('../../../lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testEvaluateConstantSum(exprString, outputList) { 6 | const lastString = outputList[outputList.length - 1]; 7 | TestUtil.testSubsteps(evaluateConstantSum, exprString, outputList, lastString); 8 | } 9 | 10 | describe('evaluateConstantSum', function () { 11 | const tests = [ 12 | ['4/10 + 3/5', 13 | ['4/10 + (3 * 2) / (5 * 2)', 14 | '4/10 + (3 * 2) / 10', 15 | '4/10 + 6/10', 16 | '(4 + 6) / 10', 17 | '10/10', 18 | '1'] 19 | ], 20 | ['4/5 + 3/5 + 2', 21 | ['2 + (4/5 + 3/5)', 22 | '2 + 7/5', 23 | '17/5'] 24 | ], 25 | ['9 + 4/5 + 1/5 + 2', 26 | ['(9 + 2) + (4/5 + 1/5)', 27 | '11 + (4/5 + 1/5)', 28 | '11 + 1', 29 | '12'] 30 | ], 31 | ]; 32 | tests.forEach(t => testEvaluateConstantSum(t[0], t[1])); 33 | }); 34 | -------------------------------------------------------------------------------- /test/simplifyExpression/distributeSearch/distributeSearch.test.js: -------------------------------------------------------------------------------- 1 | const distributeSearch = require('../../../lib/simplifyExpression/distributeSearch'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testDistribute(exprStr, outputStr) { 6 | TestUtil.testSimplification(distributeSearch, exprStr, outputStr); 7 | } 8 | 9 | describe('distribute - into paren with addition', function () { 10 | const tests = [ 11 | ['-(x+3)', '(-x - 3)'], 12 | ['-(x - 3)', '(-x + 3)'], 13 | ['-(-x^2 + 3y^6)' , '(x^2 - 3y^6)'], 14 | ]; 15 | tests.forEach(t => testDistribute(t[0], t[1])); 16 | }); 17 | 18 | describe('distribute - into paren with multiplication/division', function () { 19 | const tests = [ 20 | ['-(x*3)', '(-x * 3)'], 21 | ['-(-x * 3)', '(x * 3)'], 22 | ['-(-x^2 * 3y^6)', '(x^2 * 3y^6)'], 23 | ]; 24 | tests.forEach(t => testDistribute(t[0], t[1])); 25 | }); 26 | 27 | function testDistributeSteps(exprString, outputList) { 28 | const lastString = outputList[outputList.length - 1]; 29 | TestUtil.testSubsteps(distributeSearch, exprString, outputList, lastString); 30 | } 31 | 32 | describe('distribute', function () { 33 | const tests = [ 34 | ['x*(x+2+y)', 35 | ['(x * x + x * 2 + x * y)', 36 | '(x^2 + 2x + x * y)'] 37 | ], 38 | ['(x+2+y)*x*7', 39 | ['(x * x + 2x + y * x) * 7', 40 | '(x^2 + 2x + y * x) * 7'] 41 | ], 42 | ['(5+x)*(x+3)', 43 | ['(5 * (x + 3) + x * (x + 3))', 44 | '((5x + 15) + (x^2 + 3x))'] 45 | ], 46 | ['-2x^2 * (3x - 4)', 47 | ['(-2x^2 * 3x - 2x^2 * -4)', 48 | '(-6x^3 + 8x^2)'] 49 | ], 50 | ]; 51 | tests.forEach(t => testDistributeSteps(t[0], t[1])); 52 | }); 53 | 54 | describe('distribute with fractions', function () { 55 | const tests = [ 56 | // distribute the non-fraction term into the numerator(s) 57 | ['(3 / x^2 + x / (x^2 + 3)) * (x^2 + 3)', 58 | '((3 * (x^2 + 3)) / (x^2) + (x * (x^2 + 3)) / (x^2 + 3))', 59 | ], 60 | 61 | // if both groupings have fraction, the rule does not apply 62 | ['(3 / x^2 + x / (x^2 + 3)) * (5 / x + x^5)', 63 | '((3 / (x^2) * 5 / x + 3 / (x^2) * x^5) + (x / (x^2 + 3) * 5 / x + x / (x^2 + 3) * x^5))', 64 | ], 65 | ]; 66 | 67 | const multiStepTests = [ 68 | 69 | ['(2 / x + 3x^2) * (x^3 + 1)', 70 | ['((2 * (x^3 + 1)) / x + 3x^2 * (x^3 + 1))', 71 | '((2 * (x^3 + 1)) / x + (3x^5 + 3x^2))'] 72 | ], 73 | 74 | ['(2x + x^2) * (1 / (x^2 -4) + 4x^2)', 75 | ['((1 * (2x + x^2)) / (x^2 - 4) + 4x^2 * (2x + x^2))', 76 | '((1 * (2x + x^2)) / (x^2 - 4) + (8x^3 + 4x^4))'] 77 | ], 78 | 79 | ['(2x + x^2) * (3x^2 / (x^2 -4) + 4x^2)', 80 | ['((3x^2 * (2x + x^2)) / (x^2 - 4) + 4x^2 * (2x + x^2))', 81 | '((3x^2 * (2x + x^2)) / (x^2 - 4) + (8x^3 + 4x^4))'] 82 | ], 83 | 84 | ]; 85 | 86 | tests.forEach(t => testDistribute(t[0], t[1])); 87 | 88 | multiStepTests.forEach(t => testDistributeSteps(t[0], t[1])); 89 | }); 90 | 91 | describe('expand base', function () { 92 | const tests = [ 93 | ['(nthRoot(x, 2))^2','nthRoot(x, 2) * nthRoot(x, 2)'], 94 | ['(nthRoot(x, 2))^3','nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2)'], 95 | ['3 * (nthRoot(x, 2))^4', '3 * nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2)'], 96 | ['(nthRoot(x, 2) + nthRoot(x, 3))^2', '(nthRoot(x, 2) + nthRoot(x, 3)) * (nthRoot(x, 2) + nthRoot(x, 3))'], 97 | ['(2x + 3)^2', '(2x + 3) * (2x + 3)'], 98 | ['(x + 3 + 4)^2', '(x + 3 + 4) * (x + 3 + 4)'], 99 | // These should not expand 100 | // Needs to have a positive integer exponent > 1 101 | ['x + 2', 'x + 2'], 102 | ['(x + 2)^-1', '(x + 2)^-1'], 103 | ['(x + 1)^x', '(x + 1)^x'], 104 | ['(x + 1)^(2x)', '(x + 1)^(2x)'], 105 | ['(x + 1)^(1/2)', '(x + 1)^(1/2)'], 106 | ['(x + 1)^2.5', '(x + 1)^2.5'], 107 | ]; 108 | 109 | tests.forEach(t => testDistribute(t[0], t[1])); 110 | }); 111 | -------------------------------------------------------------------------------- /test/simplifyExpression/divisionSearch/divisionSearch.test.js: -------------------------------------------------------------------------------- 1 | const divisionSearch = require('../../../lib/simplifyExpression/divisionSearch'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testSimplifyDivision(exprStr, outputStr) { 6 | TestUtil.testSimplification(divisionSearch, exprStr, outputStr); 7 | } 8 | 9 | describe('simplifyDivision', function () { 10 | const tests = [ 11 | ['6/x/5', '6 / (x * 5)'], 12 | ['-(6/x/5)', '-(6 / (x * 5))'], 13 | ['-6/x/5', '-6 / (x * 5)'], 14 | ['(2+2)/x/6/(y-z)','(2 + 2) / (x * 6 * (y - z))'], 15 | ['2/x', '2 / x'], 16 | ['x/(2/3)', 'x * 3/2'], 17 | ['x / (y/(z+a))', 'x * (z + a) / y'], 18 | ['x/((2+z)/(3/y))', 'x * (3 / y) / (2 + z)'], 19 | ]; 20 | tests.forEach(t => testSimplifyDivision(t[0], t[1])); 21 | }); 22 | -------------------------------------------------------------------------------- /test/simplifyExpression/fractionsSearch/addConstantAndFraction.test.js: -------------------------------------------------------------------------------- 1 | const addConstantAndFraction = require('../../../lib/simplifyExpression/fractionsSearch/addConstantAndFraction'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testAddConstantAndFraction(exprString, outputList) { 6 | const lastString = outputList[outputList.length - 1]; 7 | TestUtil.testSubsteps(addConstantAndFraction, exprString, outputList, lastString); 8 | } 9 | 10 | describe('addConstantAndFraction', function () { 11 | const tests = [ 12 | ['7 + 1/2', 13 | ['14/2 + 1/2', 14 | '(14 + 1) / 2', 15 | '15/2'] 16 | ], 17 | ['5/6 + 3', 18 | ['5/6 + 18/6', 19 | '(5 + 18) / 6', 20 | '23/6'], 21 | ], 22 | ['1/2 + 5.8', 23 | ['0.5 + 5.8', 24 | '6.3'], 25 | ], 26 | ['1/3 + 5.8', 27 | ['0.3333 + 5.8', 28 | '6.1333'] 29 | ], 30 | ]; 31 | tests.forEach(t => testAddConstantAndFraction(t[0], t[1])); 32 | }); 33 | -------------------------------------------------------------------------------- /test/simplifyExpression/fractionsSearch/addConstantFractions.test.js: -------------------------------------------------------------------------------- 1 | const addConstantFractions = require('../../../lib/simplifyExpression/fractionsSearch/addConstantFractions'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testAddConstantFractions(exprString, outputList) { 6 | const lastString = outputList[outputList.length - 1]; 7 | TestUtil.testSubsteps(addConstantFractions, exprString, outputList, lastString); 8 | } 9 | 10 | describe('addConstantFractions', function () { 11 | const tests = [ 12 | ['4/5 + 3/5', 13 | ['(4 + 3) / 5', 14 | '7/5'] 15 | ], 16 | ['4/10 + 3/5', 17 | ['4/10 + (3 * 2) / (5 * 2)', 18 | '4/10 + (3 * 2) / 10', 19 | '4/10 + 6/10', 20 | '(4 + 6) / 10', 21 | '10/10', 22 | '1'] 23 | ], 24 | ['4/9 + 3/5', 25 | ['(4 * 5) / (9 * 5) + (3 * 9) / (5 * 9)', 26 | '(4 * 5) / 45 + (3 * 9) / 45', 27 | '20/45 + 27/45', 28 | '(20 + 27) / 45', 29 | '47/45'] 30 | ], 31 | ['4/5 - 4/5', 32 | ['(4 - 4) / 5', 33 | '0/5', 34 | '0'] 35 | ], 36 | ]; 37 | tests.forEach(t => testAddConstantFractions(t[0], t[1])); 38 | }); 39 | -------------------------------------------------------------------------------- /test/simplifyExpression/fractionsSearch/cancelLikeTerms.test.js: -------------------------------------------------------------------------------- 1 | const cancelLikeTerms = require('../../../lib/simplifyExpression/fractionsSearch/cancelLikeTerms'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testCancelLikeTerms(exprStr, expectedStr) { 6 | TestUtil.testSimplification(cancelLikeTerms, exprStr, expectedStr); 7 | } 8 | 9 | describe('cancel like terms', function () { 10 | const tests = [ 11 | ['2/2', '1'], 12 | ['x^2/x^2', '1'], 13 | ['x^3/x^2', 'x^(3 - (2))'], // parens will be removed at end of step 14 | ['(x^3*y)/x^2', '(x^(3 - (2)) * y)'], 15 | ['-(7+x)^8/(7+x)^2', '-(7 + x)^(8 - (2))'], 16 | ['(2x^2 * 5) / (2x^2)', '5'], // these parens have to stay around 2x^2 to be parsed correctly. 17 | ['(x^2 * y) / x', '(x^(2 - (1)) * y)'], 18 | ['2x^2 / (2x^2 * 5)', '1/5'], 19 | ['x / (x^2*y)', 'x^(1 - (2)) / y'], 20 | ['(4x^2) / (5x^2)', '(4) / (5)'], 21 | ['(2x+5)^8 / (2x+5)^2', '(2x + 5)^(8 - (2))'], 22 | ['(4x^3) / (5x^2)', '(4x^(3 - (2))) / (5)'], 23 | ['-x / -x', '1'], 24 | ['2/ (4x)', '1 / (2x)'], 25 | ['2/ (4x^2)', '1 / (2x^2)'], 26 | ['2 a / a', '2'], 27 | ['(35 * nthRoot (7)) / (5 * nthRoot(5))', '(7 * nthRoot(7)) / nthRoot(5)'], 28 | ['3/(9r^2)', '1 / (3r^2)'], 29 | ['6/(2x)', '3 / (x)'], 30 | ['(40 * x) / (20 * y)', '(2x) / (y)'], 31 | ['(20 * x) / (40 * y)', '(x) / (2y)'], 32 | ['20x / (40y)', 'x / (2y)'], 33 | ['60x / (40y)', '3x / (2y)'], 34 | ['4x / (2y)', '2x / (y)'] 35 | ]; 36 | 37 | tests.forEach(t => testCancelLikeTerms(t[0], t[1])); 38 | }); 39 | -------------------------------------------------------------------------------- /test/simplifyExpression/fractionsSearch/divideByGCD.test.js: -------------------------------------------------------------------------------- 1 | const divideByGCD = require('../../../lib/simplifyExpression/fractionsSearch/divideByGCD'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testDivideByGCD(exprStr, outputStr) { 6 | TestUtil.testSimplification(divideByGCD, exprStr, outputStr); 7 | } 8 | 9 | function testDivideByGCDSubsteps(exprString, outputList, outputStr) { 10 | TestUtil.testSubsteps(divideByGCD, exprString, outputList, outputStr); 11 | } 12 | 13 | describe('simplifyFraction', function() { 14 | const tests = [ 15 | ['2/4', '1/2'], 16 | ['9/3', '3'], 17 | ['12/27', '4/9'], 18 | ['1/-3', '-1/3'], 19 | ['-3/-2', '3/2'], 20 | ['-1/-1', '1'], 21 | ]; 22 | tests.forEach(t => testDivideByGCD(t[0], t[1])); 23 | }); 24 | 25 | describe('simplifyFraction', function() { 26 | const tests = [ 27 | ['15/6', 28 | ['(5 * 3) / (2 * 3)', 29 | '5/2'], 30 | '5/2', 31 | ], 32 | ['24/40', 33 | ['(3 * 8) / (5 * 8)', 34 | '3/5'], 35 | '3/5', 36 | ] 37 | ]; 38 | tests.forEach(t => testDivideByGCDSubsteps(t[0], t[1], t[2])); 39 | }); 40 | -------------------------------------------------------------------------------- /test/simplifyExpression/fractionsSearch/simplifyFractionSigns.test.js: -------------------------------------------------------------------------------- 1 | const simplifyFractionSigns = require('../../../lib/simplifyExpression/fractionsSearch/simplifyFractionSigns'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testSimplifyFractionSigns(exprStr, outputStr) { 6 | TestUtil.testSimplification(simplifyFractionSigns, exprStr, outputStr); 7 | } 8 | 9 | describe('simplify signs', function() { 10 | const tests = [ 11 | ['-12x / -27', '12x / 27'], 12 | ['x / -y', '-x / y'], 13 | ]; 14 | tests.forEach(t => testSimplifyFractionSigns(t[0], t[1])); 15 | }); 16 | -------------------------------------------------------------------------------- /test/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.test.js: -------------------------------------------------------------------------------- 1 | const simplifyPolynomialFraction = require('../../../lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testSimplifyPolynomialFraction(exprStr, outputStr) { 6 | TestUtil.testSimplification(simplifyPolynomialFraction, exprStr, outputStr); 7 | } 8 | 9 | describe('simplifyPolynomialFraction', function() { 10 | const tests = [ 11 | ['2x/4', '1/2 x'], 12 | ['9y/3', '3y'], 13 | ['y/-3', '-1/3 y'], 14 | ['-3y/-2', '3/2 y'], 15 | ['-y/-1', 'y'], 16 | ['12z^2/27', '4/9 z^2'], 17 | ['1.6x / 1.6', 'x'], 18 | ]; 19 | tests.forEach(t => testSimplifyPolynomialFraction(t[0], t[1])); 20 | }); 21 | -------------------------------------------------------------------------------- /test/simplifyExpression/functionsSearch/absoluteValue.test.js: -------------------------------------------------------------------------------- 1 | const absoluteValue = require('../../../lib/simplifyExpression/functionsSearch/absoluteValue'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testAbsoluteValue(exprString, outputStr) { 6 | TestUtil.testSimplification(absoluteValue, exprString, outputStr); 7 | } 8 | 9 | describe('abs', function () { 10 | const tests = [ 11 | ['abs(4)', '4'], 12 | ['abs(-5)', '5'], 13 | ]; 14 | tests.forEach(t => testAbsoluteValue(t[0], t[1])); 15 | }); 16 | -------------------------------------------------------------------------------- /test/simplifyExpression/functionsSearch/nthRoot.test.js: -------------------------------------------------------------------------------- 1 | const NthRoot = require('../../../lib/simplifyExpression/functionsSearch/nthRoot'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testNthRoot(exprString, outputStr) { 6 | TestUtil.testSimplification(NthRoot.nthRoot, exprString, outputStr); 7 | } 8 | 9 | describe('simplify nthRoot', function () { 10 | const tests = [ 11 | ['nthRoot(4)', '2'], 12 | ['nthRoot(8, 3)', '2'], 13 | ['nthRoot(5 * 7)', 'nthRoot(5 * 7)'], 14 | ['nthRoot(4, 3)', 'nthRoot(4, 3)'], 15 | ['nthRoot(12)', '2 * nthRoot(3, 2)'], 16 | ['nthRoot(36)', '6'], 17 | ['nthRoot(72)', '2 * 3 * nthRoot(2, 2)'], 18 | ['nthRoot(x^2)', 'x'], 19 | ['nthRoot(x ^ 3)', 'nthRoot(x ^ 3)'], 20 | ['nthRoot(x^3, 3)', 'x'], 21 | ['nthRoot(-2)', 'nthRoot(-2)'], 22 | ['nthRoot(2 ^ x, x)', '2'], 23 | ['nthRoot(x ^ (1/2), 1/2)', 'x'], 24 | ['nthRoot(2 * 2, 2)', '2'], 25 | ['nthRoot(3 * 2 * 3 * 2, 2)', '2 * 3'], 26 | ['nthRoot(36*x)', '2 * 3 * nthRoot(x, 2)'], 27 | ['nthRoot(2 * 18 * x ^ 2, 2)', '2 * 3 * x'], 28 | ['nthRoot(x * x, 2)', 'x'], 29 | ['nthRoot(x * x * (2 + 3), 2)', 'x * nthRoot((2 + 3), 2)'], 30 | ['nthRoot(64, 3)', '4'], 31 | ['nthRoot(35937, 3)', '33'], 32 | ]; 33 | tests.forEach(t => testNthRoot(t[0], t[1])); 34 | }); 35 | 36 | function testNthRootSteps(exprString, outputList) { 37 | const lastString = outputList[outputList.length - 1]; 38 | TestUtil.testSubsteps(NthRoot.nthRoot, exprString, outputList, lastString); 39 | } 40 | 41 | describe('nthRoot steps', function () { 42 | const tests = [ 43 | ['nthRoot(12)', 44 | ['nthRoot(2 * 2 * 3)', 45 | 'nthRoot((2 * 2) * 3, 2)', 46 | 'nthRoot(2 ^ 2 * 3, 2)', 47 | 'nthRoot(2 ^ 2, 2) * nthRoot(3, 2)', 48 | '2 * nthRoot(3, 2)'] 49 | ], 50 | ['nthRoot(72)', 51 | ['nthRoot(2 * 2 * 2 * 3 * 3)', 52 | 'nthRoot((2 * 2) * 2 * (3 * 3), 2)', 53 | 'nthRoot(2 ^ 2 * 2 * 3 ^ 2, 2)', 54 | 'nthRoot(2 ^ 2, 2) * nthRoot(2, 2) * nthRoot(3 ^ 2, 2)', 55 | '2 * nthRoot(2, 2) * 3', 56 | '2 * 3 * nthRoot(2, 2)'] 57 | ], 58 | ['nthRoot(36*x)', 59 | ['nthRoot(2 * 2 * 3 * 3 * x)', 60 | 'nthRoot((2 * 2) * (3 * 3) * x, 2)', 61 | 'nthRoot(2 ^ 2 * 3 ^ 2 * x, 2)', 62 | 'nthRoot(2 ^ 2, 2) * nthRoot(3 ^ 2, 2) * nthRoot(x, 2)', 63 | '2 * 3 * nthRoot(x, 2)'] 64 | ], 65 | ['nthRoot(2 * 18 * x ^ 2, 2)', 66 | ['nthRoot(2 * 2 * 3 * 3 * x ^ 2, 2)', 67 | 'nthRoot((2 * 2) * (3 * 3) * x ^ 2, 2)', 68 | 'nthRoot(2 ^ 2 * 3 ^ 2 * x ^ 2, 2)', 69 | 'nthRoot(2 ^ 2, 2) * nthRoot(3 ^ 2, 2) * nthRoot(x ^ 2, 2)', 70 | '2 * 3 * x'] 71 | ], 72 | ['nthRoot(32, 3)', 73 | ['nthRoot(2 * 2 * 2 * 2 * 2, 3)', 74 | 'nthRoot((2 * 2 * 2) * (2 * 2), 3)', 75 | 'nthRoot(2 ^ 3 * 2 ^ 2, 3)', 76 | 'nthRoot(2 ^ 3, 3) * nthRoot(2 ^ 2, 3)', 77 | '2 * nthRoot(2 ^ 2, 3)'] 78 | ], 79 | ['nthRoot(32, 4)', 80 | ['nthRoot(2 * 2 * 2 * 2 * 2, 4)', 81 | 'nthRoot((2 * 2 * 2 * 2) * 2, 4)', 82 | 'nthRoot(2 ^ 4 * 2, 4)', 83 | 'nthRoot(2 ^ 4, 4) * nthRoot(2, 4)', 84 | '2 * nthRoot(2, 4)'] 85 | ], 86 | ['nthRoot(2 * 2 * 3 * 2, 3)', 87 | ['nthRoot((2 * 2 * 2) * 3, 3)', 88 | 'nthRoot(2 ^ 3 * 3, 3)', 89 | 'nthRoot(2 ^ 3, 3) * nthRoot(3, 3)', 90 | '2 * nthRoot(3, 3)'] 91 | ], 92 | ]; 93 | tests.forEach(t => testNthRootSteps(t[0], t[1])); 94 | }); 95 | -------------------------------------------------------------------------------- /test/simplifyExpression/multiplyFractionsSearch/multiplyFractionsSearch.test.js: -------------------------------------------------------------------------------- 1 | const multiplyFractionsSearch = require('../../../lib/simplifyExpression//multiplyFractionsSearch'); 2 | 3 | const TestUtil = require('../../TestUtil'); 4 | 5 | function testMultiplyFractionsSearch(exprString, outputStr) { 6 | TestUtil.testSimplification(multiplyFractionsSearch, exprString, outputStr); 7 | } 8 | 9 | describe('multiplyFractions', function () { 10 | const tests = [ 11 | ['3 * 1/5 * 5/9', '(3 * 1 * 5) / (5 * 9)'], 12 | ['3/7 * 10/11', '(3 * 10) / (7 * 11)'], 13 | ['2 * 5/x', '(2 * 5) / x'], 14 | ['2 * (5/x)', '(2 * 5) / x'], 15 | ['(5/x) * (2/x)', '(5 * 2) / (x * x)'], 16 | ['(5/x) * x', '(5x) / x'], 17 | ['2x * 9/x', '(2x * 9) / x'], 18 | ['-3/8 * 2/4', '(-3 * 2) / (8 * 4)'], 19 | ['(-1/2) * 4/5', '(-1 * 4) / (2 * 5)'], 20 | ['4 * (-1/x)', '(4 * -1) / x'], 21 | ['x * 2y / x', '(x * 2y) / x'], 22 | ['x/z * 1/2', '(x * 1) / (z * 2)'], 23 | ['(6y / x) * 4x', '(6y * 4x) / x'], 24 | ['2x * y / z * 10', '(2x * y * 10) / z'], 25 | ['-(1/2) * (1/2)', '(-1 * 1) / (2 * 2)'], 26 | ['x * -(1/x)', '(x * -1) / x'], 27 | ['-(5/y) * -(x/y)', '(-5 * -x) / (y * y)'], 28 | ]; 29 | tests.forEach(t => testMultiplyFractionsSearch(t[0], t[1])); 30 | }); 31 | -------------------------------------------------------------------------------- /test/simplifyExpression/oneStep.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const print = require('../../lib/util/print'); 4 | 5 | const ChangeTypes = require('../../lib/ChangeTypes'); 6 | const simplifyExpression = require('../../lib/simplifyExpression'); 7 | 8 | function testOneStep(exprStr, outputStr, debug=false) { 9 | const steps = simplifyExpression(exprStr); 10 | if (!steps.length) { 11 | return exprStr; 12 | } 13 | const nodeStatus = steps[0]; 14 | if (debug) { 15 | if (!nodeStatus.changeType) { 16 | throw Error('missing or bad change type'); 17 | } 18 | // eslint-disable-next-line 19 | console.log(nodeStatus.changeType); 20 | // eslint-disable-next-line 21 | console.log(print.ascii(nodeStatus.newNode)); 22 | } 23 | it(exprStr + ' -> ' + outputStr, function () { 24 | assert.deepEqual( 25 | print.ascii(nodeStatus.newNode), 26 | outputStr); 27 | }); 28 | } 29 | 30 | describe('arithmetic stepping', function() { 31 | const tests = [ 32 | ['(2+2)', '4'], 33 | ['(2+2)*5', '4 * 5'], 34 | ['5*(2+2)', '5 * 4'], 35 | ['2*(2+2) + 2^3', '2 * 4 + 2^3'], 36 | ['6*6', '36'], 37 | ]; 38 | tests.forEach(t => testOneStep(t[0], t[1])); 39 | }); 40 | 41 | describe('adding symbols without breaking things', function() { 42 | // nothing old breaks 43 | const tests = [ 44 | ['2+x', '2 + x'], 45 | ['(2+2)*x', '4x'], 46 | ['(2+2)*x+3', '4x + 3'], 47 | ]; 48 | tests.forEach(t => testOneStep(t[0], t[1])); 49 | }); 50 | 51 | describe('collecting like terms within the context of the stepper', function() { 52 | const tests = [ 53 | ['2+x+7', 'x + 9'], // substeps not tested here 54 | // ['2x^2 * y * x * y^3', '2 * x^3 * y^4'], // substeps not tested here 55 | ]; 56 | tests.forEach(t => testOneStep(t[0], t[1])); 57 | }); 58 | 59 | describe('collects and combines like terms', function() { 60 | const tests = [ 61 | ['(x + x) + (x^2 + x^2)', '2x + (x^2 + x^2)'], // substeps not tested here 62 | ['10 + (y^2 + y^2)', '10 + 2y^2'], // substeps not tested here 63 | ['10y^2 + 1/2 y^2 + 3/2 y^2', '12y^2'], // substeps not tested here 64 | ['x + y + y^2', 'x + y + y^2'], 65 | ['2x^(2+1)', '2x^3'], 66 | ]; 67 | tests.forEach(t => testOneStep(t[0], t[1])); 68 | }); 69 | 70 | describe('stepThrough returning no steps', function() { 71 | it('12x^2 already simplified', function () { 72 | assert.deepEqual( 73 | simplifyExpression('12x^2'), 74 | []); 75 | }); 76 | it('2*5x^2 + sqrt(5) has unsupported sqrt', function () { 77 | assert.deepEqual( 78 | simplifyExpression('2*5x^2 + sqrt(5)'), 79 | []); 80 | }); 81 | }); 82 | 83 | describe('keeping parens in important places, on printing', function() { 84 | testOneStep('5 + (3*6) + 2 / (x / y)', '5 + (3 * 6) + 2 * y / x'); 85 | testOneStep('-(x + y) + 5+3', '8 - (x + y)'); 86 | }); 87 | 88 | describe('fractions', function() { 89 | testOneStep('2 + 5/2 + 3', '5 + 5/2'); // collect and combine without substeps 90 | }); 91 | 92 | describe('simplifyDoubleUnaryMinus step actually happens', function () { 93 | it('22 - (-7) -> 22 + 7', function() { 94 | const steps = simplifyExpression('22 - (-7)'); 95 | assert.equal(steps[0].changeType, ChangeTypes.RESOLVE_DOUBLE_MINUS); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/util/Util.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const Util = require('../../lib/util/Util'); 4 | 5 | describe('appendToArrayInObject', function () { 6 | it('creates empty array', function () { 7 | const object = {}; 8 | Util.appendToArrayInObject(object, 'key', 'value'); 9 | assert.deepEqual( 10 | object, 11 | {'key': ['value']} 12 | ); 13 | }); 14 | it('appends to array if it exists', function () { 15 | const object = {'key': ['old_value']}; 16 | Util.appendToArrayInObject(object, 'key', 'new_value'); 17 | assert.deepEqual( 18 | object, 19 | {'key': ['old_value', 'new_value']} 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/util/flattenOperands.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const math = require('mathjs'); 3 | 4 | const print = require('../../lib/util/print'); 5 | 6 | const Node = require('../../lib/node'); 7 | const TestUtil = require('../TestUtil'); 8 | 9 | function testFlatten(exprStr, afterNode, debug=false) { 10 | const flattened = TestUtil.parseAndFlatten(exprStr); 11 | if (debug) { 12 | // eslint-disable-next-line 13 | console.log(print.ascii(flattened)); 14 | } 15 | TestUtil.removeComments(flattened); 16 | TestUtil.removeComments(afterNode); 17 | it(print.ascii(flattened), function() { 18 | assert.deepEqual(flattened, afterNode); 19 | }); 20 | } 21 | 22 | // to create nodes, for testing 23 | const opNode = Node.Creator.operator; 24 | const constNode = Node.Creator.constant; 25 | const symbolNode = Node.Creator.symbol; 26 | const parenNode = Node.Creator.parenthesis; 27 | 28 | describe('flattens + and *', function () { 29 | const tests = [ 30 | ['2+2', math.parse('2+2')], 31 | ['2+2+7', opNode('+', [constNode(2), constNode(2), constNode(7)])], 32 | ['9*8*6+3+4', 33 | opNode('+', [ 34 | opNode('*', [constNode(9), constNode(8), constNode(6)]), 35 | constNode(3), 36 | constNode(4)])], 37 | ['5*(2+3+2)*10', 38 | opNode('*', [ 39 | constNode(5), 40 | parenNode(opNode('+', [constNode(2), constNode(3),constNode(2)])), 41 | constNode(10)])], 42 | // keeps the polynomial term 43 | ['9x*8*6+3+4', 44 | opNode('+', [ 45 | opNode('*', [math.parse('9x'), constNode(8), constNode(6)]), 46 | constNode(3), 47 | constNode(4)])], 48 | ['9x*8*6+3y^2+4', 49 | opNode('+', [ 50 | opNode('*', [math.parse('9x'), constNode(8), constNode(6)]), 51 | math.parse('3y^2'), 52 | constNode(4)])], 53 | // doesn't flatten 54 | ['2 x ^ (2 + 1) * y', math.parse('2 x ^ (2 + 1) * y')], 55 | ['2 x ^ (2 + 1 + 2) * y', 56 | opNode('*', [ 57 | opNode('*', [constNode(2), 58 | opNode('^', [symbolNode('x'), parenNode( 59 | opNode('+', [constNode(2), constNode(1), constNode(2)]))]), 60 | ], true), symbolNode('y')]) 61 | ], 62 | ['3x*4x', opNode('*', [math.parse('3x'), math.parse('4x')])] 63 | ]; 64 | tests.forEach(t => testFlatten(t[0], t[1])); 65 | }); 66 | 67 | describe('flattens division', function () { 68 | const tests = [ 69 | // groups x/4 and continues to flatten * 70 | ['2 * x / 4 * 6 ', 71 | opNode('*', [opNode('/', [ 72 | math.parse('2x'), math.parse('4')]), constNode(6)])], 73 | ['2*3/4/5*6', 74 | opNode('*', [constNode(2), math.parse('3/4/5'), constNode(6)])], 75 | // combines coefficient with x 76 | ['x / (4 * x) / 8', 77 | math.parse('x / (4x) / 8')], 78 | ['2 x * 4 x / 8', 79 | opNode('*', [math.parse('2x'), opNode( 80 | '/', [math.parse('4x'), constNode(8)])])], 81 | ]; 82 | tests.forEach(t => testFlatten(t[0], t[1])); 83 | }); 84 | 85 | describe('subtraction', function () { 86 | const tests = [ 87 | ['1 + 2 - 3 - 4 + 5', 88 | opNode('+', [ 89 | constNode(1), constNode(2), constNode(-3), constNode(-4), constNode(5)])], 90 | ['x - 3', opNode('+', [symbolNode('x'), constNode(-3)])], 91 | ['x + 4 - (y+4)', 92 | opNode('+', [symbolNode('x'), constNode(4), math.parse('-(y+4)')])], 93 | ]; 94 | tests.forEach(t => testFlatten(t[0], t[1])); 95 | }); 96 | 97 | describe('flattens nested functions', function () { 98 | const tests = [ 99 | ['nthRoot(11)(x+y)', 100 | math.parse('nthRoot(11) * (x+y)')], 101 | ['abs(3)(1+2)', 102 | math.parse('abs(3) * (1+2)')], 103 | ['nthRoot(2)(nthRoot(18)+4*nthRoot(3))', 104 | math.parse('nthRoot(2) * (nthRoot(18)+4*nthRoot(3))')], 105 | ['nthRoot(6,3)(10+4x)', 106 | math.parse('nthRoot(6,3) * (10+4x)')] 107 | ]; 108 | tests.forEach(t => testFlatten(t[0], t[1])); 109 | }); 110 | -------------------------------------------------------------------------------- /test/util/print.test.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const Node = require('../../lib/node'); 4 | const print = require('../../lib/util/print'); 5 | 6 | const TestUtil = require('../TestUtil'); 7 | 8 | // to create nodes, for testing 9 | const opNode = Node.Creator.operator; 10 | const constNode = Node.Creator.constant; 11 | const symbolNode = Node.Creator.symbol; 12 | 13 | function testPrintStr(exprStr, outputStr) { 14 | const input = math.parse(exprStr); 15 | TestUtil.testFunctionOutput(print.ascii, input, outputStr); 16 | } 17 | 18 | function testLatexPrintStr(exprStr, outputStr) { 19 | const input = TestUtil.parseAndFlatten(exprStr); 20 | TestUtil.testFunctionOutput(print.latex, input, outputStr); 21 | } 22 | 23 | function testPrintNode(node, outputStr) { 24 | TestUtil.testFunctionOutput(print.ascii, node, outputStr); 25 | } 26 | 27 | describe('print asciimath', function () { 28 | const tests = [ 29 | ['2+3+4', '2 + 3 + 4'], 30 | ['2 + (4 - x) + - 4', '2 + (4 - x) - 4'], 31 | ['2/3 x^2', '2/3 x^2'], 32 | ['-2/3', '-2/3'], 33 | ]; 34 | tests.forEach(t => testPrintStr(t[0], t[1])); 35 | }); 36 | 37 | describe('print latex', function() { 38 | const tests = [ 39 | ['2+3+4', '2+3+4'], 40 | ['2 + (4 - x) - 4', '2+\\left(4 - x\\right) - 4'], 41 | ['2/3 x^2', '\\frac{2}{3}~{ x}^{2}'], 42 | ['-2/3', '\\frac{-2}{3}'], 43 | ['2*x+4y', '2~ x+4~ y'], 44 | ]; 45 | tests.forEach(t => testLatexPrintStr(t[0],t[1])); 46 | }); 47 | 48 | describe('print with parenthesis', function () { 49 | const tests = [ 50 | [opNode('*', [ 51 | opNode('+', [constNode(2), constNode(3)]), 52 | symbolNode('x') 53 | ]), '(2 + 3) * x'], 54 | [opNode('^', [ 55 | opNode('-', [constNode(7), constNode(4)]), 56 | symbolNode('x') 57 | ]), '(7 - 4)^x'], 58 | [opNode('/', [ 59 | opNode('+', [constNode(9), constNode(2)]), 60 | symbolNode('x') 61 | ]), '(9 + 2) / x'], 62 | ]; 63 | tests.forEach(t => testPrintNode(t[0], t[1])); 64 | }); 65 | -------------------------------------------------------------------------------- /test/util/removeUnnecessaryParens.test.js: -------------------------------------------------------------------------------- 1 | const math = require('mathjs'); 2 | 3 | const print = require('../../lib/util/print'); 4 | const removeUnnecessaryParens = require('../../lib/util/removeUnnecessaryParens'); 5 | 6 | const TestUtil = require('../TestUtil'); 7 | 8 | function testRemoveUnnecessaryParens(exprStr, outputStr) { 9 | const input = removeUnnecessaryParens(math.parse(exprStr)); 10 | TestUtil.testFunctionOutput(print.ascii, input, outputStr); 11 | } 12 | 13 | describe('removeUnnecessaryParens', function () { 14 | const tests = [ 15 | ['(x+4) + 12', 'x + 4 + 12'], 16 | ['-(x+4x) + 12', '-(x + 4x) + 12'], 17 | ['x + (12)', 'x + 12'], 18 | ['x + (y)', 'x + y'], 19 | ['x + -(y)', 'x - y'], 20 | ['((3 - 5)) * x', '(3 - 5) * x'], 21 | ['((3 - 5)) * x', '(3 - 5) * x'], 22 | ['(((-5)))', '-5'], 23 | ['((4+5)) + ((2^3))', '(4 + 5) + 2^3'], 24 | ['(2x^6 + -50 x^2) - (x^4)', '2x^6 - 50x^2 - x^4'], 25 | ['(x+4) - (12 + x)', 'x + 4 - (12 + x)'], 26 | ['(2x)^2', '(2x)^2'], 27 | ['((4+x)-5)^(2)', '(4 + x - 5)^2'], 28 | ]; 29 | tests.forEach(t => testRemoveUnnecessaryParens(t[0], t[1])); 30 | }); 31 | --------------------------------------------------------------------------------