diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 0b55ed11..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,85 +0,0 @@ -module.exports = { - "env": { - "browser": true, - "es6": true - }, - "plugins": [ - "sort-requires" - ], - "extends": "eslint:recommended", - "parserOptions": { - "sourceType": "module" - }, - "globals": { - "require": true, - "module": true, - "describe": true, - "it": true - }, - "rules": { - "indent": [ - "error", - 2 - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "eqeqeq": [ - "error", - "always" - ], - "prefer-const": [ - "error", { - "destructuring": "any" - } - ], - "no-trailing-spaces": [ - "error" - ], - "object-shorthand": [ - "error", - "always", { - "avoidQuotes": true - } - ], - "brace-style": [ - "error", - "stroustrup", - { - "allowSingleLine": true - } - ], - "sort-requires/sort-requires": [ - "error" - ], - "keyword-spacing": [ - "error" - ], - "strict": [ - "error" - ], - "eol-last": [ - "error", - "always" - ], - "no-multiple-empty-lines": [ - "error", - { - "max": 2, - "maxEOF": 1 - } - ], - "no-trailing-spaces": [ - "error" - ] - } -}; diff --git a/.gitignore b/.gitignore index 6a48e0fb..6d2d9cba 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ node_modules *.log .DS_Store .idea + +/lib/dist +/test/dist +/dist diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/.travis.yml b/.travis.yml index c4dc5168..1adcdcf5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,4 @@ node_js: notifications: email: false script: - - npm run lint - npm test diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index d25fb234..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -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. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -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. - -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. - -## Scope - -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. - -## Enforcement - -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. - -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. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index f373df6b..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,122 +0,0 @@ -# Contributing to mathsteps - -#### 🎉 We're excited to have you helping out! Thank you so much for your time 🎉 - -## Contents - -### Before you get started - -- [Code of Conduct](#code-of-conduct) -- [How (and why) we built mathsteps](#how-and-why-we-built-mathsteps) -- [Expression trees in mathJS](#expression-trees-in-mathjs) -- [Coding conventions](#coding-conventions) - -### Contributing - -- [Ways to help out](#ways-to-help-out) -- [Creating a pull request](#creating-a-pull-request) -- [Testing](#testing) - - -## Before you get started - -### Code of Conduct - -This project adheres to the Contributor Covenant -[Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to -uphold this code. Please report unacceptable behavior to mathsteps@socratic.org - -### How (and why) we built mathsteps - -Read about how and why we built mathsteps on our -[blog](https://blog.socratic.org/stepping-into-math-open-sourcing-our-step-by-step-solver-9b5da066ae36). - -### Expression trees in mathJS - -Most of this code iterates over expression trees to make step by step -simplifications. We use -[mathJS expresison trees](http://mathjs.org/docs/expressions/expression_trees.html#expression-trees), -which we recommend you learn a bit about. - -There are a few different types of nodes that show up in the tree. This stepper -uses OperationNode, ParenthesisNode, ConstantNode, SymbolNode, and FunctionNode. -You can also read about them on the -[mathJS expressions documentation](http://mathjs.org/docs/expressions/expression_trees.html#nodes). - -Keep in mind when dealing with these trees that child nodes are called different -things depending on the parent node type. For example, operation nodes have -"args" as their children, and parenthesis nodes have a single child called -"content". - -**Tricky catch**: any subtraction in the tree will be converted to an addition, -by negating the number being subtracted, e.g. `2 - 3` would be `2 + -3` in the -tree. This is so that all addition and subtraction is flat. For instance, -`2 + 3 - 5 + 8` would become one addition operation with `2`, `3`, `-5`, and `8` -as its child nodes. This is a common strategy for computer algebra systems but -can be confusing and easy to forget. So at most points in the codebase, there -should be no operators with `-` sign. If you're curious what the code that -modifies subtraction looks like, you can find it in -[flattenOperands.js](/lib/util/flattenOperands.js). - -### Coding conventions - -mathsteps follows the node.js code style as described -[here](https://github.com/felixge/node-style-guide). - -To lint your code, run `npm run lint .` - -## Contributing - -### Ways to help out - -- **Spread the word!** If you think mathsteps is cool, tell your friends! Let - them know they can use this and that they can contribute. -- **Suggest features!** Have an idea for something mathsteps should solve or a - way for it to teach math better? If your idea is not an - [existing issue](https://github.com/socraticorg/mathsteps/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement), - create a new issue with the label "enhancement". -- **Report bugs!** If the bug is not an - [existing issue](https://github.com/socraticorg/mathsteps/issues?q=is%3Aopen+is%3Aissue+label%3Abug), - create a new issue with the label "bug" and provide as many details as you - can, so that someone else can reproduce it. -- **Contribute code!** - We'd love to have more contributors working on this. - Check out the section below with more information on how to contribute, - and feel free to email us at mathsteps@socratic.org with any questions! - -### Creating a pull request - -We're excited to see your [pull request](https://help.github.com/articles/about-pull-requests/)! - -- If you want to work on something, please comment on the - [related issue](https://github.com/socraticorg/mathsteps/issues) on GitHub - before you get started, so others are aware that you're working on it. If - there's no existing issue for the change you'd like to make, you can - [create a new issue](https://github.com/socraticorg/mathsteps/issues/new). - -- 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) - -- Make sure all the unit tests pass (with `npm test`) before creating the pull - request, and please add your own tests for the changes that you made. If - you're not sure how to add tests, or are confused about why tests are failing, - it's fine to create the pull request first and we'll help you get things - working. - -### Testing - -- Make sure you properly unit-test your changes. -- Run tests with `npm test` -- Install Git hooks with `npm run setup-hooks`. This will add a pre-commit hook - which makes sure tests are passing and the code is eslint-compliant. -- If you want to see what the expression tree looks like at any point in the - code (for debugging), you can log a `node` as an expression string (e.g. - '2x + 5') with `console.log(print.ascii(node))`, and you can log the full tree - structure with `console.log(JSON.stringify(node, null, 2))` - - -There's lots to be done, lots of students to help, and we're so glad you'll be a -part of this. - -Thanks! ❤️ ❤️ - -_mathsteps team_ diff --git a/HISTORY.md b/HISTORY.md deleted file mode 100644 index 417a1cb6..00000000 --- a/HISTORY.md +++ /dev/null @@ -1,91 +0,0 @@ -# History - - -## 2017-10-26, version 0.1.7 - -There's been a lot of great changes since the last release, here are the main updates - -Functionality and Teaching Enhancements: - -- new pedagogy for multiply powers integers #153 -- exposing the factoring module and adding more coverage #148 -- simplify roots of any degree #183 -- more cases for cancelling terms #182 -- greatest common denominator substep #188 -- multiply nthRoots #189 -- multiply fractions with parenthesis #185 -- remove unnecessary parens before solving equations #205 -- multiply denominators with terms #88 -- Better sum-product factoring steps #210 - - -Bug Fixes - -- fix the check for perfect roots of a constant when there's roundoff error #224 -- large negtive number rounding #216 - -Other: - -- (code structure) generalizing polynomial terms #190 -- latex printing for equations -- added linting rules #222 - - -## 2017-04-03, version 0.1.6 - -updated mathjs to incorporate vulnerability patch #149 - -Functionality Enhancements: - -- Added factoring support #104 -- Fixed #138: Better handling of distribution with fractions. Thanks @lexiross ! -- Fixed #126: Add parens in util > print where necessary. Thanks @Flyr1Q ! - -Bug fixes: - -- Fixed #113: handle exponents on coefficients of polynomial terms. Thanks @shirleymiao ! -- Fixed #111 (nthRoot() existence check). Thanks @shirleymiao ! - -Refactoring + Documentation + other dev enhancements: - -- Fixed #107: Improve our linter. Thanks @Raibaz ! -- Added Travis continuous integration -- Refactor test to use TestUtil. Thanks @nitin42 ! -- Work on #58: Adding missing tests. Thanks @nitin42 ! - -## 2017-01-29, version 0.1.5 - -Reverted #82 (Added script to check the installed node version) and mention -node version requiremnts in the README. - -## 2017-01-29, version 0.1.4 - -Functionality Enhancements: - -- Fixed #39: Add rule to simplify 1^x to 1. Thanks @michaelmior ! -- Fixed #82: Added script to check the installed node version. Thanks @Raibaz ! - -Bug fixes: - -- Fixed #77: bug where oldNode was null on every step. Thanks @hmaurer ! -- Handle unary minus nodes that have an argument that is a parentheses. Thanks - @tkosan ! - -Refactoring + Documentation + other dev enhancements: - -- Fixed #73: replace New Kids on the Block video with one that's not restricted - in most of the world -- Fixed #80: Use object literal property value shorthand. Thanks @cspanda ! -- Fixed #62: Separated basicsSearch simplifications into their own files. Thanks - @Raibaz ! -- Fixed #78: pre-commit hook to run tests and linter before a git commit. Thanks - @hmaurer ! -- Improvements from #44: Added Linting rules. Thanks @biyasbasak ! -- Fixed #91: Refactor isOperator to accept operator parameter. Thanks - @mcarthurgill ! -- Fixed #86: Clean up CONTRIBUTING.md. Thanks @faheel ! -- Fixed #34: Make a helper function getRadicandNode. Thanks @lexiross ! -- Fixed #95: Create RESOURCES.md for people to share relevant software, - projects, and papers -- Fixed #102: Add a complete code example for solving an equation. Thanks - @karuppiah7890 ! diff --git a/README.md b/README.md index 83efbf56..dba5a641 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -## A step by step solver for math +npm badge -[![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) -[![Build Status](https://travis-ci.org/socraticorg/mathsteps.svg?branch=master)](https://travis-ci.org/socraticorg/mathsteps) - -https://www.youtube.com/watch?v=iCrargw1rrM +## Why Mathsteps +Mathsteps aims to provide step-by-step instructions like a tutor would give them to a student. ## Requirements Mathsteps requires Node version > 6.0.0 -## Usage +## Usage Example To install mathsteps using npm: - npm install mathsteps + (Coming) + + npm install @taskbase/mathsteps ```js const mathsteps = require('mathsteps'); @@ -40,37 +40,96 @@ steps.forEach(step => { }); ``` -(if you're using mathsteps v0.1.6 or lower, use `.print()` instead of `.ascii()`) - To see all the change types: ```js const changes = mathsteps.ChangeTypes; ``` +## Which syntax is mathsteps expecting? + +[Asciimath](http://asciimath.org/) + + + + + +## Simplify Expression +Simplifying Expressions + +### What can mathsteps simplifyExpression do? +- arithmetic simplification: `["(2+2)*5", "20"]` +- collect and combines like terms: `["x^2 + 3x*(-4x) + 5x^3 + 3x^2 + 6", "5x^3 - 8x^2 + 6"]` +- simplify with division: `["(20 * x) / (5 * (40 * y))", "x / (10y)"]` +- deal with fractions: `["2(x+3)/3", "2x / 3 + 2"]` +- cancelling out: `["(1+2a)/a", "1 / a + 2"]` +- deal with absolute values: `["(x^3*y)/x^2 + abs(-5)", "x * y + 5"]` +- deal with nth roots: `["x * nthRoot(x^4, 2)", "x^3"]` + +unsure: +- deal with higher order polynomials: ? + +### What can't mathsteps simplifyExpression do? +- + + +## Solve Equation +Solving equations. +### What can solveEquation do? +- Solve linear equations with one variable +- Solve for y in linear equations with x and y as variables. +- Solve binomic equation +- Some inequalities (e.g. "x + 2 > 3" to "x > 1") +- Constant comparison (e.g. "1 = 2" to ChangeTypes.STATEMENT_IS_FALSE) -## Contributing +### What can't solveEquation do? +- Expressions with multiple variables other than y and x +- Some inequalities (e.g. "( x )/( 2x + 7) >= 4") +- Calculate square root of x^2, so result will stay e.g. x^2=2 -Hi! If you're interested in working on this, that would be super awesome! -Learn more here: [CONTRIBUTING.md](CONTRIBUTING.md). ## Build First clone the project from github: - git clone https://github.com/socraticorg/mathsteps.git - cd mathsteps +``` +git clone https://github.com/taskbase/mathsteps.git +cd mathsteps +``` Install the project dependencies: - npm install +``` +npm ci +cd lib && npm ci && cd .. +cd example-consumer && npm ci && cd .. +``` + +## Publish version + +See scripts in `package.json`. ## Test -To execute tests for the library, install the project dependencies once: +To execute tests for the library, install the project dependencies: - npm install +``` +npm ci +``` Then, the tests can be executed: - npm test +``` +npm test +``` + +## TODOs +- Fix usage of factoring for simplification +- Throw error instead of returning empty array? + +### Done (compared to https://github.com/google/mathsteps) +- Port to TypeScript! + +## Attribution +Based on google/mathsteps + diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index ea2472a6..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,27 +0,0 @@ -# Roadmap - -A rough roadmap for `mathsteps` - -Last time we did a ground truth to see what concepts are needed to answer -questions asked in the Soratic app, this was the breakdown (note that some -concepts might overlap and both need to be covered for us to solve the problem): - -- Factoring: 22.84% of questions -- Solve Equation: 17.99% of questions -- Systems of equations: 6.34% of questions -- Negative exponents: 1.96% of questions - -## Version 1.x - -- Factoring quadratics to solve equations ([open issues that are - related](https://github.com/socraticorg/mathsteps/issues?utf8=%E2%9C%93&q=is%3Aopen%20equation)) -- Factoring (quadratics and other simple factoring) to simplify polynomial - fractions -- Improving exponents ([open issues that are - related](https://github.com/socraticorg/mathsteps/issues?q=is%3Aissue+is%3Aopen+exponents+label%3Aexponents)) -- Improving roots ([open issues that are - related](https://github.com/socraticorg/mathsteps/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Aroots%20)) -- Systems of equations ([#48](https://github.com/socraticorg/mathsteps/issues/48)) - -## Version 2.x - diff --git a/example-consumer/index.ts b/example-consumer/index.ts new file mode 100644 index 00000000..96cd3f78 --- /dev/null +++ b/example-consumer/index.ts @@ -0,0 +1,3 @@ +import {solveEquation} from '@taskbase/mathsteps'; +const solved = solveEquation('2x + 2x = 2'); +console.log(solved); diff --git a/example-consumer/package-lock.json b/example-consumer/package-lock.json new file mode 100644 index 00000000..dd2835e9 --- /dev/null +++ b/example-consumer/package-lock.json @@ -0,0 +1,131 @@ +{ + "name": "mathsteps-example-consumer", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@taskbase/mathsteps": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@taskbase/mathsteps/-/mathsteps-1.0.0-beta.1.tgz", + "integrity": "sha512-in+B1urMAXj13SL004mIarlOOZqLqspEVd24ykPF5KyU85lMhTE+JWefkaX9cTqIAQCMXynhCRE2BHk5NEiKng==", + "requires": { + "mathjs": "3.11.2" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "complex.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.1.tgz", + "integrity": "sha1-6pDHoFrs6vOjdtLA9qeEIXJ9aHk=" + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "decimal.js": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-7.1.1.tgz", + "integrity": "sha1-GtytfXDXqRxCbXVvHrZWbDvmy88=" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "fraction.js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.0.tgz", + "integrity": "sha1-c5dOL4tR73CVNtYkzJB4Liu2EnQ=" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "mathjs": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-3.11.2.tgz", + "integrity": "sha1-0spyPX6SVMxY0UIaWmHYz6yP9kI=", + "requires": { + "complex.js": "2.0.1", + "decimal.js": "7.1.1", + "fraction.js": "4.0.0", + "seed-random": "2.2.0", + "tiny-emitter": "1.0.2", + "typed-function": "0.10.5" + } + }, + "seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "tiny-emitter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.0.2.tgz", + "integrity": "sha1-jklHDT9V+J4kchA2imu5+1GqFgE=" + }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "typed-function": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-0.10.5.tgz", + "integrity": "sha1-Lg8Yq9BlIZ+raUpEamXG0ZgYMsA=" + }, + "typescript": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/example-consumer/package.json b/example-consumer/package.json new file mode 100644 index 00000000..9179e377 --- /dev/null +++ b/example-consumer/package.json @@ -0,0 +1,22 @@ +{ + "name": "mathsteps-example-consumer", + "version": "0.0.0", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "test": "npx ts-node index", + "linklib": "npm run --prefix ../ build && npm link ../dist", + "install-beta": "npm install @taskbase/mathsteps@beta", + "install-lib": "npm install @taskbase/mathsteps" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "ts-node": "^9.1.1", + "typescript": "^4.2.3" + }, + "dependencies": { + "@taskbase/mathsteps": "^1.0.0-beta.1" + } +} diff --git a/index.js b/index.js deleted file mode 100644 index ce11df3f..00000000 --- a/index.js +++ /dev/null @@ -1,11 +0,0 @@ -const ChangeTypes = require('./lib/ChangeTypes'); -const factor = require('./lib/factor'); -const simplifyExpression = require('./lib/simplifyExpression'); -const solveEquation = require('./lib/solveEquation'); - -module.exports = { - factor, - simplifyExpression, - solveEquation, - ChangeTypes, -}; diff --git a/lib/ChangeTypes.js b/lib/ChangeTypes.js deleted file mode 100644 index 0090044e..00000000 --- a/lib/ChangeTypes.js +++ /dev/null @@ -1,207 +0,0 @@ -// The text to identify rules for each possible step that can be taken - -module.exports = { - NO_CHANGE: 'NO_CHANGE', - - // ARITHMETIC - - // e.g. 2 + 2 -> 4 or 2 * 2 -> 4 - SIMPLIFY_ARITHMETIC: 'SIMPLIFY_ARITHMETIC', - - // BASICS - - // e.g. 2/-1 -> -2 - DIVISION_BY_NEGATIVE_ONE: 'DIVISION_BY_NEGATIVE_ONE', - // e.g. 2/1 -> 2 - DIVISION_BY_ONE: 'DIVISION_BY_ONE', - // e.g. x * 0 -> 0 - MULTIPLY_BY_ZERO: 'MULTIPLY_BY_ZERO', - // e.g. x * 2 -> 2x - REARRANGE_COEFF: 'REARRANGE_COEFF', - // e.g. x ^ 0 -> 1 - REDUCE_EXPONENT_BY_ZERO: 'REDUCE_EXPONENT_BY_ZERO', - // e.g. 0/1 -> 0 - REDUCE_ZERO_NUMERATOR: 'REDUCE_ZERO_NUMERATOR', - // e.g. 2 + 0 -> 2 - REMOVE_ADDING_ZERO: 'REMOVE_ADDING_ZERO', - // e.g. x ^ 1 -> x - REMOVE_EXPONENT_BY_ONE: 'REMOVE_EXPONENT_BY_ONE', - // e.g. 1 ^ x -> 1 - REMOVE_EXPONENT_BASE_ONE: 'REMOVE_EXPONENT_BASE_ONE', - // e.g. x * -1 -> -x - REMOVE_MULTIPLYING_BY_NEGATIVE_ONE: 'REMOVE_MULTIPLYING_BY_NEGATIVE_ONE', - // e.g. x * 1 -> x - REMOVE_MULTIPLYING_BY_ONE: 'REMOVE_MULTIPLYING_BY_ONE', - // e.g. 2 - - 3 -> 2 + 3 - RESOLVE_DOUBLE_MINUS: 'RESOLVE_DOUBLE_MINUS', - - // COLLECT AND COMBINE AND BREAK UP - - // e.g. 2 + x + 3 + x -> 5 + 2x - COLLECT_AND_COMBINE_LIKE_TERMS: 'COLLECT_AND_COMBINE_LIKE_TERMS', - // e.g. x + 2 + x^2 + x + 4 -> x^2 + (x + x) + (4 + 2) - COLLECT_LIKE_TERMS: 'COLLECT_LIKE_TERMS', - - // MULTIPLYING CONSTANT POWERS - // e.g. 10^2 * 10^3 -> 10^(2+3) - COLLECT_CONSTANT_EXPONENTS: 'COLLECT_CONSTANT_EXPONENTS', - - // ADDING POLYNOMIALS - - // e.g. 2x + x -> 2x + 1x - ADD_COEFFICIENT_OF_ONE: 'ADD_COEFFICIENT_OF_ONE', - // e.g. x^2 + x^2 -> 2x^2 - ADD_POLYNOMIAL_TERMS: 'ADD_POLYNOMIAL_TERMS', - // e.g. 2x^2 + 3x^2 + 5x^2 -> (2+3+5)x^2 - GROUP_COEFFICIENTS: 'GROUP_COEFFICIENTS', - // e.g. -x + 2x => -1*x + 2x - UNARY_MINUS_TO_NEGATIVE_ONE: 'UNARY_MINUS_TO_NEGATIVE_ONE', - - // MULTIPLYING POLYNOMIALS - - // e.g. x^2 * x -> x^2 * x^1 - ADD_EXPONENT_OF_ONE: 'ADD_EXPONENT_OF_ONE', - // e.g. x^2 * x^3 * x^1 -> x^(2 + 3 + 1) - COLLECT_POLYNOMIAL_EXPONENTS: 'COLLECT_POLYNOMIAL_EXPONENTS', - // e.g. 2x * 3x -> (2 * 3)(x * x) - MULTIPLY_COEFFICIENTS: 'MULTIPLY_COEFFICIENTS', - // e.g. 2x * x -> 2x ^ 2 - MULTIPLY_POLYNOMIAL_TERMS: 'MULTIPLY_POLYNOMIAL_TERMS', - - // FRACTIONS - - // e.g. (x + 2)/2 -> x/2 + 2/2 - BREAK_UP_FRACTION: 'BREAK_UP_FRACTION', - // e.g. -2/-3 => 2/3 - CANCEL_MINUSES: 'CANCEL_MINUSES', - // e.g. 2x/2 -> x - CANCEL_TERMS: 'CANCEL_TERMS', - // e.g. 2/6 -> 1/3 - SIMPLIFY_FRACTION: 'SIMPLIFY_FRACTION', - // e.g. 2/-3 -> -2/3 - SIMPLIFY_SIGNS: 'SIMPLIFY_SIGNS', - // e.g. 15/6 -> (5*3)/(2*3) - FIND_GCD: 'FIND_GCD', - // e.g. (5*3)/(2*3) -> 5/2 - CANCEL_GCD: 'CANCEL_GCD', - // e.g. 1 2/3 -> 5/3 - CONVERT_MIXED_NUMBER_TO_IMPROPER_FRACTION: 'CONVERT_MIXED_NUMBER_TO_IMPROPER_FRACTION', - // e.g. 1 2/3 -> ((1 * 3) + 2) / 3 - IMPROPER_FRACTION_NUMERATOR: 'IMPROPER_FRACTION_NUMERATOR', - - // ADDING FRACTIONS - - // e.g. 1/2 + 1/3 -> 5/6 - ADD_FRACTIONS: 'ADD_FRACTIONS', - // e.g. (1 + 2)/3 -> 3/3 - ADD_NUMERATORS: 'ADD_NUMERATORS', - // e.g. (2+1)/5 - COMBINE_NUMERATORS: 'COMBINE_NUMERATORS', - // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) - COMMON_DENOMINATOR: 'COMMON_DENOMINATOR', - // e.g. 3 + 1/2 -> 6/2 + 1/2 (for addition) - CONVERT_INTEGER_TO_FRACTION: 'CONVERT_INTEGER_TO_FRACTION', - // e.g. 1.2 + 1/2 -> 1.2 + 0.5 - DIVIDE_FRACTION_FOR_ADDITION: 'DIVIDE_FRACTION_FOR_ADDITION', - // e.g. (2*2)/(6*2) + (1*3)/(4*3) -> (2*2)/12 + (1*3)/12 - MULTIPLY_DENOMINATORS: 'MULTIPLY_DENOMINATORS', - // e.g. (2*2)/12 + (1*3)/12 -> 4/12 + 3/12 - MULTIPLY_NUMERATORS: 'MULTIPLY_NUMERATORS', - - // MULTIPLYING FRACTIONS - - // e.g. 1/2 * 2/3 -> 2/6 - MULTIPLY_FRACTIONS: 'MULTIPLY_FRACTIONS', - - // DIVISION - - // e.g. 2/3/4 -> 2/(3*4) - SIMPLIFY_DIVISION: 'SIMPLIFY_DIVISION', - // e.g. x/(2/3) -> x * 3/2 - MULTIPLY_BY_INVERSE: 'MULTIPLY_BY_INVERSE', - - // DISTRIBUTION - - // e.g. 2(x + y) -> 2x + 2y - DISTRIBUTE: 'DISTRIBUTE', - // e.g. -(2 + x) -> -2 - x - DISTRIBUTE_NEGATIVE_ONE: 'DISTRIBUTE_NEGATIVE_ONE', - // e.g. 2 * 4x + 2*5 --> 8x + 10 (as part of distribution) - SIMPLIFY_TERMS: 'SIMPLIFY_TERMS', - // e.g. (nthRoot(x, 2))^2 -> nthRoot(x, 2) * nthRoot(x, 2) - // e.g. (2x + 3)^2 -> (2x + 3) (2x + 3) - EXPAND_EXPONENT: 'EXPAND_EXPONENT', - - // ABSOLUTE - // e.g. |-3| -> 3 - ABSOLUTE_VALUE: 'ABSOLUTE_VALUE', - - // ROOTS - // e.g. nthRoot(x ^ 2, 4) -> nthRoot(x, 2) - CANCEL_EXPONENT: 'CANCEL_EXPONENT', - // e.g. nthRoot(x ^ 2, 2) -> x - CANCEL_EXPONENT_AND_ROOT: 'CANCEL_EXPONENT_AND_ROOT', - // e.g. nthRoot(x ^ 4, 2) -> x ^ 2 - CANCEL_ROOT: 'CANCEL_ROOT', - // e.g. nthRoot(2, 2) * nthRoot(3, 2) -> nthRoot(2 * 3, 2) - COMBINE_UNDER_ROOT: 'COMBINE_UNDER_ROOT', - // e.g. 2 * 2 * 2 -> 2 ^ 3 - CONVERT_MULTIPLICATION_TO_EXPONENT: 'CONVERT_MULTIPLICATION_TO_EXPONENT', - // e.g. nthRoot(2 * x) -> nthRoot(2) * nthRoot(x) - DISTRIBUTE_NTH_ROOT: 'DISTRIBUTE_NTH_ROOT', - // e.g. nthRoot(4) * nthRoot(x^2) -> 2 * x - EVALUATE_DISTRIBUTED_NTH_ROOT: 'EVALUATE_DISTRIBUTED_NTH_ROOT', - // e.g. 12 -> 2 * 2 * 3 - FACTOR_INTO_PRIMES: 'FACTOR_INTO_PRIMES', - // e.g. nthRoot(2 * 2 * 2, 2) -> nthRoot((2 * 2) * 2) - GROUP_TERMS_BY_ROOT: 'GROUP_TERMS_BY_ROOT', - // e.g. nthRoot(4) -> 2 - NTH_ROOT_VALUE: 'NTH_ROOT_VALUE', - // e.g. nthRoot(4) + nthRoot(4) = 2*nthRoot(4) - ADD_NTH_ROOTS: 'ADD_NTH_ROOTS', - // e.g. nthRoot(x, 2) * nthRoot(x, 2) -> nthRoot(x^2, 2) - MULTIPLY_NTH_ROOTS: 'MULTIPLY_NTH_ROOTS', - - // SOLVING FOR A VARIABLE - - // e.g. x - 3 = 2 -> x - 3 + 3 = 2 + 3 - ADD_TO_BOTH_SIDES: 'ADD_TO_BOTH_SIDES', - // e.g. 2x = 1 -> (2x)/2 = 1/2 - DIVIDE_FROM_BOTH_SIDES: 'DIVIDE_FROM_BOTH_SIDES', - // e.g. (2/3)x = 1 -> (2/3)x * (3/2) = 1 * (3/2) - MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION: 'MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION', - // e.g. -x = 2 -> -1 * -x = -1 * 2 - MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE: 'MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE', - // e.g. x/2 = 1 -> (x/2) * 2 = 1 * 2 - MULTIPLY_TO_BOTH_SIDES: 'MULTIPLY_TO_BOTH_SIDES', - // e.g. x + 2 - 1 = 3 -> x + 1 = 3 - SIMPLIFY_LEFT_SIDE: 'SIMPLIFY_LEFT_SIDE', - // e.g. x = 3 - 1 -> x = 2 - SIMPLIFY_RIGHT_SIDE: 'SIMPLIFY_RIGHT_SIDE', - // e.g. x + 3 = 2 -> x + 3 - 3 = 2 - 3 - SUBTRACT_FROM_BOTH_SIDES: 'SUBTRACT_FROM_BOTH_SIDES', - // e.g. 2 = x -> x = 2 - SWAP_SIDES: 'SWAP_SIDES', - // e.g. (x - 2) (x + 2) = 0 => x = [-2, 2] - FIND_ROOTS: 'FIND_ROOTS', - - // CONSTANT EQUATION - - // e.g. 2 = 2 - STATEMENT_IS_TRUE: 'STATEMENT_IS_TRUE', - // e.g. 2 = 3 - STATEMENT_IS_FALSE: 'STATEMENT_IS_FALSE', - - // FACTORING - - // e.g. x^2 - 4x -> x(x - 4) - FACTOR_SYMBOL: 'FACTOR_SYMBOL', - // e.g. x^2 - 4 -> (x - 2)(x + 2) - FACTOR_DIFFERENCE_OF_SQUARES: 'FACTOR_DIFFERENCE_OF_SQUARES', - // e.g. x^2 + 2x + 1 -> (x + 1)^2 - FACTOR_PERFECT_SQUARE: 'FACTOR_PERFECT_SQUARE', - // e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) - FACTOR_SUM_PRODUCT_RULE: 'FACTOR_SUM_PRODUCT_RULE', - // e.g. 2x^2 + 4x + 2 -> 2x^2 + 2x + 2x + 2 - BREAK_UP_TERM: 'BREAK_UP_TERM', -}; diff --git a/lib/Negative.js b/lib/Negative.js deleted file mode 100644 index 0079a519..00000000 --- a/lib/Negative.js +++ /dev/null @@ -1,96 +0,0 @@ -const NodeCreator = require('./node/Creator'); -const NodeType = require('./node/Type'); -const PolynomialTerm = require('./node/PolynomialTerm'); - -const Negative = {}; - -// Returns if the given node is negative. Treats a unary minus as a negative, -// as well as a negative constant value or a constant fraction that would -// evaluate to a negative number -Negative.isNegative = function(node) { - if (NodeType.isUnaryMinus(node)) { - return !Negative.isNegative(node.args[0]); - } - else if (NodeType.isConstant(node)) { - return parseFloat(node.value) < 0; - } - else if (NodeType.isConstantFraction(node)) { - const numeratorValue = parseFloat(node.args[0].value); - const denominatorValue = parseFloat(node.args[1].value); - if (numeratorValue < 0 || denominatorValue < 0) { - return !(numeratorValue < 0 && denominatorValue < 0); - } - } - else if (PolynomialTerm.isPolynomialTerm(node)) { - const polyNode = new PolynomialTerm(node); - return Negative.isNegative(polyNode.getCoeffNode(true)); - } - - return false; -}; - -// Given a node, returns the negated node -// If naive is true, then we just add an extra unary minus to the expression -// otherwise, we do the actual negation -// E.g. -// not naive: -3 -> 3, x -> -x -// naive: -3 -> --3, x -> -x -Negative.negate = function(node, naive=false) { - if (NodeType.isConstantFraction(node)) { - node.args[0] = Negative.negate(node.args[0], naive); - return node; - } - else if (PolynomialTerm.isPolynomialTerm(node)) { - return Negative.negatePolynomialTerm(node, naive); - } - else if (!naive) { - if (NodeType.isUnaryMinus(node)) { - return node.args[0]; - } - else if (NodeType.isConstant(node)) { - return NodeCreator.constant(0 - parseFloat(node.value)); - } - } - return NodeCreator.unaryMinus(node); -}; - -// Multiplies a polynomial term by -1 and returns the new node -// If naive is true, then we just add an extra unary minus to the expression -// otherwise, we do the actual negation -// E.g. -// not naive: -3x -> 3x, x -> -x -// naive: -3x -> --3x, x -> -x -Negative.negatePolynomialTerm = function(node, naive=false) { - if (!PolynomialTerm.isPolynomialTerm(node)) { - throw Error('node is not a polynomial term'); - } - const polyNode = new PolynomialTerm(node); - - let newCoeff; - if (!polyNode.hasCoeff()) { - newCoeff = NodeCreator.constant(-1); - } - else { - const oldCoeff = polyNode.getCoeffNode(); - if (oldCoeff.value === '-1') { - newCoeff = null; - } - else if (polyNode.hasFractionCoeff()) { - let numerator = oldCoeff.args[0]; - numerator = Negative.negate(numerator, naive); - - const denominator = oldCoeff.args[1]; - newCoeff = NodeCreator.operator('/', [numerator, denominator]); - } - else { - newCoeff = Negative.negate(oldCoeff, naive); - if (newCoeff.value === '1') { - newCoeff = null; - } - } - } - return NodeCreator.polynomialTerm( - polyNode.getSymbolNode(), polyNode.getExponentNode(), newCoeff); -}; - -module.exports = Negative; diff --git a/lib/Symbols.js b/lib/Symbols.js deleted file mode 100644 index 1129e5be..00000000 --- a/lib/Symbols.js +++ /dev/null @@ -1,133 +0,0 @@ -const Node = require('./node'); - -const Symbols = {}; - -// returns the set of all the symbols in an equation -Symbols.getSymbolsInEquation = function(equation) { - const leftSymbols = Symbols.getSymbolsInExpression(equation.leftNode); - const rightSymbols = Symbols.getSymbolsInExpression(equation.rightNode); - const symbols = new Set([...leftSymbols, ...rightSymbols]); - return symbols; -}; - -// return the set of symbols in the expression tree -Symbols.getSymbolsInExpression = function(expression) { - const symbolNodes = expression.filter(node => node.isSymbolNode); // all the symbol nodes - const symbols = symbolNodes.map(node => node.name); // all the symbol nodes' names - const symbolSet = new Set(symbols); // to get rid of duplicates - return symbolSet; -}; - -// Iterates through a node and returns the last term with the symbol name -// Returns null if no terms with the symbol name are in the node. -// e.g. 4x^2 + 2x + y + 2 with `symbolName=x` would return 2x -Symbols.getLastSymbolTerm = function(node, symbolName) { - // First check if the node itself is a polyomial term with symbolName - if (isSymbolTerm(node, symbolName)) { - return node; - } - // If it's a sum of terms, look through the operands for a term - // with `symbolName` - else if (Node.Type.isOperator(node, '+')) { - for (let i = node.args.length - 1; i >= 0 ; i--) { - const child = node.args[i]; - if (Node.Type.isOperator(child, '+')) { - return Symbols.getLastSymbolTerm(child, symbolName); - } - else if (isSymbolTerm(child, symbolName)) { - return child; - } - } - } - else if (Node.Type.isParenthesis(node)) { - return Symbols.getLastSymbolTerm(node.content, symbolName); - } - - return null; -}; - -// Iterates through a node and returns the last term that does not have the -// symbolName including other polynomial terms, and constants or constant -// fractions -// e.g. 4x^2 with `symbolName=x` would return 4 -// e.g. 4x^2 + 2x + 2/4 with `symbolName=x` would return 2/4 -// e.g. 4x^2 + 2x + y with `symbolName=x` would return y -Symbols.getLastNonSymbolTerm = function(node, symbolName) { - if (isPolynomialTermWithSymbol(node, symbolName)) { - return new Node.PolynomialTerm(node).getCoeffNode(); - } - else if (hasDenominatorSymbol(node, symbolName)) { - return null; - } - else if (Node.Type.isOperator(node)) { - for (let i = node.args.length - 1; i >= 0 ; i--) { - const child = node.args[i]; - if (Node.Type.isOperator(child, '+')) { - return Symbols.getLastNonSymbolTerm(child, symbolName); - } - else if (!isSymbolTerm(child, symbolName)) { - return child; - } - } - } - - return null; -}; - -// Iterates through a node and returns the denominator if it has a -// symbolName in its denominator -// e.g. 1/(2x) with `symbolName=x` would return 2x -// e.g. 1/(x+2) with `symbolName=x` would return x+2 -// e.g. 1/(x+2) + (x+1)/(2x+3) with `symbolName=x` would return (2x+3) -Symbols.getLastDenominatorWithSymbolTerm = function(node, symbolName) { - // First check if the node itself has a symbol in the denominator - if (hasDenominatorSymbol(node, symbolName)) { - return node.args[1]; - } - // Otherwise, it's a sum of terms. e.g. 1/x + 1(2+x) - // Look through the operands for a - // denominator term with `symbolName` - else if (Node.Type.isOperator(node, '+')) { - for (let i = node.args.length - 1; i >= 0 ; i--) { - const child = node.args[i]; - if (Node.Type.isOperator(child, '+')) { - return Symbols.getLastDenominatorWithSymbolTerm(child, symbolName); - } - else if (hasDenominatorSymbol(child, symbolName)) { - return child.args[1]; - } - } - } - return null; -}; - -// Returns if `node` is a term with symbol `symbolName` -function isSymbolTerm(node, symbolName) { - return isPolynomialTermWithSymbol(node, symbolName) || - hasDenominatorSymbol(node, symbolName); -} - -function isPolynomialTermWithSymbol(node, symbolName) { - if (Node.PolynomialTerm.isPolynomialTerm(node)) { - const polyTerm = new Node.PolynomialTerm(node); - if (polyTerm.getSymbolName() === symbolName) { - return true; - } - } - - return false; -} - -// Return if `node` has a symbol in its denominator -// e.g. true for 1/(2x) -// e.g. false for 5x -function hasDenominatorSymbol(node, symbolName) { - if (Node.Type.isOperator(node) && node.op === '/') { - const allSymbols = Symbols.getSymbolsInExpression(node.args[1]); - return allSymbols.has(symbolName); - } - - return false; -} - -module.exports = Symbols; diff --git a/lib/TreeSearch.js b/lib/TreeSearch.js deleted file mode 100644 index f7854f5e..00000000 --- a/lib/TreeSearch.js +++ /dev/null @@ -1,69 +0,0 @@ -const Node = require('./node'); - -const TreeSearch = {}; - -// Returns a function that performs a preorder search on the tree for the given -// simplification function -TreeSearch.preOrder = function(simplificationFunction) { - return function (node) { - return search(simplificationFunction, node, true); - }; -}; - -// Returns a function that performs a postorder search on the tree for the given -// simplification function -TreeSearch.postOrder = function(simplificationFunction) { - return function (node) { - return search(simplificationFunction, node, false); - }; -}; - -// A helper function for performing a tree search with a function -function search(simplificationFunction, node, preOrder) { - let status; - - if (preOrder) { - status = simplificationFunction(node); - if (status.hasChanged()) { - return status; - } - } - - if (Node.Type.isConstant(node) || Node.Type.isSymbol(node)) { - return Node.Status.noChange(node); - } - else if (Node.Type.isUnaryMinus(node)) { - status = search(simplificationFunction, node.args[0], preOrder); - if (status.hasChanged()) { - return Node.Status.childChanged(node, status); - } - } - else if (Node.Type.isOperator(node) || Node.Type.isFunction(node)) { - for (let i = 0; i < node.args.length; i++) { - const child = node.args[i]; - const childNodeStatus = search(simplificationFunction, child, preOrder); - if (childNodeStatus.hasChanged()) { - return Node.Status.childChanged(node, childNodeStatus, i); - } - } - } - else if (Node.Type.isParenthesis(node)) { - status = search(simplificationFunction, node.content, preOrder); - if (status.hasChanged()) { - return Node.Status.childChanged(node, status); - } - } - else { - throw Error('Unsupported node type: ' + node); - } - - if (!preOrder) { - return simplificationFunction(node); - } - else { - return Node.Status.noChange(node); - } -} - - -module.exports = TreeSearch; diff --git a/lib/checks/canAddLikeTerms.js b/lib/checks/canAddLikeTerms.js deleted file mode 100644 index b275a3bd..00000000 --- a/lib/checks/canAddLikeTerms.js +++ /dev/null @@ -1,48 +0,0 @@ -const Node = require('../node'); - -// Returns true if the nodes are terms that can be added together. -// The nodes need to have the same base and exponent -// e.g. 2x + 5x, 6x^2 + x^2, nthRoot(4,2) + nthRoot(4,2) -function canAddLikeTermNodes(node, termSubclass) { - if (!Node.Type.isOperator(node, '+')) { - return false; - } - const args = node.args; - if (!args.every(n => Node.Term.isTerm(n, termSubclass.baseNodeFunc))) { - return false; - } - if (args.length === 1) { - return false; - } - - const termList = args.map(n => new termSubclass(n)); - - // to add terms, they must have the same base *and* exponent - const firstTerm = termList[0]; - const sharedBase = firstTerm.getBaseNode(); - const sharedExponentNode = firstTerm.getExponentNode(true); - - const restTerms = termList.slice(1); - return restTerms.every(term => { - const haveSameBase = sharedBase.equals(term.getBaseNode()); - const exponentNode = term.getExponentNode(true); - const haveSameExponent = exponentNode.equals(sharedExponentNode); - return haveSameBase && haveSameExponent; - }); -} - -// Returns true if the nodes are nth roots that can be added together -function canAddLikeTermNthRootNodes(node) { - return canAddLikeTermNodes(node, Node.NthRootTerm); -} - -// Returns true if the nodes are polynomial terms that can be added together. -function canAddLikeTermPolynomialNodes(node) { - return canAddLikeTermNodes(node, Node.PolynomialTerm); -} - -module.exports = { - canAddLikeTermNodes, - canAddLikeTermNthRootNodes, - canAddLikeTermPolynomialNodes, -}; diff --git a/lib/checks/canFindRoots.js b/lib/checks/canFindRoots.js deleted file mode 100644 index 9c335132..00000000 --- a/lib/checks/canFindRoots.js +++ /dev/null @@ -1,35 +0,0 @@ -const Node = require('../node'); -const resolvesToConstant = require('./resolvesToConstant.js'); -/* - Return true if the equation is of the form factor * factor = 0 or factor^power = 0 - // e.g (x - 2)^2 = 0, x(x + 2)(x - 2) = 0 -*/ -function canFindRoots(equation) { - const left = equation.leftNode; - const right = equation.rightNode; - - const zeroRightSide = Node.Type.isConstant(right) - && parseFloat(right.value) === 0; - - const isMulOrPower = Node.Type.isOperator(left, '*') || Node.Type.isOperator(left, '^'); - - if (!(zeroRightSide && isMulOrPower)) { - return false; - } - - // If the left side of the equation is multiplication, filter out all the factors - // that do evaluate to constants because they do not have roots. If the - // resulting array is empty, there is no roots to be found. Do a similiar check - // for when the left side is a power node. - // e.g 2^7 and (33 + 89) do not have solutions when set equal to 0 - - if (Node.Type.isOperator(left, '*')) { - const factors = left.args.filter(arg => !resolvesToConstant(arg)); - return factors.length >= 1; - } - else if (Node.Type.isOperator(left, '^')) { - return !resolvesToConstant(left); - } -} - -module.exports = canFindRoots; diff --git a/lib/checks/canMultiplyLikeTermConstantNodes.js b/lib/checks/canMultiplyLikeTermConstantNodes.js deleted file mode 100644 index 01045344..00000000 --- a/lib/checks/canMultiplyLikeTermConstantNodes.js +++ /dev/null @@ -1,29 +0,0 @@ -const ConstantOrPowerTerm = require('../simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower'); -const Node = require('../node'); - -// Returns true if node is a multiplication of constant power nodes -// where you can combine their exponents, e.g. 10^2 * 10^4 * 10 can become 10^7. -// The node can either be on form c^n or c, as long as c is the same for all. -function canMultiplyLikeTermConstantNodes(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return false; - } - const args = node.args; - if (!args.every(n => ConstantOrPowerTerm.isConstantOrConstantPower(n))) { - return false; - } - - // if none of the terms have exponents, return false here, - // else e.g. 6*6 will become 6^1 * 6^1 => 6^2 - if (args.every(arg => !Node.Type.isOperator(arg, '^'))) { - return false; - } - - const constantTermBaseList = args.map(n => ConstantOrPowerTerm.getBaseNode(n)); - const firstTerm = constantTermBaseList[0]; - const restTerms = constantTermBaseList.slice(1); - // they're considered like terms if they have the same base value - return restTerms.every(term => firstTerm.value === term.value); -} - -module.exports = canMultiplyLikeTermConstantNodes; diff --git a/lib/checks/canMultiplyLikeTermPolynomialNodes.js b/lib/checks/canMultiplyLikeTermPolynomialNodes.js deleted file mode 100644 index 0654ae82..00000000 --- a/lib/checks/canMultiplyLikeTermPolynomialNodes.js +++ /dev/null @@ -1,28 +0,0 @@ -const Node = require('../node'); - -// Returns true if the nodes are symbolic terms with the same symbol and no -// coefficients. -function canMultiplyLikeTermPolynomialNodes(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return false; - } - const args = node.args; - if (!args.every(n => Node.PolynomialTerm.isPolynomialTerm(n))) { - return false; - } - if (args.length === 1) { - return false; - } - - const polynomialTermList = node.args.map(n => new Node.PolynomialTerm(n)); - if (!polynomialTermList.every(polyTerm => !polyTerm.hasCoeff())) { - return false; - } - - const firstTerm = polynomialTermList[0]; - const restTerms = polynomialTermList.slice(1); - // they're considered like terms if they have the same symbol name - return restTerms.every(term => firstTerm.getSymbolName() === term.getSymbolName()); -} - -module.exports = canMultiplyLikeTermPolynomialNodes; diff --git a/lib/checks/canMultiplyLikeTermsNthRoots.js b/lib/checks/canMultiplyLikeTermsNthRoots.js deleted file mode 100644 index 416b0346..00000000 --- a/lib/checks/canMultiplyLikeTermsNthRoots.js +++ /dev/null @@ -1,24 +0,0 @@ -const Node = require('../node'); -const NthRoot = require('../simplifyExpression/functionsSearch/nthRoot'); - -// Function to check if nthRoot nodes can be multiplied -// e.g. nthRoot(x, 2) * nthRoot(x, 2) -> true -// e.g. nthRoot(x, 2) * nthRoot(x, 3) -> false -function canMultiplyLikeTermsNthRoots(node) { - // checks if node is a multiplication of nthRoot nodes - // all the terms has to have the same root node to be multiplied - - if (!Node.Type.isOperator(node, '*') - || !(node.args.every(term => Node.Type.isFunction(term, 'nthRoot')))){ - return false; - } - - // Take arbitrary root node - const firstTerm = node.args[0]; - const rootNode = NthRoot.getRootNode(firstTerm); - - return node.args.every( - term => NthRoot.getRootNode(term).equals(rootNode)); -} - -module.exports = canMultiplyLikeTermsNthRoots; diff --git a/lib/checks/canRearrangeCoefficient.js b/lib/checks/canRearrangeCoefficient.js deleted file mode 100644 index 36979a4f..00000000 --- a/lib/checks/canRearrangeCoefficient.js +++ /dev/null @@ -1,25 +0,0 @@ -const Node = require('../node'); - -// Returns true if the expression is a multiplication between a constant -// and polynomial without a coefficient. -function canRearrangeCoefficient(node) { - // implicit multiplication doesn't count as multiplication here, since it - // represents a single term. - if (node.op !== '*' || node.implicit) { - return false; - } - if (node.args.length !== 2) { - return false; - } - if (!Node.Type.isConstantOrConstantFraction(node.args[1])) { - return false; - } - if (!Node.PolynomialTerm.isPolynomialTerm(node.args[0])) { - return false; - } - - const polyNode = new Node.PolynomialTerm(node.args[0]); - return !polyNode.hasCoeff(); -} - -module.exports = canRearrangeCoefficient; diff --git a/lib/checks/canSimplifyPolynomialTerms.js b/lib/checks/canSimplifyPolynomialTerms.js deleted file mode 100644 index 9205a6a6..00000000 --- a/lib/checks/canSimplifyPolynomialTerms.js +++ /dev/null @@ -1,13 +0,0 @@ -const canAddLikeTerms = require('./canAddLikeTerms'); -const canMultiplyLikeTermPolynomialNodes = require('./canMultiplyLikeTermPolynomialNodes'); -const canRearrangeCoefficient = require('./canRearrangeCoefficient'); - -// Returns true if the node is an operation node with parameters that are -// polynomial terms that can be combined in some way. -function canSimplifyPolynomialTerms(node) { - return (canAddLikeTerms.canAddLikeTermPolynomialNodes(node) || - canMultiplyLikeTermPolynomialNodes(node) || - canRearrangeCoefficient(node)); -} - -module.exports = canSimplifyPolynomialTerms; diff --git a/lib/checks/hasUnsupportedNodes.js b/lib/checks/hasUnsupportedNodes.js deleted file mode 100644 index fb229ab4..00000000 --- a/lib/checks/hasUnsupportedNodes.js +++ /dev/null @@ -1,34 +0,0 @@ -const Node = require('../node'); -const resolvesToConstant = require('./resolvesToConstant'); - -function hasUnsupportedNodes(node) { - if (Node.Type.isParenthesis(node)) { - return hasUnsupportedNodes(node.content); - } - else if (Node.Type.isUnaryMinus(node)) { - return hasUnsupportedNodes(node.args[0]); - } - else if (Node.Type.isOperator(node)) { - return node.args.some(hasUnsupportedNodes); - } - else if (Node.Type.isSymbol(node) || Node.Type.isConstant(node)) { - return false; - } - else if (Node.Type.isFunction(node, 'abs')) { - if (node.args.length !== 1) { - return true; - } - if (node.args.some(hasUnsupportedNodes)) { - return true; - } - return !resolvesToConstant(node.args[0]); - } - else if (Node.Type.isFunction(node, 'nthRoot')) { - return node.args.some(hasUnsupportedNodes) || node.args.length < 1; - } - else { - return true; - } -} - -module.exports = hasUnsupportedNodes; diff --git a/lib/checks/index.js b/lib/checks/index.js deleted file mode 100644 index 23c5753a..00000000 --- a/lib/checks/index.js +++ /dev/null @@ -1,23 +0,0 @@ -const canAddLikeTerms = require('./canAddLikeTerms'); -const canFindRoots = require('./canFindRoots'); -const canMultiplyLikeTermConstantNodes = require('./canMultiplyLikeTermConstantNodes'); -const canMultiplyLikeTermPolynomialNodes = require('./canMultiplyLikeTermPolynomialNodes'); -const canMultiplyLikeTermsNthRoots = require('./canMultiplyLikeTermsNthRoots'); -const canRearrangeCoefficient = require('./canRearrangeCoefficient'); -const canSimplifyPolynomialTerms = require('./canSimplifyPolynomialTerms'); -const hasUnsupportedNodes = require('./hasUnsupportedNodes'); -const isQuadratic = require('./isQuadratic'); -const resolvesToConstant = require('./resolvesToConstant'); - -module.exports = { - canFindRoots, - canAddLikeTerms, - canMultiplyLikeTermConstantNodes, - canMultiplyLikeTermPolynomialNodes, - canMultiplyLikeTermsNthRoots, - canRearrangeCoefficient, - canSimplifyPolynomialTerms, - hasUnsupportedNodes, - isQuadratic, - resolvesToConstant, -}; diff --git a/lib/checks/isQuadratic.js b/lib/checks/isQuadratic.js deleted file mode 100644 index f23ec7a7..00000000 --- a/lib/checks/isQuadratic.js +++ /dev/null @@ -1,54 +0,0 @@ -const Node = require('../node'); -const Symbols = require('../Symbols'); - -// Given a node, will determine if the expression is in the form of a quadratic -// e.g. `x^2 + 2x + 1` OR `x^2 - 1` but not `x^3 + x^2 + x + 1` -function isQuadratic(node) { - if (!Node.Type.isOperator(node, '+')) { - return false; - } - - if (node.args.length > 3) { - return false; - } - - // make sure only one symbol appears in the expression - const symbolSet = Symbols.getSymbolsInExpression(node); - if (symbolSet.size !== 1) { - return false; - } - - const secondDegreeTerms = node.args.filter(isPolynomialTermOfDegree(2)); - const firstDegreeTerms = node.args.filter(isPolynomialTermOfDegree(1)); - const constantTerms = node.args.filter(Node.Type.isConstant); - - // Check that there is one second degree term and at most one first degree - // term and at most one constant term - if (secondDegreeTerms.length !== 1 || firstDegreeTerms.length > 1 || - constantTerms.length > 1) { - return false; - } - - // check that there are no terms that don't fall into these groups - if ((secondDegreeTerms.length + firstDegreeTerms.length + - constantTerms.length) !== node.args.length) { - return false; - } - - return true; -} - -// Given a degree, returns a function that checks if a node -// is a polynomial term of the given degree. -function isPolynomialTermOfDegree(degree) { - return function(node) { - if (Node.PolynomialTerm.isPolynomialTerm(node)) { - const polyTerm = new Node.PolynomialTerm(node); - const exponent = polyTerm.getExponentNode(true); - return exponent && parseFloat(exponent.value) === degree; - } - return false; - }; -} - -module.exports = isQuadratic; diff --git a/lib/checks/resolvesToConstant.js b/lib/checks/resolvesToConstant.js deleted file mode 100644 index c70829c9..00000000 --- a/lib/checks/resolvesToConstant.js +++ /dev/null @@ -1,28 +0,0 @@ -const Node = require('../node'); - -// Returns true if the node is a constant or can eventually be resolved to -// a constant. -// e.g. 2, 2+4, (2+4)^2 would all return true. x + 4 would return false -function resolvesToConstant(node) { - if (Node.Type.isOperator(node) || Node.Type.isFunction(node)) { - return node.args.every( - (child) => resolvesToConstant(child)); - } - else if (Node.Type.isParenthesis(node)) { - return resolvesToConstant(node.content); - } - else if (Node.Type.isConstant(node, true)) { - return true; - } - else if (Node.Type.isSymbol(node)) { - return false; - } - else if (Node.Type.isUnaryMinus(node)) { - return resolvesToConstant(node.args[0]); - } - else { - throw Error('Unsupported node type: ' + node.type); - } -} - -module.exports = resolvesToConstant; diff --git a/lib/equation/Equation.js b/lib/equation/Equation.js deleted file mode 100644 index f100d2be..00000000 --- a/lib/equation/Equation.js +++ /dev/null @@ -1,53 +0,0 @@ -const math = require('mathjs'); - -const printNode = require('../util/print'); - -// This represents an equation, made up of the leftNode (LHS), the -// rightNode (RHS) and a comparator (=, <, >, <=, or >=) -class Equation { - constructor(leftNode, rightNode, comparator) { - this.leftNode = leftNode; - this.rightNode = rightNode; - this.comparator = comparator; - } - - // Prints an Equation properly using the print module - ascii(showPlusMinus=false) { - const leftSide = printNode.ascii(this.leftNode, showPlusMinus); - const rightSide = printNode.ascii(this.rightNode, showPlusMinus); - const comparator = this.comparator; - - return `${leftSide} ${comparator} ${rightSide}`; - } - - // Prints an Equation properly using LaTeX - latex(showPlusMinus=false) { - const leftSide = printNode.latex(this.leftNode, showPlusMinus); - const rightSide = printNode.latex(this.rightNode, showPlusMinus); - const comparator = this.comparator; - - return `${leftSide} ${comparator} ${rightSide}`; - } - - clone() { - const newLeft = this.leftNode.cloneDeep(); - const newRight = this.rightNode.cloneDeep(); - return new Equation(newLeft, newRight, this.comparator); - } -} - -// Splits a string on the given comparator and returns a new Equation object -// from the left and right hand sides -Equation.createEquationFromString = function(str, comparator) { - const sides = str.split(comparator); - if (sides.length !== 2) { - throw Error('Expected two sides of an equation using comparator: ' + - comparator); - } - const leftNode = math.parse(sides[0]); - const rightNode = math.parse(sides[1]); - - return new Equation(leftNode, rightNode, comparator); -}; - -module.exports = Equation; diff --git a/lib/equation/Status.js b/lib/equation/Status.js deleted file mode 100644 index a8754442..00000000 --- a/lib/equation/Status.js +++ /dev/null @@ -1,73 +0,0 @@ -const ChangeTypes = require('../ChangeTypes'); -const Equation = require('./Equation'); -const Node = require('../node'); - -// This represents the current equation we're solving. -// As we move step by step, an equation might be updated. Functions return this -// status object to pass on the updated equation and information on if/how it was -// changed. -class Status { - constructor(changeType, oldEquation, newEquation, substeps=[]) { - if (!newEquation) { - throw Error('new equation isn\'t defined'); - } - if (changeType === undefined || typeof(changeType) !== 'string') { - throw Error('changetype isn\'t valid'); - } - - this.changeType = changeType; - this.oldEquation = oldEquation; - this.newEquation = newEquation; - this.substeps = substeps; - } - - hasChanged() { - return this.changeType !== ChangeTypes.NO_CHANGE; - } -} - -// A wrapper around the Status constructor for the case where equation -// hasn't been changed. -Status.noChange = function(equation) { - return new Status(ChangeTypes.NO_CHANGE, null, equation); -}; - -Status.addLeftStep = function(equation, leftStep) { - const substeps = []; - leftStep.substeps.forEach(substep => { - substeps.push(Status.addLeftStep(equation, substep)); - }); - let oldEquation = null; - if (leftStep.oldNode) { - oldEquation = equation.clone(); - oldEquation.leftNode = leftStep.oldNode; - } - const newEquation = equation.clone(); - newEquation.leftNode = leftStep.newNode; - return new Status( - leftStep.changeType, oldEquation, newEquation, substeps); -}; - -Status.addRightStep = function(equation, rightStep) { - const substeps = []; - rightStep.substeps.forEach(substep => { - substeps.push(Status.addRightStep(equation, substep)); - }); - let oldEquation = null; - if (rightStep.oldNode) { - oldEquation = equation.clone(); - oldEquation.rightNode = rightStep.oldNode; - } - const newEquation = equation.clone(); - newEquation.rightNode = rightStep.newNode; - return new Status( - rightStep.changeType, oldEquation, newEquation, substeps); -}; - -Status.resetChangeGroups = function(equation) { - const leftNode = Node.Status.resetChangeGroups(equation.leftNode); - const rightNode = Node.Status.resetChangeGroups(equation.rightNode); - return new Equation(leftNode, rightNode, equation.comparator); -}; - -module.exports = Status; diff --git a/lib/equation/index.js b/lib/equation/index.js deleted file mode 100644 index 0f18d847..00000000 --- a/lib/equation/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const Equation = require('./Equation'); -const Status = require('./Status'); - -module.exports = { - Equation, - Status, -}; diff --git a/lib/factor/ConstantFactors.js b/lib/factor/ConstantFactors.js deleted file mode 100644 index ff72f287..00000000 --- a/lib/factor/ConstantFactors.js +++ /dev/null @@ -1,58 +0,0 @@ -// This module deals with getting constant factors, including prime factors -// and factor pairs of a number - -const ConstantFactors = {}; - -// Given a number, will return all the prime factors of that number as a list -// sorted from smallest to largest -ConstantFactors.getPrimeFactors = function(number){ - let factors = []; - if (number < 0) { - factors = [-1]; - factors = factors.concat(ConstantFactors.getPrimeFactors(-1 * number)); - return factors; - } - - const root = Math.sqrt(number); - let candidate = 2; - if (number % 2) { - candidate = 3; // assign first odd - while (number % candidate && candidate <= root) { - candidate = candidate + 2; - } - } - - // if no factor found then the number is prime - if (candidate > root) { - factors.push(number); - } - // if we find a factor, make a recursive call on the quotient of the number and - // our newly found prime factor in order to find more factors - else { - factors.push(candidate); - factors = factors.concat(ConstantFactors.getPrimeFactors(number/candidate)); - } - - return factors; -}; - -// Given a number, will return all the factor pairs for that number as a list -// of 2-item lists -ConstantFactors.getFactorPairs = function(number){ - const factors = []; - - const bound = Math.floor(Math.sqrt(Math.abs(number))); - for (var divisor = -bound; divisor <= bound; divisor++) { - if (divisor === 0) { - continue; - } - if (number % divisor === 0) { - const quotient = number / divisor; - factors.push([divisor, quotient]); - } - } - - return factors; -}; - -module.exports = ConstantFactors; diff --git a/lib/factor/factorQuadratic.js b/lib/factor/factorQuadratic.js deleted file mode 100644 index e276784a..00000000 --- a/lib/factor/factorQuadratic.js +++ /dev/null @@ -1,361 +0,0 @@ -const math = require('mathjs'); - -const ConstantFactors = require('./ConstantFactors'); - -const ChangeTypes = require('../ChangeTypes'); -const evaluate = require('../util/evaluate'); -const Negative = require('../Negative'); -const Node = require('../node'); - -const FACTOR_FUNCTIONS = [ - // factor just the symbol e.g. x^2 + 2x -> x(x + 2) - factorSymbol, - // factor difference of squares e.g. x^2 - 4 - factorDifferenceOfSquares, - // factor perfect square e.g. x^2 + 2x + 1 - factorPerfectSquare, - // factor sum product rule e.g. x^2 + 3x + 2 - factorSumProductRule -]; - -// Given a node, will check if it's in the form of a quadratic equation -// `ax^2 + bx + c`, and -// if it is, will factor it using one of the following rules: -// - Factor out the symbol e.g. x^2 + 2x -> x(x + 2) -// - Difference of squares e.g. x^2 - 4 -> (x+2)(x-2) -// - Perfect square e.g. x^2 + 2x + 1 -> (x+1)^2 -// - Sum/product rule e.g. x^2 + 3x + 2 -> (x+1)(x+2) -// - TODO: quadratic formula -// requires us simplify the following only within the parens: -// a(x - (-b + sqrt(b^2 - 4ac)) / 2a)(x - (-b - sqrt(b^2 - 4ac)) / 2a) -function factorQuadratic(node) { - // get a, b and c - let symbol, aValue = 0, bValue = 0, cValue = 0; - for (const term of node.args) { - if (Node.Type.isConstant(term)) { - cValue = evaluate(term); - } - else if (Node.PolynomialTerm.isPolynomialTerm(term)) { - const polyTerm = new Node.PolynomialTerm(term); - const exponent = polyTerm.getExponentNode(true); - if (exponent.value === '2') { - symbol = polyTerm.getSymbolNode(); - aValue = polyTerm.getCoeffValue(); - } - else if (exponent.value === '1') { - bValue = polyTerm.getCoeffValue(); - } - else { - return Node.Status.noChange(node); - } - } - else { - return Node.Status.noChange(node); - } - } - - if (!symbol || !aValue) { - return Node.Status.noChange(node); - } - - let negate = false; - if (aValue < 0) { - negate = true; - aValue = -aValue; - bValue = -bValue; - cValue = -cValue; - } - - for (let i = 0; i < FACTOR_FUNCTIONS.length; i++) { - const nodeStatus = FACTOR_FUNCTIONS[i](node, symbol, aValue, bValue, cValue, negate); - if (nodeStatus.hasChanged()) { - return nodeStatus; - } - } - - return Node.Status.noChange(node); -} - -// Will factor the node if it's in the form of ax^2 + bx -function factorSymbol(node, symbol, aValue, bValue, cValue, negate) { - if (!bValue || cValue) { - return Node.Status.noChange(node); - } - - const gcd = math.gcd(aValue, bValue); - const gcdNode = Node.Creator.constant(gcd); - const aNode = Node.Creator.constant(aValue/gcd); - const bNode = Node.Creator.constant(bValue/gcd); - - const factoredNode = Node.Creator.polynomialTerm(symbol, null, gcdNode); - const polyTerm = Node.Creator.polynomialTerm(symbol, null, aNode); - const paren = Node.Creator.parenthesis( - Node.Creator.operator('+', [polyTerm, bNode])); - - let newNode = Node.Creator.operator('*', [factoredNode, paren], true); - if (negate) { - newNode = Negative.negate(newNode); - } - - return Node.Status.nodeChanged(ChangeTypes.FACTOR_SYMBOL, node, newNode); -} - -// Will factor the node if it's in the form of ax^2 - c, and the aValue -// and cValue are perfect squares -// e.g. 4x^2 - 4 -> (2x + 2)(2x - 2) -function factorDifferenceOfSquares(node, symbol, aValue, bValue, cValue, negate) { - // check if difference of squares: - // (i) abs(a) and abs(c) are squares, - // (ii) b = 0, - // (iii) c is negative - if (bValue || !cValue) { - return Node.Status.noChange(node); - } - - // we factor out the gcd first, providing us with a modified expression to - // factor with new a and c values - const gcd = math.gcd(aValue, cValue); - aValue = aValue/gcd; - cValue = cValue/gcd; - const aRootValue = Math.sqrt(Math.abs(aValue)); - const cRootValue = Math.sqrt(Math.abs(cValue)); - - // must be a difference of squares - if (Number.isInteger(aRootValue) && - Number.isInteger(cRootValue) && - cValue < 0) { - - const aRootNode = Node.Creator.constant(aRootValue); - const cRootNode = Node.Creator.constant(cRootValue); - - const polyTerm = Node.Creator.polynomialTerm(symbol, null, aRootNode); - const firstParen = Node.Creator.parenthesis( - Node.Creator.operator('+', [polyTerm, cRootNode])); - const secondParen = Node.Creator.parenthesis( - Node.Creator.operator('-', [polyTerm, cRootNode])); - - // create node in difference of squares form - let newNode = Node.Creator.operator('*', [firstParen, secondParen], true); - if (gcd !== 1) { - const gcdNode = Node.Creator.constant(gcd); - newNode = Node.Creator.operator('*', [gcdNode, newNode], true); - } - if (negate) { - newNode = Negative.negate(newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.FACTOR_DIFFERENCE_OF_SQUARES, node, newNode); - } - - return Node.Status.noChange(node); -} - -// Will factor the node if it's in the form of ax^2 + bx + c, where a and c -// are perfect squares and b = 2*sqrt(a)*sqrt(c) -// e.g. x^2 + 2x + 1 -> (x + 1)^2 -function factorPerfectSquare(node, symbol, aValue, bValue, cValue, negate) { - // check if perfect square: (i) a and c squares, (ii) b = 2*sqrt(a)*sqrt(c) - if (!bValue || !cValue) { - return Node.Status.noChange(node); - } - - // we factor out the gcd first, providing us with a modified expression to - // factor with new a and c values - const gcd = math.gcd(aValue, bValue, cValue); - aValue = aValue/gcd; - cValue = cValue/gcd; - const aRootValue = Math.sqrt(Math.abs(aValue)); - let cRootValue = Math.sqrt(Math.abs(cValue)); - - // if the second term is negative, then the constant in the parens is - // subtracted: e.g. x^2 - 2x + 1 -> (x - 1)^2 - if (bValue < 0) { - cRootValue = cRootValue * -1; - } - - // apply the perfect square test - const perfectProduct = 2 * aRootValue * cRootValue; - if (Number.isInteger(aRootValue) && - Number.isInteger(cRootValue) && - (bValue/gcd) === perfectProduct) { - const aRootNode = Node.Creator.constant(aRootValue); - const cRootNode = Node.Creator.constant(cRootValue); - - const polyTerm = Node.Creator.polynomialTerm(symbol, null, aRootNode); - const paren = Node.Creator.parenthesis( - Node.Creator.operator('+', [polyTerm, cRootNode])); - const exponent = Node.Creator.constant(2); - - // create node in perfect square form - let newNode = Node.Creator.operator('^', [paren, exponent]); - if (gcd !== 1) { - const gcdNode = Node.Creator.constant(gcd); - newNode = Node.Creator.operator('*', [gcdNode, newNode], true); - } - if (negate) { - newNode = Negative.negate(newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.FACTOR_PERFECT_SQUARE, node, newNode); - } - - return Node.Status.noChange(node); -} - -// Will factor the node if it's in the form of ax^2 + bx + c, by -// applying the sum product rule: finding factors of a*c that add up to b. -// e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) or -// or 2x^2 + 5x + 3 -> (2x - 1)(x + 3) -function factorSumProductRule(node, symbol, aValue, bValue, cValue, negate) { - let newNode; - - if (bValue && cValue) { - // we factor out the gcd first, providing us with a modified expression to - // factor with new a, b and c values - const gcd = math.gcd(aValue, bValue, cValue); - const gcdNode = Node.Creator.constant(gcd); - - aValue = aValue/gcd; - bValue = bValue/gcd; - cValue = cValue/gcd; - - // try sum/product rule: find a factor pair of a*c that adds up to b - const product = aValue * cValue; - const factorPairs = ConstantFactors.getFactorPairs(product, true); - for (const pair of factorPairs) { - if (pair[0] + pair[1] === bValue) { - // To factor, we go through some transformations - // 1. Break apart the middle term into two terms using our factor pair - // (p and q): e.g. ax^2 + bx + c -> ax^2 + px + qx + c - // 2. Consider the first two terms together and the second two terms - // together (this doesn't require any actual change to the expression) - // e.g. first group: [ax^2 + px] and second group: [qx + c] - // 3. Factor both groups separately - // e.g first group: [ux(rx + s)] and second group [v(rx + s)] - // 4. Finish factoring by combining the factored terms through grouping: - // e.g. (ux + v)(rx + s) - const substeps = []; - let status; - - const a = Node.Creator.constant(aValue); - const b = Node.Creator.constant(bValue); - const c = Node.Creator.constant(cValue); - const ax2 = Node.Creator.polynomialTerm(symbol, Node.Creator.constant(2), a); - const bx = Node.Creator.polynomialTerm(symbol, null, b); - - // OPTIONAL SUBSTEP (this happens iff a is negative) - // ax^2 + bx + c -> -(-ax^2 - bx - c) - if (negate) { - newNode = Node.Creator.operator('+', [ax2, bx, c], true); - newNode = Negative.negate(newNode); - status = Node.Status.nodeChanged( - ChangeTypes.REARRANGE_COEFF, node, newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // SUBSTEP 1: ax^2 + bx + c -> ax^2 + px + qx + c - const pValue = pair[0]; - const qValue = pair[1]; - const p = Node.Creator.constant(pValue); - const q = Node.Creator.constant(qValue); - const px = Node.Creator.polynomialTerm(symbol, null, p); - const qx = Node.Creator.polynomialTerm(symbol, null, q); - - newNode = Node.Creator.operator('+', [ax2, px, qx, c], true); - if (negate) { - newNode = Negative.negate(newNode); - } - status = Node.Status.nodeChanged( - ChangeTypes.BREAK_UP_TERM, node, newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 2: ax^2 + px + qx + c -> (ax^2 + px) + (qx + c) - const firstTerm = Node.Creator.parenthesis( - Node.Creator.operator('+', [ax2, px])); - const secondTerm = Node.Creator.parenthesis( - Node.Creator.operator('+', [qx, c])); - - newNode = Node.Creator.operator('+', [firstTerm, secondTerm], true); - if (negate) { - newNode = Negative.negate(newNode); - } - status = Node.Status.nodeChanged( - ChangeTypes.COLLECT_LIKE_TERMS, node, newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // SUBSTEP 3A: (ax^2 + px) + (qx + c) -> ux(rx + s) + (qx + c) - const u = Node.Creator.constant(math.gcd(aValue, pValue)); - const r = Node.Creator.constant(aValue/u); - const s = Node.Creator.constant(pValue/u); - const ux = Node.Creator.polynomialTerm(symbol, null, u); - - // create the first group's part that's in parentheses: (rx + s) - const rx = Node.Creator.polynomialTerm(symbol, null, r); - const firstParen = Node.Creator.parenthesis( - Node.Creator.operator('+', [rx, s])); - - const firstFactoredGroup = Node.Creator.operator('*', [ux, firstParen], true); - newNode = Node.Creator.operator('+', [firstFactoredGroup, secondTerm], true); - if (negate) { - newNode = Negative.negate(newNode); - } - status = Node.Status.nodeChanged( - ChangeTypes.FACTOR_SYMBOL, node, newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 3B: ux(rx + s) + (qx + c) -> ux(rx + s) + v(rx + s) - let vValue = math.gcd(cValue, qValue); - if (qValue < 0) { - vValue = vValue * -1; - } - const v = Node.Creator.constant(vValue); - - // create the second parenthesis - const secondParen = Node.Creator.parenthesis( - Node.Creator.operator('+', [ux, v])); - - const secondFactoredGroup = Node.Creator.operator('*', [v, firstParen], true); - newNode = Node.Creator.operator('+', [firstFactoredGroup, secondFactoredGroup], true); - if (negate) { - newNode = Negative.negate(newNode); - } - status = Node.Status.nodeChanged( - ChangeTypes.FACTOR_SYMBOL, node, newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 4: ux(rx + s) + v(rx + s) -> (ux + v)(rx + s) - if (gcd === 1) { - newNode = Node.Creator.operator( - '*', [firstParen, secondParen], true); - } - else { - newNode = Node.Creator.operator( - '*', [gcdNode, firstParen, secondParen], true); - } - - if (negate) { - newNode = Negative.negate(newNode); - } - - status = Node.Status.nodeChanged( - ChangeTypes.FACTOR_SUM_PRODUCT_RULE, node, newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - return Node.Status.nodeChanged( - ChangeTypes.FACTOR_SUM_PRODUCT_RULE, node, newNode, true, substeps); - } - } - } - - return Node.Status.noChange(node); -} - -module.exports = factorQuadratic; diff --git a/lib/factor/index.js b/lib/factor/index.js deleted file mode 100644 index eb3a41e7..00000000 --- a/lib/factor/index.js +++ /dev/null @@ -1,19 +0,0 @@ -const math = require('mathjs'); -const stepThrough = require('./stepThrough'); - -function factorString(expressionString, debug=false) { - let node; - try { - node = math.parse(expressionString); - } - catch (err) { - return []; - } - - if (node) { - return stepThrough(node, debug); - } - return []; -} - -module.exports = factorString; diff --git a/lib/factor/stepThrough.js b/lib/factor/stepThrough.js deleted file mode 100644 index 87ea30c8..00000000 --- a/lib/factor/stepThrough.js +++ /dev/null @@ -1,37 +0,0 @@ -const checks = require('../checks'); - -const factorQuadratic = require('./factorQuadratic'); - -const flattenOperands = require('../util/flattenOperands'); -const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); - -// Given a mathjs expression node, steps through factoring the expression. -// Currently only supports factoring quadratics. -// Returns a list of details about each step. -function stepThrough(node, debug=false) { - if (debug) { - // eslint-disable-next-line - console.log('\n\nFactoring: ' + print.ascii(node, false, true)); - } - - if (checks.hasUnsupportedNodes(node)) { - return []; - } - - let nodeStatus; - const steps = []; - - node = flattenOperands(node); - node = removeUnnecessaryParens(node, true); - if (checks.isQuadratic(node)) { - nodeStatus = factorQuadratic(node); - if (nodeStatus.hasChanged()) { - steps.push(nodeStatus); - } - } - // Add factoring higher order polynomials... - - return steps; -} - -module.exports = stepThrough; diff --git a/lib/node/Creator.js b/lib/node/Creator.js deleted file mode 100644 index 19f07d23..00000000 --- a/lib/node/Creator.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - Functions to generate any mathJS node supported by the stepper - see http://mathjs.org/docs/expressions/expression_trees.html#nodes for more - information on nodes in mathJS -*/ - -const math = require('mathjs'); -const NodeType = require('./Type'); - -const NodeCreator = { - operator (op, args, implicit=false) { - switch (op) { - case '+': - return new math.expression.node.OperatorNode('+', 'add', args); - case '-': - return new math.expression.node.OperatorNode('-', 'subtract', args); - case '/': - return new math.expression.node.OperatorNode('/', 'divide', args); - case '*': - return new math.expression.node.OperatorNode( - '*', 'multiply', args, implicit); - case '^': - return new math.expression.node.OperatorNode('^', 'pow', args); - default: - throw Error('Unsupported operation: ' + op); - } - }, - - // In almost all cases, use Negative.negate (with naive = true) to add a - // unary minus to your node, rather than calling this constructor directly - unaryMinus (content) { - return new math.expression.node.OperatorNode( - '-', 'unaryMinus', [content]); - }, - - constant (val) { - return new math.expression.node.ConstantNode(val); - }, - - symbol (name) { - return new math.expression.node.SymbolNode(name); - }, - - parenthesis (content) { - return new math.expression.node.ParenthesisNode(content); - }, - - list (content) { - return new math.expression.node.ArrayNode(content); - }, - - // exponent might be null, which means there's no exponent node. - // similarly, coefficient might be null, which means there's no coefficient - // the base node can never be null. - term (base, exponent, coeff, explicitCoeff=false) { - let term = base; - if (exponent) { - term = this.operator('^', [term, exponent]); - } - if (coeff && (explicitCoeff || parseFloat(coeff.value) !== 1)) { - if (NodeType.isConstant(coeff) && - parseFloat(coeff.value) === -1 && - !explicitCoeff) { - // if you actually want -1 as the coefficient, set explicitCoeff to true - term = this.unaryMinus(term); - } - else { - term = this.operator('*', [coeff, term], true); - } - } - return term; - }, - - polynomialTerm (symbol, exponent, coeff, explicitCoeff=false) { - return this.term(symbol, exponent, coeff, explicitCoeff); - }, - - // Given a root value and a radicand (what is under the radical) - nthRoot (radicandNode, rootNode) { - const symbol = NodeCreator.symbol('nthRoot'); - return new math.expression.node.FunctionNode(symbol, [radicandNode, rootNode]); - } -}; - -module.exports = NodeCreator; diff --git a/lib/node/CustomType.js b/lib/node/CustomType.js deleted file mode 100644 index e264883b..00000000 --- a/lib/node/CustomType.js +++ /dev/null @@ -1,78 +0,0 @@ -const Negative = require('../Negative'); -const NodeCreator = require('./Creator'); -const NodeType = require('./Type'); - -const NodeCustomType = {}; - -// Returns true if `node` belongs to the type specified by boolean `isTypeFunc`. -// If `allowUnaryMinus/allowParens` is true, we allow for the node to be nested. -NodeCustomType.isType = function(node, isTypeFunc, allowUnaryMinus=true, allowParens=true) { - if (isTypeFunc(node)) { - return true; - } - else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { - return NodeCustomType.isType(node.args[0], isTypeFunc, allowUnaryMinus, allowParens); - } - else if (allowParens && NodeType.isParenthesis(node)) { - return NodeCustomType.isType(node.content, isTypeFunc, allowUnaryMinus, allowParens); - } - - return false; -}; - -// Returns `node` if `node` belongs to the type specified by boolean `isTypeFunc`. -// If `allowUnaryMinus/allowParens` is true, we check for an inner node of this type. -// `moveUnaryMinus` should be defined if `allowUnaryMinus` is true, and should -// move the unaryMinus into the inside of the type -// e.g. for fractions, this function will negate the numerator -NodeCustomType.getType = function( - node, isTypeFunc, allowUnaryMinus=true, allowParens=true, moveUnaryMinus=undefined) { - if (allowUnaryMinus === true && moveUnaryMinus === undefined) { - throw Error('Error in `getType`: moveUnaryMinus is undefined'); - } - - if (isTypeFunc(node)) { - return node; - } - else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { - return moveUnaryMinus( - NodeCustomType.getType( - node.args[0], isTypeFunc, allowUnaryMinus, allowParens, moveUnaryMinus)); - } - else if (allowParens && NodeType.isParenthesis(node)) { - return NodeCustomType.getType( - node.content, isTypeFunc, allowUnaryMinus, allowParens, moveUnaryMinus); - } - - throw Error('`getType` called on a node that does not belong to specified type'); -}; - -NodeCustomType.isFraction = function(node, allowUnaryMinus=true, allowParens=true) { - return NodeCustomType.isType( - node, - (node) => NodeType.isOperator(node, '/'), - allowUnaryMinus, - allowParens); -}; - -NodeCustomType.getFraction = function(node, allowUnaryMinus=true, allowParens=true) { - const moveUnaryMinus = function(node) { - if (!(NodeType.isOperator(node, '/'))) { - throw Error('Expected a fraction'); - } - - const numerator = node.args[0]; - const denominator = node.args[1]; - const newNumerator = Negative.negate(numerator); - return NodeCreator.operator('/', [newNumerator, denominator]); - }; - - return NodeCustomType.getType( - node, - (node) => NodeType.isOperator(node, '/'), - allowParens, - allowUnaryMinus, - moveUnaryMinus); -}; - -module.exports = NodeCustomType; diff --git a/lib/node/MixedNumber.js b/lib/node/MixedNumber.js deleted file mode 100644 index e6edb786..00000000 --- a/lib/node/MixedNumber.js +++ /dev/null @@ -1,117 +0,0 @@ -const Negative = require('../Negative'); -const NodeType = require('./Type'); - -// Returns true if `node` is a mixed number -// e.g. 2 1/2, 19 2/3 -// Right now mathjs cannot parse the above examples; -// instead it expects the input to look like e.g. 2(1)/(2), -// which is division with implicit multiplication in the numerator -// TODO: Add better support for mixed numbers in the future -function isMixedNumber(node) { - if (!NodeType.isOperator(node, '/')) { - return false; - } - - if (node.args.length !== 2) { - return false; - } - - const numerator = node.args[0]; - const denominator = node.args[1]; - - // check for implicit multiplication between two constants in the numerator - // first can be wrapped in unary minus - // second one can be optionally wrapped in parenthesis - if (!(NodeType.isOperator(numerator, '*') && numerator.implicit)) { - return false; - } - - const numeratorFirstArg = NodeType.isUnaryMinus(numerator.args[0]) ? - Negative.negate(numerator.args[0].args[0]) - : numerator.args[0]; - - const numeratorSecondArg = NodeType.isParenthesis(numerator.args[1]) ? - numerator.args[1].content - : numerator.args[1]; - - if (!(NodeType.isConstant(numeratorFirstArg) && - NodeType.isConstant(numeratorSecondArg))) { - return false; - } - - // check for a constant in the denominator, - // optionally wrapped in parenthesis - const denominatorValue = NodeType.isParenthesis(denominator) ? - denominator.content - : denominator; - - if (!NodeType.isConstant(denominatorValue)) { - return false; - } - - return true; -} - -// Returns true if the mixed number is negative, -// in which case we have to ignore the negative while converting to an -// improper fraction, and instead we negate the whole thing at the end -// e.g. -1 2/3 !== ((-1 * 3) + 2)/3 = -1/3 -// -1 2/3 == -((1 * 3) + 2)/3 = -5/2 -function isNegativeMixedNumber(node) { - if (!isMixedNumber(node)) { - throw Error('Expected a mixed number'); - } - - return NodeType.isUnaryMinus(node.args[0].args[0]); -} - -// Get the whole number part of a mixed number -// e.g. 1 2/3 -> 1 -// Negatives are ignored; e.g. -1 2/3 -> 1 -function getWholeNumberValue(node) { - if (!isMixedNumber(node)) { - throw Error('Expected a mixed number'); - } - - const wholeNumberNode = NodeType.isUnaryMinus(node.args[0].args[0]) ? - node.args[0].args[0].args[0] - : node.args[0].args[0]; - - return parseInt(wholeNumberNode.value); -} - -// Get the numerator part of a mixed number -// e.g. 1 2/3 -> 2 -function getNumeratorValue(node) { - if (!isMixedNumber(node)) { - throw Error('Expected a mixed number'); - } - - const numeratorNode = NodeType.isParenthesis(node.args[0].args[1]) ? - node.args[0].args[1].content - : node.args[0].args[1]; - - return parseInt(numeratorNode.value); -} - -// Get the denominator part of a mixed number -// e.g. 1 2/3 -> 3 -function getDenominatorValue(node) { - if (!isMixedNumber(node)) { - throw Error('Expected a mixed number'); - } - - const denominatorNode = NodeType.isParenthesis(node.args[1]) ? - node.args[1].content - : node.args[1]; - - return parseInt(denominatorNode.value); -} - -module.exports = { - isMixedNumber, - isNegativeMixedNumber, - getWholeNumberValue, - getNumeratorValue, - getDenominatorValue -}; diff --git a/lib/node/NthRootTerm.js b/lib/node/NthRootTerm.js deleted file mode 100644 index 5e112003..00000000 --- a/lib/node/NthRootTerm.js +++ /dev/null @@ -1,27 +0,0 @@ -const NodeType = require('./Type'); -const Term = require('./Term'); - -// For storing nth root terms, which are a subclass of Term -// where the base node is an nth root -class NthRootTerm extends Term { - constructor(node, onlyImplicitMultiplication=false) { - super(node, NthRootTerm.baseNodeFunc, onlyImplicitMultiplication); - } -} - -// Returns true if the term has a base node that makes it an nth root term -// e.g. 4x^2 has a base of x, so it is not an nth root term -// 4*sqrt(x)^2 has a base of sqrt(x), so it is an nth root term -NthRootTerm.baseNodeFunc = function(node) { - return NodeType.isFunction(node, 'nthRoot'); -}; - -// Returns true if the node represents an nth root term. -// e.g. nthRoot(4), nthRoot(x^2), 4*nthRoot(10)^2 -NthRootTerm.isNthRootTerm = function( - node, onlyImplicitMultiplication=false) { - return Term.isTerm( - node, NthRootTerm.baseNodeFunc, onlyImplicitMultiplication); -}; - -module.exports = NthRootTerm; diff --git a/lib/node/PolynomialTerm.js b/lib/node/PolynomialTerm.js deleted file mode 100644 index 3410dab4..00000000 --- a/lib/node/PolynomialTerm.js +++ /dev/null @@ -1,37 +0,0 @@ -const NodeType = require('./Type'); -const Term = require('./Term'); - -// For storing polynomial terms, which are a subclass of Term -// where the base node is a symbol -class PolynomialTerm extends Term { - constructor(node, onlyImplicitMultiplication=false) { - super(node, PolynomialTerm.baseNodeFunc, onlyImplicitMultiplication); - } - - getSymbolNode() { - return this.base; - } - - getSymbolName() { - return this.base.name; - } -} - -// Returns true if the term has a base node that makes it a polynomial term -// e.g. 4x^2 has a base of x, so it is a polynomial -// 4*sqrt(x)^2 has a base of sqrt(x), so it is not -PolynomialTerm.baseNodeFunc = function(node) { - return NodeType.isSymbol(node); -}; - -// Returns true if the node is a polynomial term. -// e.g. x^2, 2y, z, 3x/5 are all polynomial terms. -// 4, 2+x, 3*7, x-z are all not polynomial terms. -// See the tests for some more thorough examples. -PolynomialTerm.isPolynomialTerm = function( - node, onlyImplicitMultiplication=false) { - return Term.isTerm( - node, PolynomialTerm.baseNodeFunc, onlyImplicitMultiplication); -}; - -module.exports = PolynomialTerm; diff --git a/lib/node/Status.js b/lib/node/Status.js deleted file mode 100644 index cbc026a8..00000000 --- a/lib/node/Status.js +++ /dev/null @@ -1,124 +0,0 @@ -const ChangeTypes = require('../ChangeTypes'); -const Type = require('./Type'); - -// This represents the current (sub)expression we're simplifying. -// As we move step by step, a node might be updated. Functions return this -// status object to pass on the updated node and information on if/how it was -// changed. -// Status(node) creates a Status object that signals no change -class Status { - constructor(changeType, oldNode, newNode, substeps=[]) { - if (!newNode) { - throw Error('node is not defined'); - } - if (changeType === undefined || typeof(changeType) !== 'string') { - throw Error('changetype isn\'t valid'); - } - - this.changeType = changeType; - this.oldNode = oldNode; - this.newNode = newNode; - this.substeps = substeps; - } - - hasChanged() { - return this.changeType !== ChangeTypes.NO_CHANGE; - } -} - -Status.resetChangeGroups = function(node) { - node = node.cloneDeep(); - node.filter(node => node.changeGroup).forEach(change => { - delete change.changeGroup; - }); - return node; -}; - -// A wrapper around the Status constructor for the case where node hasn't -// been changed. -Status.noChange = function(node) { - return new Status(ChangeTypes.NO_CHANGE, null, node); -}; - -// A wrapper around the Status constructor for the case of a change -// that is happening at the level of oldNode + newNode -// e.g. 2 + 2 --> 4 (an addition node becomes a constant node) -Status.nodeChanged = function( - changeType, oldNode, newNode, defaultChangeGroup=true, steps=[]) { - if (defaultChangeGroup) { - oldNode.changeGroup = 1; - newNode.changeGroup = 1; - } - - return new Status(changeType, oldNode, newNode, steps); -}; - -// A wrapper around the Status constructor for the case where there was -// a change that happened deeper `node`'s tree, and `node`'s children must be -// updated to have the newNode/oldNode metadata (changeGroups) -// e.g. (2 + 2) + x --> 4 + x has to update the left argument -Status.childChanged = function(node, childStatus, childArgIndex=null) { - const oldNode = node.cloneDeep(); - const newNode = node.cloneDeep(); - let substeps = childStatus.substeps; - - if (!childStatus.oldNode) { - throw Error ('Expected old node for changeType: ' + childStatus.changeType); - } - - function updateSubsteps(substeps, fn) { - substeps.map((step) => { - step = fn(step); - step.substeps = updateSubsteps(step.substeps, fn); - }); - return substeps; - } - - if (Type.isParenthesis(node)) { - oldNode.content = childStatus.oldNode; - newNode.content = childStatus.newNode; - substeps = updateSubsteps(substeps, (step) => { - const oldNode = node.cloneDeep(); - const newNode = node.cloneDeep(); - oldNode.content = step.oldNode; - newNode.content = step.newNode; - step.oldNode = oldNode; - step.newNode = newNode; - return step; - }); - } - else if ((Type.isOperator(node) || Type.isFunction(node) && - childArgIndex !== null)) { - oldNode.args[childArgIndex] = childStatus.oldNode; - newNode.args[childArgIndex] = childStatus.newNode; - substeps = updateSubsteps(substeps, (step) => { - const oldNode = node.cloneDeep(); - const newNode = node.cloneDeep(); - oldNode.args[childArgIndex] = step.oldNode; - newNode.args[childArgIndex] = step.newNode; - step.oldNode = oldNode; - step.newNode = newNode; - return step; - }); - } - else if (Type.isUnaryMinus(node)) { - oldNode.args[0] = childStatus.oldNode; - newNode.args[0] = childStatus.newNode; - substeps = updateSubsteps(substeps, (step) => { - const oldNode = node.cloneDeep(); - const newNode = node.cloneDeep(); - oldNode.args[0] = step.oldNode; - newNode.args[0] = step.newNode; - step.oldNode = oldNode; - step.newNode = newNode; - return step; - }); - } - else { - throw Error('Unexpected node type: ' + node.type); - } - - return new Status(childStatus.changeType, oldNode, newNode, substeps); -}; - -module.exports = Status; diff --git a/lib/node/Term.js b/lib/node/Term.js deleted file mode 100644 index 34bb04ae..00000000 --- a/lib/node/Term.js +++ /dev/null @@ -1,202 +0,0 @@ -const NodeCreator = require('./Creator'); -const NodeType = require('./Type'); - -const evaluate = require('../util/evaluate'); - -// For storing term that have a base node, maybe an exponent, -// and maybe a coefficient. -// These expressions are Terms: -// -- x^2, 2y, z, 3x/5 (PolynomialTerm) -// -- nthRoot(4), 5*nthRoot(x) (NthRootTerm) -// These expressions are not: 4, x^(3+4), 2+x, 3*7, x-z -/* Fields: - - coeff: either a constant node or a fraction of two constant nodes - (might be null if no coefficient) - - base: the base node (e.g. in x^2, the node x) - - exponent: a node that can take any form, e.g. x^(2+x^2) - (might be null if no exponent) -*/ -class Term { - // Params: - // -- node: The node from which to construct the Term - // -- baseNodeFunc: A boolean function returning true if the base node - // is of the right type - // e.g., for PolynomialTerms, baseNodeFunc checks if the base is a symbol - // for NthRootTerms, baseNodeFunc checks if the base node is an nth root - // -- onlyImplicitMultiplication: If onlyImplicitMultiplication is true, - // we throw an error if `node` is a term without implicit multiplication - // (i.e. 2*x instead of 2x) and therefore isTerm will return false. - constructor(node, baseNodeFunc, onlyImplicitMultiplication=false) { - const values = Term.parseNode(node, baseNodeFunc, onlyImplicitMultiplication); - this.base = values.base; - this.exponent = values.exponent; - this.coeff = values.coeff; - } - /* GETTER FUNCTIONS */ - getBaseNode() { - return this.base; - } - - getCoeffNode(defaultOne=false) { - if (!this.coeff && defaultOne) { - return NodeCreator.constant(1); - } - else { - return this.coeff; - } - } - - getCoeffValue() { - if (this.coeff) { - return evaluate(this.coeff); - } - else { - return 1; // no coefficient is like a coeff of 1 - } - } - - getExponentNode(defaultOne=false) { - if (!this.exponent && defaultOne) { - return NodeCreator.constant(1); - } - else { - return this.exponent; - } - } - - // note: there is no exponent value getter function because the exponent - // can be any expression and not necessarily a number. - - /* CHECKER FUNCTIONS (returns true / false for certain conditions) */ - - // Returns true if the coefficient is a fraction - hasFractionCoeff() { - // coeffNode is either a constant or a division operation. - return this.coeff && NodeType.isOperator(this.coeff); - } - - hasCoeff() { - return !!this.coeff; - } -} - -// Returns if the node represents an expression that can be considered -// a term with a coefficient. -Term.isTerm = function( - node, baseNodeFunc, onlyImplicitMultiplication=false) { - try { - // will throw error if node isn't term with coefficient - new Term(node, baseNodeFunc, onlyImplicitMultiplication); - return true; - } - catch (err) { - return false; - } -}; - -Term.parseNode = function(node, baseNodeFunc, onlyImplicitMultiplication) { - let base, exponent, coeff; - if (NodeType.isOperator(node)) { - if (node.op === '^') { - const baseNode = node.args[0]; - if (!baseNodeFunc(baseNode)) { - throw Error('Expected base term, got ' + baseNode); - } - base = baseNode; - exponent = node.args[1]; - } - // it's '*' ie it has a coefficient - else if (node.op === '*') { - if (onlyImplicitMultiplication && !node.implicit) { - throw Error('Expected implicit multiplication'); - } - if (node.args.length !== 2) { - throw Error('Expected two arguments to *'); - } - const coeffNode = node.args[0]; - if (!NodeType.isConstantOrConstantFraction(coeffNode)) { - throw Error('Expected coefficient to be constant or fraction of ' + - 'constants term, got ' + coeffNode); - } - coeff = coeffNode; - const nonCoefficientTerm = new Term( - node.args[1], baseNodeFunc, onlyImplicitMultiplication); - if (nonCoefficientTerm.hasCoeff()) { - throw Error('Cannot have two coefficients ' + coeffNode + - ' and ' + nonCoefficientTerm.getCoeffNode()); - } - base = nonCoefficientTerm.getBaseNode(); - exponent = nonCoefficientTerm.getExponentNode(); - } - // this means there's a fraction coefficient - else if (node.op === '/') { - const denominatorNode = node.args[1]; - if (!NodeType.isConstant(denominatorNode)) { - throw Error('denominator must be constant node, instead of ' + - denominatorNode); - } - const numeratorNode = new Term( - node.args[0], baseNodeFunc, onlyImplicitMultiplication); - if (numeratorNode.hasFractionCoeff()) { - throw Error('Terms with coefficients cannot have nested fractions'); - } - exponent = numeratorNode.getExponentNode(); - base = numeratorNode.getBaseNode(); - const numeratorConstantNode = numeratorNode.getCoeffNode(true); - coeff = NodeCreator.operator( - '/', [numeratorConstantNode, denominatorNode]); - } - else { - throw Error('Unsupported operatation for term with coefficent: ' + node.op); - } - } - else if (NodeType.isUnaryMinus(node)) { - var arg = node.args[0]; - if (NodeType.isParenthesis(arg)) { - arg = arg.content; - } - const termNode = new Term( - arg, baseNodeFunc, onlyImplicitMultiplication); - exponent = termNode.getExponentNode(); - base = termNode.getBaseNode(); - if (!termNode.hasCoeff()) { - coeff = NodeCreator.constant(-1); - } - else { - coeff = negativeCoefficient(termNode.getCoeffNode()); - } - } - else if (baseNodeFunc(node)) { - base = node; - } - else if (NodeType.isParenthesis(node)) { - return Term.parseNode(node.content, baseNodeFunc, onlyImplicitMultiplication); - } - else { - throw Error('Unsupported node type: ' + node.type); - } - - return { - base, - exponent, - coeff, - }; -}; - - -// Multiplies `node`, a constant or fraction of two constant nodes, by -1 -// Returns a node -function negativeCoefficient(node) { - if (NodeType.isConstant(node)) { - // Node is a constant - node = NodeCreator.constant(0 - parseFloat(node.value)); - } - else { - // Node is a constant fraction - const numeratorValue = 0 - parseFloat(node.args[0].value); - node.args[0] = NodeCreator.constant(numeratorValue); - } - return node; -} - -module.exports = Term; diff --git a/lib/node/Type.js b/lib/node/Type.js deleted file mode 100644 index c1d6bd96..00000000 --- a/lib/node/Type.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - For determining the type of a mathJS node. - */ - -const NodeType = {}; - -NodeType.isOperator = function(node, operator=null) { - return node.type === 'OperatorNode' && - node.fn !== 'unaryMinus' && - '*+-/^'.includes(node.op) && - (operator ? node.op === operator : true); -}; - -NodeType.isParenthesis = function(node) { - return node.type === 'ParenthesisNode'; -}; - -NodeType.isUnaryMinus = function(node) { - return node.type === 'OperatorNode' && node.fn === 'unaryMinus'; -}; - -NodeType.isFunction = function(node, functionName=null) { - if (node.type !== 'FunctionNode') { - return false; - } - if (functionName && node.fn.name !== functionName) { - return false; - } - return true; -}; - -NodeType.isSymbol = function(node, allowUnaryMinus=false) { - if (node.type === 'SymbolNode') { - return true; - } - else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { - return NodeType.isSymbol(node.args[0], false); - } - else { - return false; - } -}; - -NodeType.isConstant = function(node, allowUnaryMinus=false) { - if (node.type === 'ConstantNode') { - return true; - } - else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { - if (NodeType.isConstant(node.args[0], false)) { - const value = parseFloat(node.args[0].value); - return value >= 0; - } - else { - return false; - } - } - else { - return false; - } -}; - -NodeType.isConstantFraction = function(node, allowUnaryMinus=false) { - if (NodeType.isOperator(node, '/')) { - return node.args.every(n => NodeType.isConstant(n, allowUnaryMinus)); - } - else { - return false; - } -}; - -NodeType.isConstantOrConstantFraction = function(node, allowUnaryMinus=false) { - if (NodeType.isConstant(node, allowUnaryMinus) || - NodeType.isConstantFraction(node, allowUnaryMinus)) { - return true; - } - else { - return false; - } -}; - -NodeType.isIntegerFraction = function(node, allowUnaryMinus=false) { - if (!NodeType.isConstantFraction(node, allowUnaryMinus)) { - return false; - } - let numerator = node.args[0]; - let denominator = node.args[1]; - if (allowUnaryMinus) { - if (NodeType.isUnaryMinus(numerator)) { - numerator = numerator.args[0]; - } - if (NodeType.isUnaryMinus(denominator)) { - denominator = denominator.args[0]; - } - } - return (Number.isInteger(parseFloat(numerator.value)) && - Number.isInteger(parseFloat(denominator.value))); -}; - -module.exports = NodeType; diff --git a/lib/node/index.js b/lib/node/index.js deleted file mode 100644 index 0804621d..00000000 --- a/lib/node/index.js +++ /dev/null @@ -1,19 +0,0 @@ -const Creator = require('./Creator'); -const CustomType = require('./CustomType'); -const MixedNumber = require('./MixedNumber'); -const NthRootTerm = require('./NthRootTerm'); -const PolynomialTerm = require('./PolynomialTerm'); -const Status = require('./Status'); -const Term = require('./Term'); -const Type = require('./Type'); - -module.exports = { - Creator, - CustomType, - MixedNumber, - NthRootTerm, - PolynomialTerm, - Status, - Term, - Type, -}; diff --git a/lib/package-lock.json b/lib/package-lock.json new file mode 100644 index 00000000..67a18690 --- /dev/null +++ b/lib/package-lock.json @@ -0,0 +1,51 @@ +{ + "name": "@taskbase/mathsteps", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "complex.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.1.tgz", + "integrity": "sha1-6pDHoFrs6vOjdtLA9qeEIXJ9aHk=" + }, + "decimal.js": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-7.1.1.tgz", + "integrity": "sha1-GtytfXDXqRxCbXVvHrZWbDvmy88=" + }, + "fraction.js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.0.tgz", + "integrity": "sha1-c5dOL4tR73CVNtYkzJB4Liu2EnQ=" + }, + "mathjs": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-3.11.2.tgz", + "integrity": "sha1-0spyPX6SVMxY0UIaWmHYz6yP9kI=", + "requires": { + "complex.js": "2.0.1", + "decimal.js": "7.1.1", + "fraction.js": "4.0.0", + "seed-random": "2.2.0", + "tiny-emitter": "1.0.2", + "typed-function": "0.10.5" + } + }, + "seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=" + }, + "tiny-emitter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.0.2.tgz", + "integrity": "sha1-jklHDT9V+J4kchA2imu5+1GqFgE=" + }, + "typed-function": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-0.10.5.tgz", + "integrity": "sha1-Lg8Yq9BlIZ+raUpEamXG0ZgYMsA=" + } + } +} diff --git a/lib/package.json b/lib/package.json new file mode 100644 index 00000000..0c31375d --- /dev/null +++ b/lib/package.json @@ -0,0 +1,32 @@ +{ + "name": "@taskbase/mathsteps", + "version": "1.0.0", + "description": "Step by step math solutions", + "main": "./index.js", + "types": "./index.d.ts", + "engines": { + "node": ">=6.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/taskbase/mathsteps.git" + }, + "keywords": [ + "math", + "steps", + "algebra", + "cas", + "computer", + "algebra", + "system" + ], + "author": "Taskbase", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/taskbase/mathsteps/issues" + }, + "homepage": "https://github.com/taskbase/mathsteps#readme", + "dependencies": { + "mathjs": "3.11.2" + } +} diff --git a/lib/simplifyExpression/arithmeticSearch/index.js b/lib/simplifyExpression/arithmeticSearch/index.js deleted file mode 100644 index 60e35794..00000000 --- a/lib/simplifyExpression/arithmeticSearch/index.js +++ /dev/null @@ -1,61 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -// Searches through the tree, prioritizing deeper nodes, and evaluates -// arithmetic (e.g. 2+2 or 3*5*2) on an operation node if possible. -// Returns a Node.Status object. -const search = TreeSearch.postOrder(arithmetic); - -// evaluates arithmetic (e.g. 2+2 or 3*5*2) on an operation node. -// Returns a Node.Status object. -function arithmetic(node) { - if (!Node.Type.isOperator(node)) { - return Node.Status.noChange(node); - } - if (!node.args.every(child => Node.Type.isConstant(child, true))) { - return Node.Status.noChange(node); - } - - // we want to eval each arg so unary minuses around constant nodes become - // constant nodes with negative values - node.args.forEach((arg, i) => { - node.args[i] = Node.Creator.constant(evaluate(arg)); - }); - - // Only resolve division of integers if we get an integer result. - // Note that a fraction of decimals will be divided out. - if (Node.Type.isIntegerFraction(node)) { - const numeratorValue = parseInt(node.args[0]); - const denominatorValue = parseInt(node.args[1]); - if (numeratorValue % denominatorValue === 0) { - const newNode = Node.Creator.constant(numeratorValue/denominatorValue); - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode); - } - else { - return Node.Status.noChange(node); - } - } - else { - const evaluatedValue = evaluateAndRound(node); - const newNode = Node.Creator.constant(evaluatedValue); - return Node.Status.nodeChanged(ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode); - } -} - -// Evaluates a math expression to a constant, e.g. 3+4 -> 7 and rounds if -// necessary -function evaluateAndRound(node) { - let result = evaluate(node); - if (Math.abs(result) < 1) { - result = parseFloat(result.toPrecision(4)); - } - else { - result = parseFloat(result.toFixed(4)); - } - return result; -} - -module.exports = search; diff --git a/lib/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.js b/lib/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.js deleted file mode 100644 index 84865e7e..00000000 --- a/lib/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.js +++ /dev/null @@ -1,133 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Converts a mixed number to an improper fraction -// e.g. 1 2/3 -> 5/3 -// All comments in the function are based on this example -function convertMixedNumberToImproperFraction(node) { - if (!Node.MixedNumber.isMixedNumber(node)) { - return Node.Status.noChange(node); - } - - const substeps = []; - let newNode = node.cloneDeep(); - - // e.g. 1 2/3 - const wholeNumber = Node.MixedNumber.getWholeNumberValue(node); // 1 - const numerator = Node.MixedNumber.getNumeratorValue(node); // 2 - const denominator = Node.MixedNumber.getDenominatorValue(node); // 3 - const isNegativeMixedNumber = Node.MixedNumber.isNegativeMixedNumber(node); - - // STEP 1: Convert to unsimplified improper fraction - // e.g. 1 2/3 -> ((1 * 3) + 2) / 3 - let status = convertToUnsimplifiedImproperFraction( - newNode, wholeNumber, numerator, denominator, isNegativeMixedNumber); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 2: Simplify multiplication in numerator - // e.g. ((1 * 3) + 2) / 3 -> (3 + 2) / 3 - status = simplifyMultiplicationInImproperFraction( - newNode, wholeNumber, numerator, denominator, isNegativeMixedNumber); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // STEP 3: Simplify addition in numerator - // e.g. (3 + 2) / 3 -> 5/3 - status = simplifyAdditionInImproperFraction( - newNode, wholeNumber, numerator, denominator, isNegativeMixedNumber); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - return Node.Status.nodeChanged( - ChangeTypes.CONVERT_MIXED_NUMBER_TO_IMPROPER_FRACTION, - node, newNode, true, substeps); -} - -// Convert a mixed number to an unsimplified proper fraction -// e.g. 1 2/3 -> ((1 * 3) + 2) / 3 -function convertToUnsimplifiedImproperFraction( - oldNode, wholeNumber, numerator, denominator, isNegativeMixedNumber) { - // (wholeNumber * denominator) - // e.g. (1 * 3) - const newNumeratorMultiplication = Node.Creator.parenthesis( - Node.Creator.operator( - '*', - [Node.Creator.constant(wholeNumber), - Node.Creator.constant(denominator)])); - - // (wholeNumber * denominator) + numerator - // e.g. (1 * 3) + 2 - const newNumerator = Node.Creator.operator( - '+', - [newNumeratorMultiplication, Node.Creator.constant(numerator)]); - oldNode.args[0].args[0].changeGroup = 1; - newNumerator.changeGroup = 1; - - // e.g. 3 - const newDenominator = Node.Creator.constant(denominator); - - let newNode = Node.Creator.operator( - '/', [newNumerator, newDenominator]); - - if (isNegativeMixedNumber) { - newNode = Node.Creator.unaryMinus(newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.IMPROPER_FRACTION_NUMERATOR, oldNode, newNode, false); -} - -// Simplify multiplication in the numerator of an improper fraction -// e.g. ((1 * 3) + 2) / 3 -> (3 + 2) / 3 -function simplifyMultiplicationInImproperFraction( - oldNode, wholeNumber, numerator, denominator, isNegativeMixedNumber) { - // (wholeNumber * denominator) + numerator - // e.g. 3 + 2 - const newNumerator = Node.Creator.operator( - '+', - [Node.Creator.constant(wholeNumber * denominator), - Node.Creator.constant(numerator)]); - oldNode.args[0].changeGroup = 1; - newNumerator.changeGroup = 1; - - // e.g. 3 - const newDenominator = Node.Creator.constant(denominator); - - let newNode = Node.Creator.operator( - '/', [newNumerator, newDenominator]); - - if (isNegativeMixedNumber) { - newNode = Node.Creator.unaryMinus(newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, oldNode, newNode, false); -} - -// Simplify addition in the numerator of an improper fraction -// e.g. (3 + 2) / 3 -> 5/3 -function simplifyAdditionInImproperFraction( - oldNode, wholeNumber, numerator, denominator, isNegativeMixedNumber) { - // (wholeNumber * denominator) + numerator - // e.g. 5 - const newNumerator = Node.Creator.constant( - wholeNumber * denominator + numerator); - oldNode.args[0].changeGroup = 1; - newNumerator.changeGroup = 1; - - // e.g. 3 - const newDenominator = Node.Creator.constant(denominator); - - let newNode = Node.Creator.operator( - '/', [newNumerator, newDenominator]); - - if (isNegativeMixedNumber) { - newNode = Node.Creator.unaryMinus(newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, oldNode, newNode, false); -} - -module.exports = convertMixedNumberToImproperFraction; diff --git a/lib/simplifyExpression/basicsSearch/rearrangeCoefficient.js b/lib/simplifyExpression/basicsSearch/rearrangeCoefficient.js deleted file mode 100644 index 4350cba6..00000000 --- a/lib/simplifyExpression/basicsSearch/rearrangeCoefficient.js +++ /dev/null @@ -1,26 +0,0 @@ -const checks = require('../../checks'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Rearranges something of the form x * 5 to be 5x, ie putting the coefficient -// in the right place. -// Returns a Node.Status object -function rearrangeCoefficient(node) { - if (!checks.canRearrangeCoefficient(node)) { - return Node.Status.noChange(node); - } - - let newNode = node.cloneDeep(); - - const polyNode = new Node.PolynomialTerm(newNode.args[0]); - const constNode = newNode.args[1]; - const exponentNode = polyNode.getExponentNode(); - newNode = Node.Creator.polynomialTerm( - polyNode.getSymbolNode(), exponentNode, constNode); - - return Node.Status.nodeChanged( - ChangeTypes.REARRANGE_COEFF, node, newNode); -} - -module.exports = rearrangeCoefficient; diff --git a/lib/simplifyExpression/basicsSearch/reduceExponentByZero.js b/lib/simplifyExpression/basicsSearch/reduceExponentByZero.js deleted file mode 100644 index c24d43c6..00000000 --- a/lib/simplifyExpression/basicsSearch/reduceExponentByZero.js +++ /dev/null @@ -1,21 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is an exponent of something to 0, we can reduce that to just 1. -// Returns a Node.Status object. -function reduceExponentByZero(node) { - if (node.op !== '^') { - return Node.Status.noChange(node); - } - const exponent = node.args[1]; - if (Node.Type.isConstant(exponent) && exponent.value === '0') { - const newNode = Node.Creator.constant(1); - return Node.Status.nodeChanged( - ChangeTypes.REDUCE_EXPONENT_BY_ZERO, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = reduceExponentByZero; diff --git a/lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero.js b/lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero.js deleted file mode 100644 index b9bc4d57..00000000 --- a/lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero.js +++ /dev/null @@ -1,31 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is a multiplication node with 0 as one of its operands, -// reduce the node to 0. Returns a Node.Status object. -function reduceMultiplicationByZero(node) { - if (node.op !== '*') { - return Node.Status.noChange(node); - } - const zeroIndex = node.args.findIndex(arg => { - if (Node.Type.isConstant(arg) && arg.value === '0') { - return true; - } - if (Node.PolynomialTerm.isPolynomialTerm(arg)) { - const polyTerm = new Node.PolynomialTerm(arg); - return polyTerm.getCoeffValue() === 0; - } - return false; - }); - if (zeroIndex >= 0) { - // reduce to just the 0 node - const newNode = Node.Creator.constant(0); - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_BY_ZERO, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = reduceMultiplicationByZero; diff --git a/lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.js b/lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.js deleted file mode 100644 index b6c4aed3..00000000 --- a/lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.js +++ /dev/null @@ -1,20 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is a fraction with 0 as the numerator, reduce the node to 0. -// Returns a Node.Status object. -function reduceZeroDividedByAnything(node) { - if (node.op !== '/') { - return Node.Status.noChange(node); - } - if (node.args[0].value === '0') { - const newNode = Node.Creator.constant(0); - return Node.Status.nodeChanged( - ChangeTypes.REDUCE_ZERO_NUMERATOR, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = reduceZeroDividedByAnything; diff --git a/lib/simplifyExpression/basicsSearch/removeAdditionOfZero.js b/lib/simplifyExpression/basicsSearch/removeAdditionOfZero.js deleted file mode 100644 index 63e5e48a..00000000 --- a/lib/simplifyExpression/basicsSearch/removeAdditionOfZero.js +++ /dev/null @@ -1,28 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is an addition node with 0 as one of its operands, -// remove 0 from the operands list. Returns a Node.Status object. -function removeAdditionOfZero(node) { - if (node.op !== '+') { - return Node.Status.noChange(node); - } - const zeroIndex = node.args.findIndex(arg => { - return Node.Type.isConstant(arg) && arg.value === '0'; - }); - let newNode = node.cloneDeep(); - if (zeroIndex >= 0) { - // remove the 0 node - newNode.args.splice(zeroIndex, 1); - // if there's only one operand left, there's nothing left to add it to, - // so move it up the tree - if (newNode.args.length === 1) { - newNode = newNode.args[0]; - } - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_ADDING_ZERO, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeAdditionOfZero; diff --git a/lib/simplifyExpression/basicsSearch/removeDivisionByOne.js b/lib/simplifyExpression/basicsSearch/removeDivisionByOne.js deleted file mode 100644 index 3e17dd26..00000000 --- a/lib/simplifyExpression/basicsSearch/removeDivisionByOne.js +++ /dev/null @@ -1,42 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// If `node` is a division operation of something by 1 or -1, we can remove the -// denominator. Returns a Node.Status object. -function removeDivisionByOne(node) { - if (node.op !== '/') { - return Node.Status.noChange(node); - } - const denominator = node.args[1]; - if (!Node.Type.isConstant(denominator)) { - return Node.Status.noChange(node); - } - // It's taken 40ms on average to pass distribution test, - // TODO: see if we should keep using utils/clone here - let numerator = node.args[0].cloneDeep(); - - // if denominator is -1, we make the numerator negative - if (parseFloat(denominator.value) === -1) { - // If the numerator was an operation, wrap it in parens before adding - - // to the front. - // e.g. 2+3 / -1 ---> -(2+3) - if (Node.Type.isOperator(numerator)) { - numerator = Node.Creator.parenthesis(numerator); - } - const changeType = Negative.isNegative(numerator) ? - ChangeTypes.RESOLVE_DOUBLE_MINUS : - ChangeTypes.DIVISION_BY_NEGATIVE_ONE; - numerator = Negative.negate(numerator); - return Node.Status.nodeChanged(changeType, node, numerator); - } - else if (parseFloat(denominator.value) === 1) { - return Node.Status.nodeChanged( - ChangeTypes.DIVISION_BY_ONE, node, numerator); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = removeDivisionByOne; diff --git a/lib/simplifyExpression/basicsSearch/removeExponentBaseOne.js b/lib/simplifyExpression/basicsSearch/removeExponentBaseOne.js deleted file mode 100644 index dd62bf3b..00000000 --- a/lib/simplifyExpression/basicsSearch/removeExponentBaseOne.js +++ /dev/null @@ -1,20 +0,0 @@ -const checks = require('../../checks'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is of the form 1^x, reduces it to a node of the form 1. -// Returns a Node.Status object. -function removeExponentBaseOne(node) { - if (node.op === '^' && // an exponent with - checks.resolvesToConstant(node.args[1]) && // a power not a symbol and - Node.Type.isConstant(node.args[0]) && // a constant base - node.args[0].value === '1') { // of value 1 - const newNode = node.args[0].cloneDeep(); - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_EXPONENT_BASE_ONE, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeExponentBaseOne; diff --git a/lib/simplifyExpression/basicsSearch/removeExponentByOne.js b/lib/simplifyExpression/basicsSearch/removeExponentByOne.js deleted file mode 100644 index ff2d3f09..00000000 --- a/lib/simplifyExpression/basicsSearch/removeExponentByOne.js +++ /dev/null @@ -1,17 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is of the form x^1, reduces it to a node of the form x. -// Returns a Node.Status object. -function removeExponentByOne(node) { - if (node.op === '^' && // exponent of anything - Node.Type.isConstant(node.args[1]) && // to a constant - node.args[1].value === '1') { // of value 1 - const newNode = node.args[0].cloneDeep(); - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_EXPONENT_BY_ONE, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeExponentByOne; diff --git a/lib/simplifyExpression/basicsSearch/removeMultiplicationByOne.js b/lib/simplifyExpression/basicsSearch/removeMultiplicationByOne.js deleted file mode 100644 index 34c31a18..00000000 --- a/lib/simplifyExpression/basicsSearch/removeMultiplicationByOne.js +++ /dev/null @@ -1,28 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If `node` is a multiplication node with 1 as one of its operands, -// remove 1 from the operands list. Returns a Node.Status object. -function removeMultiplicationByOne(node) { - if (node.op !== '*') { - return Node.Status.noChange(node); - } - const oneIndex = node.args.findIndex(arg => { - return Node.Type.isConstant(arg) && arg.value === '1'; - }); - if (oneIndex >= 0) { - let newNode = node.cloneDeep(); - // remove the 1 node - newNode.args.splice(oneIndex, 1); - // if there's only one operand left, there's nothing left to multiply it - // to, so move it up the tree - if (newNode.args.length === 1) { - newNode = newNode.args[0]; - } - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_MULTIPLYING_BY_ONE, node, newNode); - } - return Node.Status.noChange(node); -} - -module.exports = removeMultiplicationByOne; diff --git a/lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.js b/lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.js deleted file mode 100644 index 03e39221..00000000 --- a/lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.js +++ /dev/null @@ -1,36 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// Simplifies two unary minuses in a row by removing both of them. -// e.g. -(- 4) --> 4 -function simplifyDoubleUnaryMinus(node) { - if (!Node.Type.isUnaryMinus(node)) { - return Node.Status.noChange(node); - } - const unaryArg = node.args[0]; - // e.g. in - -x, -x is the unary arg, and we'd want to reduce to just x - if (Node.Type.isUnaryMinus(unaryArg)) { - const newNode = unaryArg.args[0].cloneDeep(); - return Node.Status.nodeChanged( - ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); - } - // e.g. - -4, -4 could be a constant with negative value - else if (Node.Type.isConstant(unaryArg) && parseFloat(unaryArg.value) < 0) { - const newNode = Node.Creator.constant(parseFloat(unaryArg.value) * -1); - return Node.Status.nodeChanged( - ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); - } - // e.g. -(-(5+2)) - else if (Node.Type.isParenthesis(unaryArg)) { - const parenthesisNode = unaryArg; - const parenthesisContent = parenthesisNode.content; - if (Node.Type.isUnaryMinus(parenthesisContent)) { - const newNode = Node.Creator.parenthesis(parenthesisContent.args[0]); - return Node.Status.nodeChanged( - ChangeTypes.RESOLVE_DOUBLE_MINUS, node, newNode); - } - } - return Node.Status.noChange(node); -} - -module.exports = simplifyDoubleUnaryMinus; diff --git a/lib/simplifyExpression/breakUpNumeratorSearch/index.js b/lib/simplifyExpression/breakUpNumeratorSearch/index.js deleted file mode 100644 index 2dcff7a2..00000000 --- a/lib/simplifyExpression/breakUpNumeratorSearch/index.js +++ /dev/null @@ -1,45 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -// Breaks up any fraction (deeper nodes getting priority) that has a numerator -// that is a sum. e.g. (2+x)/5 -> (2/5 + x/5) -// This step must happen after things have been collected and combined, or -// else things will infinite loop, so it's a tree search of its own. -// Returns a Node.Status object -const search = TreeSearch.postOrder(breakUpNumerator); - -// If `node` is a fraction with a numerator that is a sum, breaks up the -// fraction e.g. (2+x)/5 -> (2/5 + x/5) -// Returns a Node.Status object -function breakUpNumerator(node) { - if (!Node.Type.isOperator(node) || node.op !== '/') { - return Node.Status.noChange(node); - } - let numerator = node.args[0]; - if (Node.Type.isParenthesis(numerator)) { - numerator = numerator.content; - } - if (!Node.Type.isOperator(numerator) || numerator.op !== '+') { - return Node.Status.noChange(node); - } - - // At this point, we know that node is a fraction and its numerator is a sum - // of terms that can't be collected or combined, so we should break it up. - const fractionList = []; - const denominator = node.args[1]; - numerator.args.forEach(arg => { - const newFraction = Node.Creator.operator('/', [arg, denominator]); - newFraction.changeGroup = 1; - fractionList.push(newFraction); - }); - - let newNode = Node.Creator.operator('+', fractionList); - // Wrap in parens for cases like 2*(2+3)/5 => 2*(2/5 + 3/5) - newNode = Node.Creator.parenthesis(newNode); - node.changeGroup = 1; - return Node.Status.nodeChanged( - ChangeTypes.BREAK_UP_FRACTION, node, newNode, false); -} - -module.exports = search; diff --git a/lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector.js b/lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector.js deleted file mode 100644 index 1dd4ee09..00000000 --- a/lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector.js +++ /dev/null @@ -1,295 +0,0 @@ -const print = require('../../util/print'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const NthRoot = require('../functionsSearch/nthRoot'); -const Util = require('../../util/Util'); - -const CONSTANT = 'constant'; -const CONSTANT_FRACTION = 'constantFraction'; -const NTH_ROOT = 'nthRoot'; -const OTHER = 'other'; - -const LikeTermCollector = {}; - -// Given an expression tree, returns true if there are terms that can be -// collected -LikeTermCollector.canCollectLikeTerms = function(node) { - // We can collect like terms through + or through * - // Note that we never collect like terms with - or /, those expressions will - // always be manipulated in flattenOperands so that the top level operation is - // + or *. - if (!(Node.Type.isOperator(node, '+') || Node.Type.isOperator(node, '*'))) { - return false; - } - - let terms; - if (node.op === '+') { - terms = getTermsForCollectingAddition(node); - } - else if (node.op === '*') { - terms = getTermsForCollectingMultiplication(node); - } - else { - throw Error('Operation not supported: ' + node.op); - } - - // Conditions we need to meet to decide to to reorganize (collect) the terms: - // - more than 1 term type - // - more than 1 of at least one type (not including other) - // (note that this means x^2 + x + x + 2 -> x^2 + (x + x) + 2, - // which will be recorded as a step, but doesn't change the order of terms) - const termTypes = Object.keys(terms); - const filteredTermTypes = termTypes.filter(x => x !== OTHER); - return (termTypes.length > 1 && - filteredTermTypes.some(x => terms[x].length > 1)); -}; - -// Collects like terms for an operation node and returns a Node.Status object. -LikeTermCollector.collectLikeTerms = function(node) { - if (!LikeTermCollector.canCollectLikeTerms(node)) { - return Node.Status.noChange(node); - } - - const op = node.op; - let terms = []; - if (op === '+') { - terms = getTermsForCollectingAddition(node); - } - else if (op === '*') { - terms = getTermsForCollectingMultiplication(node); - } - else { - throw Error('Operation not supported: ' + op); - } - - // List the symbols alphabetically - const termTypesSorted = Object.keys(terms) - .filter(x => (x !== CONSTANT && x !== CONSTANT_FRACTION && x !== OTHER)) - .sort(sortTerms); - - - // Then add const - if (terms[CONSTANT]) { - // at the end for addition (since we'd expect x^2 + (x + x) + 4) - if (op === '+') { - termTypesSorted.push(CONSTANT); - } - // for multipliation it should be at the front (e.g. (3*4) * x^2) - if (op === '*') { - termTypesSorted.unshift(CONSTANT); - } - } - if (terms[CONSTANT_FRACTION]) { - termTypesSorted.push(CONSTANT_FRACTION); - } - - // Collect the new operands under op. - let newOperands = []; - let changeGroup = 1; - termTypesSorted.forEach(termType => { - const termsOfType = terms[termType]; - if (termsOfType.length === 1) { - const singleTerm = termsOfType[0].cloneDeep(); - singleTerm.changeGroup = changeGroup; - newOperands.push(singleTerm); - } - // Any like terms should be wrapped in parens. - else { - const termList = Node.Creator.parenthesis( - Node.Creator.operator(op, termsOfType)).cloneDeep(); - termList.changeGroup = changeGroup; - newOperands.push(termList); - } - termsOfType.forEach(term => { - term.changeGroup = changeGroup; - }); - changeGroup++; - }); - - // then stick anything else (paren nodes, operator nodes) at the end - if (terms[OTHER]) { - newOperands = newOperands.concat(terms[OTHER]); - } - - const newNode = node.cloneDeep(); - newNode.args = newOperands; - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_LIKE_TERMS, node, newNode, false); -}; - -// Terms with coefficients are collected by categorizing them by their 'name' -// which is used to separate them into groups that can be combined. getTermName -// returns this group 'name' -function getTermName(node, termSubclass, op) { - const term = new termSubclass(node); - // we 'name' terms by their base node name - let termName = print.ascii(term.getBaseNode()); - // when adding terms, the exponent matters too (e.g. 2x^2 + 5x^3 can't be combined) - if (op === '+') { - const exponent = print.ascii(term.getExponentNode(true)); - termName += '^' + exponent; - } - return termName; -} - -// Collects like terms in an addition expression tree into categories. -// Returns a dictionary of termname to lists of nodes with that name -// e.g. 2x + 4 + 5x would return {'x': [2x, 5x], CONSTANT: [4]} -// (where 2x, 5x, and 4 would actually be expression trees) -function getTermsForCollectingAddition(node) { - let terms = {}; - - for (let i = 0; i < node.args.length; i++) { - const child = node.args[i]; - - if (Node.PolynomialTerm.isPolynomialTerm(child)) { - const termName = getTermName(child, Node.PolynomialTerm, '+'); - terms = Util.appendToArrayInObject(terms, termName, child); - } - else if (Node.NthRootTerm.isNthRootTerm(child)) { - const termName = getTermName(child, Node.NthRootTerm, '+'); - terms = Util.appendToArrayInObject(terms, termName, child); - } - else if (Node.Type.isIntegerFraction(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT_FRACTION, child); - } - else if (Node.Type.isConstant(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT, child); - } - else if (Node.Type.isOperator(node) || - Node.Type.isFunction(node) || - Node.Type.isParenthesis(node) || - Node.Type.isUnaryMinus(node)) { - terms = Util.appendToArrayInObject(terms, OTHER, child); - } - else { - // Note that we shouldn't get any symbol nodes in the switch statement - // since they would have been handled by isPolynomialTerm - throw Error('Unsupported node type: ' + child.type); - } - } - // If there's exactly one constant and one fraction, we collect them - // to add them together. - // e.g. 2 + 1/3 + 5 would collect the constants (2+5) + 1/3 - // but 2 + 1/3 + x would collect (2 + 1/3) + x so we can add them together - if (terms[CONSTANT] && terms[CONSTANT].length === 1 && - terms[CONSTANT_FRACTION] && terms[CONSTANT_FRACTION].length === 1) { - const fraction = terms[CONSTANT_FRACTION][0]; - terms = Util.appendToArrayInObject(terms, CONSTANT, fraction); - delete terms[CONSTANT_FRACTION]; - } - - return terms; -} - -// Collects like terms in a multiplication expression tree into categories. -// For multiplication, polynomial terms with constants are separated into -// a symbolic term and a constant term. -// Returns a dictionary of termname to lists of nodes with that name -// e.g. 2x + 4 + 5x^2 would return {'x': [x, x^2], CONSTANT: [2, 4, 5]} -// (where x, x^2, 2, 4, and 5 would actually be expression trees) -function getTermsForCollectingMultiplication(node) { - let terms = {}; - - for (let i = 0; i < node.args.length; i++) { - let child = node.args[i]; - - if (Node.Type.isUnaryMinus(child)) { - terms = Util.appendToArrayInObject( - terms, CONSTANT, Node.Creator.constant(-1)); - child = child.args[0]; - } - if (Node.PolynomialTerm.isPolynomialTerm(child)) { - terms = addToTermsforPolynomialMultiplication(terms, child); - } - else if (Node.Type.isFunction(child, 'nthRoot')) { - terms = addToTermsforNthRootMultiplication(terms, child); - } - else if (Node.Type.isIntegerFraction(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT, child); - } - else if (Node.Type.isConstant(child)) { - terms = Util.appendToArrayInObject(terms, CONSTANT, child); - } - else if (Node.Type.isOperator(node) || - Node.Type.isFunction(node) || - Node.Type.isParenthesis(node) || - Node.Type.isUnaryMinus(node)) { - terms = Util.appendToArrayInObject(terms, OTHER, child); - } - else { - // Note that we shouldn't get any symbol nodes in the switch statement - // since they would have been handled by isPolynomialTerm - throw Error('Unsupported node type: ' + child.type); - } - } - return terms; -} - -// A helper function for getTermsForCollectingMultiplication -// e.g. nthRoot(x, 2), append 'nthRoot2': nthRootNode to terms dictionary -// Takes the terms dictionary and the nthRoot node, and returns an updated -// terms dictionary. -function addToTermsforNthRootMultiplication(terms, node) { - const rootNode = NthRoot.getRootNode(node); - const rootNodeValue = rootNode.value; - - terms = Util.appendToArrayInObject(terms, NTH_ROOT + rootNodeValue , node); - - return terms; -} - -// A helper function for getTermsForCollectingMultiplication -// Polynomial terms need to be divided into their coefficient + symbolic parts. -// e.g. 2x^4 -> 2 (coeffient) and x^4 (symbolic, named after the symbol node) -// Takes the terms list and the polynomial term node, and returns an updated -// terms list. -function addToTermsforPolynomialMultiplication(terms, node) { - const polyNode = new Node.PolynomialTerm(node); - let termName; - - if (!polyNode.hasCoeff()) { - termName = getTermName(node, Node.PolynomialTerm, '*'); - terms = Util.appendToArrayInObject(terms, termName, node); - } - else { - const coefficient = polyNode.getCoeffNode(); - let termWithoutCoefficient = polyNode.getSymbolNode(); - if (polyNode.getExponentNode()) { - termWithoutCoefficient = Node.Creator.operator( - '^', [termWithoutCoefficient, polyNode.getExponentNode()]); - } - - terms = Util.appendToArrayInObject(terms, CONSTANT, coefficient); - termName = getTermName(termWithoutCoefficient, Node.PolynomialTerm, '*'); - terms = Util.appendToArrayInObject(terms, termName, termWithoutCoefficient); - } - return terms; -} - -// Sort function for termnames. Sort first by symbol name, and then by exponent. -function sortTerms(a, b) { - if (a === b) { - return 0; - } - // if no exponent, sort alphabetically - if (a.indexOf('^') === -1) { - return a < b ? -1 : 1; - } - // if exponent: sort by symbol, but then exponent decreasing - else { - const symbA = a.split('^')[0]; - const expA = a.split('^')[1]; - const symbB = b.split('^')[0]; - const expB = b.split('^')[1]; - if (symbA !== symbB) { - return symbA < symbB ? -1 : 1; - } - else { - return expA > expB ? -1 : 1; - } - } -} - -module.exports = LikeTermCollector; diff --git a/lib/simplifyExpression/fractionsSearch/addConstantFractions.js b/lib/simplifyExpression/fractionsSearch/addConstantFractions.js deleted file mode 100644 index 4dd0629d..00000000 --- a/lib/simplifyExpression/fractionsSearch/addConstantFractions.js +++ /dev/null @@ -1,172 +0,0 @@ -const divideByGCD = require('./divideByGCD'); -const math = require('mathjs'); - -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); - -// Adds constant fractions -- can start from either step 1 or 2 -// 1A. Find the LCD if denominators are different and multiplies to make -// denominators equal, e.g. 2/3 + 4/6 --> (2*2)/(3*2) + 4/6 -// 1B. Multiplies out to make constant fractions again -// e.g. (2*2)/(3*2) + 4/6 -> 4/6 + 4/6 -// 2A. Combines numerators, e.g. 4/6 + 4/6 -> e.g. 2/5 + 4/5 --> (2+4)/5 -// 2B. Adds numerators together, e.g. (2+4)/5 -> 6/5 -// Returns a Node.Status object with substeps -function addConstantFractions(node) { - let newNode = node.cloneDeep(); - - if (!Node.Type.isOperator(node) || node.op !== '+') { - return Node.Status.noChange(node); - } - if (!node.args.every(n => Node.Type.isIntegerFraction(n, true))) { - return Node.Status.noChange(node); - } - const denominators = node.args.map(fraction => { - return parseFloat(evaluate(fraction.args[1])); - }); - - const substeps = []; - let status; - - // 1A. First create the common denominator if needed - // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) - if (!denominators.every(denominator => denominator === denominators[0])) { - status = makeCommonDenominator(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 1B. Multiply out the denominators - status = evaluateDenominators(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 1B. Multiply out the numerators - status = evaluateNumerators(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // 2A. Now that they all have the same denominator, combine the numerators - // e.g. 2/3 + 5/3 -> (2+5)/3 - status = combineNumeratorsAboveCommonDenominator(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 2B. Finally, add the numerators together - status = addNumeratorsTogether(newNode); - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - // 2C. If the numerator is 0, simplify to just 0 - status = reduceNumerator(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - // 2D. If we can simplify the fraction, do so - status = divideByGCD(newNode); - if (status.hasChanged()) { - substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - } - - return Node.Status.nodeChanged( - ChangeTypes.ADD_FRACTIONS, node, newNode, true, substeps); -} - -// Given a + operation node with a list of fraction nodes as args that all have -// the same denominator, add them together. e.g. 2/3 + 5/3 -> (2+5)/3 -// Returns the new node. -function combineNumeratorsAboveCommonDenominator(node) { - let newNode = node.cloneDeep(); - - const commonDenominator = newNode.args[0].args[1]; - const numeratorArgs = []; - newNode.args.forEach(fraction => { - numeratorArgs.push(fraction.args[0]); - }); - const newNumerator = Node.Creator.parenthesis( - Node.Creator.operator('+', numeratorArgs)); - - newNode = Node.Creator.operator('/', [newNumerator, commonDenominator]); - return Node.Status.nodeChanged( - ChangeTypes.COMBINE_NUMERATORS, node, newNode); -} - -// Given a node with a numerator that is an addition node, will add -// all the numerators and return the result -function addNumeratorsTogether(node) { - const newNode = node.cloneDeep(); - - newNode.args[0] = Node.Creator.constant(evaluate(newNode.args[0])); - return Node.Status.nodeChanged( - ChangeTypes.ADD_NUMERATORS, node, newNode); -} - -function reduceNumerator(node) { - let newNode = node.cloneDeep(); - - if (newNode.args[0].value === '0') { - newNode = Node.Creator.constant(0); - return Node.Status.nodeChanged( - ChangeTypes.REDUCE_ZERO_NUMERATOR, node, newNode); - } - - return Node.Status.noChange(node); -} - -// Takes `node`, a sum of fractions, and returns a node that's a sum of -// fractions with denominators that evaluate to the same common denominator -// e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) -// Returns the new node. -function makeCommonDenominator(node) { - const newNode = node.cloneDeep(); - - const denominators = newNode.args.map(fraction => { - return parseFloat(fraction.args[1].value); - }); - const commonDenominator = math.lcm(...denominators); - - newNode.args.forEach((child, i) => { - // missingFactor is what we need to multiply the top and bottom by - // so that the denominator is the LCD - const missingFactor = commonDenominator / denominators[i]; - if (missingFactor !== 1) { - const missingFactorNode = Node.Creator.constant(missingFactor); - const newNumerator = Node.Creator.parenthesis( - Node.Creator.operator('*', [child.args[0], missingFactorNode])); - const newDeominator = Node.Creator.parenthesis( - Node.Creator.operator('*', [child.args[1], missingFactorNode])); - newNode.args[i] = Node.Creator.operator('/', [newNumerator, newDeominator]); - } - }); - - return Node.Status.nodeChanged( - ChangeTypes.COMMON_DENOMINATOR, node, newNode); -} - -function evaluateDenominators(node) { - const newNode = node.cloneDeep(); - - newNode.args.map(fraction => { - fraction.args[1] = Node.Creator.constant(evaluate(fraction.args[1])); - }); - - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_DENOMINATORS, node, newNode); -} - -function evaluateNumerators(node) { - const newNode = node.cloneDeep(); - - newNode.args.map(fraction => { - fraction.args[0] = Node.Creator.constant(evaluate(fraction.args[0])); - }); - - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_NUMERATORS, node, newNode); -} - -module.exports = addConstantFractions; diff --git a/lib/simplifyExpression/fractionsSearch/simplifyFractionSigns.js b/lib/simplifyExpression/fractionsSearch/simplifyFractionSigns.js deleted file mode 100644 index e3779ef1..00000000 --- a/lib/simplifyExpression/fractionsSearch/simplifyFractionSigns.js +++ /dev/null @@ -1,33 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// Simplifies negative signs if possible -// e.g. -1/-3 --> 1/3 4/-5 --> -4/5 -// Note that -4/5 doesn't need to be simplified. -// Note that our goal is for the denominator to always be positive. If it -// isn't, we can simplify signs. -// Returns a Node.Status object -function simplifySigns(fraction) { - if (!Node.Type.isOperator(fraction) || fraction.op !== '/') { - return Node.Status.noChange(fraction); - } - const oldFraction = fraction.cloneDeep(); - let numerator = fraction.args[0]; - let denominator = fraction.args[1]; - // The denominator should never be negative. - if (Negative.isNegative(denominator)) { - denominator = Negative.negate(denominator); - const changeType = Negative.isNegative(numerator) ? - ChangeTypes.CANCEL_MINUSES : - ChangeTypes.SIMPLIFY_SIGNS; - numerator = Negative.negate(numerator); - const newFraction = Node.Creator.operator('/', [numerator, denominator]); - return Node.Status.nodeChanged(changeType, oldFraction, newFraction); - } - else { - return Node.Status.noChange(fraction); - } -} - -module.exports = simplifySigns; diff --git a/lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.js b/lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.js deleted file mode 100644 index c2fc586b..00000000 --- a/lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.js +++ /dev/null @@ -1,44 +0,0 @@ -const arithmeticSearch = require('../arithmeticSearch'); -const divideByGCD = require('./divideByGCD'); -const Node = require('../../node'); - -// Simplifies a polynomial term with a fraction as its coefficients. -// e.g. 2x/4 --> x/2 10x/5 --> 2x -// Also simplified negative signs -// e.g. -y/-3 --> y/3 4x/-5 --> -4x/5 -// returns the new simplified node in a Node.Status object -function simplifyPolynomialFraction(node) { - if (!Node.PolynomialTerm.isPolynomialTerm(node)) { - return Node.Status.noChange(node); - } - - const polyNode = new Node.PolynomialTerm(node.cloneDeep()); - if (!polyNode.hasFractionCoeff()) { - return Node.Status.noChange(node); - } - - const coefficientSimplifications = [ - divideByGCD, // for integer fractions - arithmeticSearch, // for decimal fractions - ]; - - for (let i = 0; i < coefficientSimplifications.length; i++) { - const coefficientFraction = polyNode.getCoeffNode(); // a division node - const newCoeffStatus = coefficientSimplifications[i](coefficientFraction); - if (newCoeffStatus.hasChanged()) { - // we need to reset change groups because we're creating a new node - let newCoeff = Node.Status.resetChangeGroups(newCoeffStatus.newNode); - if (newCoeff.value === '1') { - newCoeff = null; - } - const exponentNode = polyNode.getExponentNode(); - const newNode = Node.Creator.polynomialTerm( - polyNode.getSymbolNode(), exponentNode, newCoeff); - return Node.Status.nodeChanged(newCoeffStatus.changeType, node, newNode); - } - } - - return Node.Status.noChange(node); -} - -module.exports = simplifyPolynomialFraction; diff --git a/lib/simplifyExpression/functionsSearch/absoluteValue.js b/lib/simplifyExpression/functionsSearch/absoluteValue.js deleted file mode 100644 index 252400fc..00000000 --- a/lib/simplifyExpression/functionsSearch/absoluteValue.js +++ /dev/null @@ -1,37 +0,0 @@ -const math = require('mathjs'); - -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); - -// Evaluates abs() function if it's on a single constant value. -// Returns a Node.Status object. -function absoluteValue(node) { - if (!Node.Type.isFunction(node, 'abs')) { - return Node.Status.noChange(node); - } - if (node.args.length > 1) { - return Node.Status.noChange(node); - } - let newNode = node.cloneDeep(); - const argument = newNode.args[0]; - if (Node.Type.isConstant(argument, true)) { - newNode = Node.Creator.constant(math.abs(evaluate(argument))); - return Node.Status.nodeChanged( - ChangeTypes.ABSOLUTE_VALUE, node, newNode); - } - else if (Node.Type.isConstantFraction(argument, true)) { - const newNumerator = Node.Creator.constant( - math.abs(evaluate(argument.args[0]))); - const newDenominator = Node.Creator.constant( - math.abs(evaluate(argument.args[1]))); - newNode = Node.Creator.operator('/', [newNumerator, newDenominator]); - return Node.Status.nodeChanged( - ChangeTypes.ABSOLUTE_VALUE, node, newNode); - } - else { - return Node.Status.noChange(node); - } -} - -module.exports = absoluteValue; diff --git a/lib/simplifyExpression/functionsSearch/index.js b/lib/simplifyExpression/functionsSearch/index.js deleted file mode 100644 index ee4104aa..00000000 --- a/lib/simplifyExpression/functionsSearch/index.js +++ /dev/null @@ -1,32 +0,0 @@ -const absoluteValue = require('./absoluteValue'); - -const Node = require('../../node'); -const NthRoot = require('./nthRoot'); -const TreeSearch = require('../../TreeSearch'); - -const FUNCTIONS = [ - NthRoot.nthRoot, - absoluteValue -]; - -// Searches through the tree, prioritizing deeper nodes, and evaluates -// functions (e.g. abs(-4)) if possible. -// Returns a Node.Status object. -const search = TreeSearch.postOrder(functions); - -// Evaluates a function call if possible. Returns a Node.Status object. -function functions(node) { - if (!Node.Type.isFunction(node)) { - return Node.Status.noChange(node); - } - - for (let i = 0; i < FUNCTIONS.length; i++) { - const nodeStatus = FUNCTIONS[i](node); - if (nodeStatus.hasChanged()) { - return nodeStatus; - } - } - return Node.Status.noChange(node); -} - -module.exports = search; diff --git a/lib/simplifyExpression/index.js b/lib/simplifyExpression/index.js deleted file mode 100644 index ab919afb..00000000 --- a/lib/simplifyExpression/index.js +++ /dev/null @@ -1,18 +0,0 @@ -const math = require('mathjs'); -const stepThrough = require('./stepThrough'); - -function simplifyExpressionString(expressionString, debug=false) { - let exprNode; - try { - exprNode = math.parse(expressionString); - } - catch (err) { - return []; - } - if (exprNode) { - return stepThrough(exprNode, debug); - } - return []; -} - -module.exports = simplifyExpressionString; diff --git a/lib/simplifyExpression/multiplyFractionsSearch/index.js b/lib/simplifyExpression/multiplyFractionsSearch/index.js deleted file mode 100644 index 24a677ef..00000000 --- a/lib/simplifyExpression/multiplyFractionsSearch/index.js +++ /dev/null @@ -1,75 +0,0 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -// If `node` is a product of terms where: -// 1) at least one is a fraction -// 2) either none are polynomial terms, OR -// at least one has a symbol in the denominator -// then multiply them together. -// e.g. 2 * 5/x -> (2*5)/x -// e.g. 3 * 1/5 * 5/9 = (3*1*5)/(5*9) -// e.g. 2x * 1/x -> (2x*1) / x -// NOTE: The reason we exclude the case of polynomial terms is because -// we do not want to combine 9/2 * x -> 9x / 2 (which is less readable). -// Cases like 5/2 * x * y/5 will be handled in collect and combine. -// TODO: add a step somewhere to remove common terms in numerator and -// denominator (so the 5s would cancel out on the next step after this) -// This step must happen after things have been distributed, or else the answer -// will be formatted badly, so it's a tree search of its own. -// Returns a Node.Status object. -const search = TreeSearch.postOrder(multiplyFractions); - -function multiplyFractions(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return Node.Status.noChange(node); - } - - // we need to use the verbose syntax for `some` here because isFraction - // can take more than one parameter - const atLeastOneFraction = node.args.some( - arg => Node.CustomType.isFraction(arg)); - const hasPolynomialTerms = node.args.some(Node.PolynomialTerm.isPolynomialTerm); - const hasPolynomialInDenominatorTerms = node.args.some(hasPolynomialInDenominator); - - if (!atLeastOneFraction || (hasPolynomialTerms && !hasPolynomialInDenominatorTerms)) { - return Node.Status.noChange(node); - } - - const numeratorArgs = []; - const denominatorArgs = []; - node.args.forEach(operand => { - if (Node.CustomType.isFraction(operand)) { - const fraction = Node.CustomType.getFraction(operand); - numeratorArgs.push(fraction.args[0]); - denominatorArgs.push(fraction.args[1]); - } - else { - numeratorArgs.push(operand); - } - }); - - const newNumerator = Node.Creator.parenthesis( - Node.Creator.operator('*', numeratorArgs)); - const newDenominator = denominatorArgs.length === 1 - ? denominatorArgs[0] - : Node.Creator.parenthesis(Node.Creator.operator('*', denominatorArgs)); - - const newNode = Node.Creator.operator('/', [newNumerator, newDenominator]); - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_FRACTIONS, node, newNode); -} - -// Returns true if `node` has a polynomial in the denominator, -// e.g. 5/x or 1/2x^2 -function hasPolynomialInDenominator(node) { - if (!(Node.CustomType.isFraction(node))) { - return false; - } - - const fraction = Node.CustomType.getFraction(node); - const denominator = fraction.args[1]; - return Node.PolynomialTerm.isPolynomialTerm(denominator); -} - -module.exports = search; diff --git a/lib/solveEquation/EquationOperations.js b/lib/solveEquation/EquationOperations.js deleted file mode 100644 index e1431ea7..00000000 --- a/lib/solveEquation/EquationOperations.js +++ /dev/null @@ -1,245 +0,0 @@ -// Operations on equation nodes - -const ChangeTypes = require('../ChangeTypes'); -const Equation = require('../equation/Equation'); -const EquationStatus = require('../equation/Status'); -const Negative = require('../Negative'); -const Node = require('../node'); -const Symbols = require('../Symbols'); - -const COMPARATOR_TO_INVERSE = { - '>': '<', - '>=': '<=', - '<': '>', - '<=': '>=', - '=': '=' -}; - -const EquationOperations = {}; - -// Ensures that the given equation has the given symbolName on the left side, -// by swapping the right and left sides if it is only in the right side. -// So 3 = x would become x = 3. -EquationOperations.ensureSymbolInLeftNode = function(equation, symbolName) { - const leftSideSymbolTerm = Symbols.getLastSymbolTerm( - equation.leftNode, symbolName); - const rightSideSymbolTerm = Symbols.getLastSymbolTerm( - equation.rightNode, symbolName); - - if (!leftSideSymbolTerm) { - if (rightSideSymbolTerm) { - const comparator = COMPARATOR_TO_INVERSE[equation.comparator]; - const oldEquation = equation; - const newEquation = new Equation( - equation.rightNode, equation.leftNode, comparator); - // no change groups are set for this step because everything changes, so - // they wouldn't be pedagogically helpful. - return new EquationStatus( - ChangeTypes.SWAP_SIDES, oldEquation, newEquation); - } - else { - throw Error('No term with symbol: ' + symbolName); - } - } - return EquationStatus.noChange(equation); -}; - -// Ensures that a symbol is not in the denominator by multiplying -// both sides by the denominator if there is a symbol present. -EquationOperations.removeSymbolFromDenominator = function(equation, symbolName) { - // Can't multiply a symbol across non-equal comparators - // because you don't know if it's negative and need to flip the sign - if (equation.comparator !== '=') { - return EquationStatus.noChange(equation); - } - const leftNode = equation.leftNode; - const denominator = Symbols.getLastDenominatorWithSymbolTerm(leftNode, symbolName); - if (denominator) { - return performTermOperationOnEquation( - equation, '*', denominator, ChangeTypes.MULTIPLY_TO_BOTH_SIDES); - } - return EquationStatus.noChange(equation); -}; - -// Removes the given symbolName from the right side by adding or subtracting -// it from both sides as appropriate. -// e.g. 2x = 3x + 5 --> 2x - 3x = 5 -// There are actually no cases where we'd remove symbols from the right side -// by multiplying or dividing by a symbol term. -// TODO: support inverting functions e.g. sqrt, ^, log etc. -EquationOperations.removeSymbolFromRightSide = function(equation, symbolName) { - const rightNode = equation.rightNode; - let symbolTerm = Symbols.getLastSymbolTerm(rightNode, symbolName); - - let inverseOp, inverseTerm, changeType; - if (!symbolTerm){ - return EquationStatus.noChange(equation); - } - - // Clone it so that any operations on it don't affect the node already - // in the equation - symbolTerm = symbolTerm.cloneDeep(); - - if (Node.PolynomialTerm.isPolynomialTerm(rightNode)) { - if (Negative.isNegative(symbolTerm)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = Negative.negate(symbolTerm); - } - else { - inverseOp = '-'; - changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; - inverseTerm = symbolTerm; - } - } - else if (Node.Type.isOperator(rightNode)) { - if (rightNode.op === '+') { - if (Negative.isNegative(symbolTerm)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = Negative.negate(symbolTerm); - } - else { - inverseOp = '-'; - changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; - inverseTerm = symbolTerm; - } - } - else { - // Note that operator '-' won't show up here because subtraction is - // flattened into adding the negative. See 'TRICKY catch' in the README - // for more details. - throw Error('Unsupported operation: ' + symbolTerm.op); - } - } - else if (Node.Type.isUnaryMinus(rightNode)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = symbolTerm.args[0]; - } - else { - throw Error('Unsupported node type: ' + rightNode.type); - } - return performTermOperationOnEquation( - equation, inverseOp, inverseTerm, changeType); -}; - -// Isolates the given symbolName to the left side by adding, multiplying, subtracting -// or dividing all other symbols and constants from both sides appropriately -// TODO: support inverting functions e.g. sqrt, ^, log etc. -EquationOperations.isolateSymbolOnLeftSide = function(equation, symbolName) { - let leftNode = equation.leftNode; - - if (Node.Type.isParenthesis(leftNode)) { - // if entire left node is a parenthesis, we can ignore the parenthesis - leftNode = leftNode.content; - } - - let nonSymbolTerm = Symbols.getLastNonSymbolTerm(leftNode, symbolName); - let inverseOp, inverseTerm, changeType; - - if (!nonSymbolTerm) { - return EquationStatus.noChange(equation); - } - - // Clone it so that any operations on it don't affect the node already - // in the equation - nonSymbolTerm = nonSymbolTerm.cloneDeep(); - - if (Node.Type.isOperator(leftNode)) { - if (leftNode.op === '+') { - if (Negative.isNegative(nonSymbolTerm)) { - inverseOp = '+'; - changeType = ChangeTypes.ADD_TO_BOTH_SIDES; - inverseTerm = Negative.negate(nonSymbolTerm); - } - else { - inverseOp = '-'; - changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; - inverseTerm = nonSymbolTerm; - } - } - else if (leftNode.op === '*') { - if (Node.Type.isConstantFraction(nonSymbolTerm)) { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION; - inverseTerm = Node.Creator.operator( - '/', [nonSymbolTerm.args[1], nonSymbolTerm.args[0]]); - } - else { - inverseOp = '/'; - changeType = ChangeTypes.DIVIDE_FROM_BOTH_SIDES; - inverseTerm = nonSymbolTerm; - } - } - else if (leftNode.op === '/') { - // The non symbol term is always a fraction because it's the - // coefficient of our symbol term. - // If the numerator is 1, we multiply both sides by the denominator, - // otherwise we multiply by the inverse - if (['1', '-1'].indexOf(nonSymbolTerm.args[0].value) !== -1) { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_TO_BOTH_SIDES; - inverseTerm = nonSymbolTerm.args[1]; - } - else { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION; - inverseTerm = Node.Creator.operator( - '/', [nonSymbolTerm.args[1], nonSymbolTerm.args[0]]); - } - } - else if (leftNode.op === '^') { - // TODO: support roots - return EquationStatus.noChange(equation); - } - else { - throw Error('Unsupported operation: ' + leftNode.op); - } - } - else if (Node.Type.isUnaryMinus(leftNode)) { - inverseOp = '*'; - changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE; - inverseTerm = Node.Creator.constant(-1); - } - else { - throw Error('Unsupported node type: ' + leftNode.type); - } - - return performTermOperationOnEquation( - equation, inverseOp, inverseTerm, changeType); -}; - -// Modifies the left and right sides of an equation by `op`-ing `term` -// to both sides. Returns an Status object. -function performTermOperationOnEquation(equation, op, term, changeType) { - const oldEquation = equation.clone(); - - const leftTerm = term.cloneDeep(); - const rightTerm = term.cloneDeep(); - const leftNode = performTermOperationOnExpression( - equation.leftNode, op, leftTerm); - const rightNode = performTermOperationOnExpression( - equation.rightNode, op, rightTerm); - - let comparator = equation.comparator; - if (Negative.isNegative(term) && (op === '*' || op === '/')) { - comparator = COMPARATOR_TO_INVERSE[comparator]; - } - - const newEquation = new Equation(leftNode, rightNode, comparator); - return new EquationStatus(changeType, oldEquation, newEquation); -} - -// Performs an operation of a term on an entire given expression -function performTermOperationOnExpression(expression, op, term) { - const node = (Node.Type.isOperator(expression) ? - Node.Creator.parenthesis(expression) : expression); - - term.changeGroup = 1; - const newNode = Node.Creator.operator(op, [node, term]); - - return newNode; -} - -module.exports = EquationOperations; diff --git a/lib/src/ChangeTypes.ts b/lib/src/ChangeTypes.ts new file mode 100644 index 00000000..b8576a22 --- /dev/null +++ b/lib/src/ChangeTypes.ts @@ -0,0 +1,299 @@ +// The text to identify rules for each possible step that can be taken + +export enum ChangeTypes { + NO_CHANGE = "NO_CHANGE", + + /** + * ARITHMETIC + * */ + + // e.g. 2 + 2 -> 4 or 2 * 2 -> 4 + SIMPLIFY_ARITHMETIC = "SIMPLIFY_ARITHMETIC", + + /** + * BASICS + * */ + + // e.g. 2/-1 -> -2 + DIVISION_BY_NEGATIVE_ONE = "DIVISION_BY_NEGATIVE_ONE", + + // e.g. 2/1 -> 2 + DIVISION_BY_ONE = "DIVISION_BY_ONE", + + // e.g. x * 0 -> 0 + MULTIPLY_BY_ZERO = "MULTIPLY_BY_ZERO", + + // e.g. x * 2 -> 2x + REARRANGE_COEFF = "REARRANGE_COEFF", + + // e.g. x ^ 0 -> 1 + REDUCE_EXPONENT_BY_ZERO = "REDUCE_EXPONENT_BY_ZERO", + + // e.g. 0/1 -> 0 + REDUCE_ZERO_NUMERATOR = "REDUCE_ZERO_NUMERATOR", + + // e.g. 2 + 0 -> 2 + REMOVE_ADDING_ZERO = "REMOVE_ADDING_ZERO", + + // e.g. x ^ 1 -> x + REMOVE_EXPONENT_BY_ONE = "REMOVE_EXPONENT_BY_ONE", + + // e.g. 1 ^ x -> 1 + REMOVE_EXPONENT_BASE_ONE = "REMOVE_EXPONENT_BASE_ONE", + + // e.g. x * -1 -> -x + REMOVE_MULTIPLYING_BY_NEGATIVE_ONE = "REMOVE_MULTIPLYING_BY_NEGATIVE_ONE", + + // e.g. x * 1 -> x + REMOVE_MULTIPLYING_BY_ONE = "REMOVE_MULTIPLYING_BY_ONE", + + // e.g. 2 - - 3 -> 2 + 3 + RESOLVE_DOUBLE_MINUS = "RESOLVE_DOUBLE_MINUS", + + /** + * COLLECT AND COMBINE AND BREAK UP + * */ + + // e.g. 2 + x + 3 + x -> 5 + 2x + COLLECT_AND_COMBINE_LIKE_TERMS = "COLLECT_AND_COMBINE_LIKE_TERMS", + // e.g. x + 2 + x^2 + x + 4 -> x^2 + (x + x) + (4 + 2) + COLLECT_LIKE_TERMS = "COLLECT_LIKE_TERMS", + + // MULTIPLYING CONSTANT POWERS + // e.g. 10^2 * 10^3 -> 10^(2+3) + COLLECT_CONSTANT_EXPONENTS = "COLLECT_CONSTANT_EXPONENTS", + + /** + * ADDING POLYNOMIALS + * */ + + // e.g. 2x + x -> 2x + 1x + ADD_COEFFICIENT_OF_ONE = "ADD_COEFFICIENT_OF_ONE", + + // e.g. x^2 + x^2 -> 2x^2 + ADD_POLYNOMIAL_TERMS = "ADD_POLYNOMIAL_TERMS", + + // e.g. 2x^2 + 3x^2 + 5x^2 -> (2+3+5)x^2 + GROUP_COEFFICIENTS = "GROUP_COEFFICIENTS", + + // e.g. -x + 2x => -1*x + 2x + UNARY_MINUS_TO_NEGATIVE_ONE = "UNARY_MINUS_TO_NEGATIVE_ONE", + + /** + * MULTIPLYING POLYNOMIALS + * */ + + // e.g. x^2 * x -> x^2 * x^1 + ADD_EXPONENT_OF_ONE = "ADD_EXPONENT_OF_ONE", + + // e.g. x^2 * x^3 * x^1 -> x^(2 + 3 + 1) + COLLECT_POLYNOMIAL_EXPONENTS = "COLLECT_POLYNOMIAL_EXPONENTS", + + // e.g. 2x * 3x -> (2 * 3)(x * x) + MULTIPLY_COEFFICIENTS = "MULTIPLY_COEFFICIENTS", + // e.g. 2x * x -> 2x ^ 2 + + MULTIPLY_POLYNOMIAL_TERMS = "MULTIPLY_POLYNOMIAL_TERMS", + + /** + * FRACTIONS + * */ + + // e.g. (x + 2)/2 -> x/2 + 2/2 + BREAK_UP_FRACTION = "BREAK_UP_FRACTION", + + // e.g. -2/-3 => 2/3 + CANCEL_MINUSES = "CANCEL_MINUSES", + + // e.g. 2x/2 -> x + CANCEL_TERMS = "CANCEL_TERMS", + + // e.g. 2/6 -> 1/3 + SIMPLIFY_FRACTION = "SIMPLIFY_FRACTION", + + // e.g. 2/-3 -> -2/3 + SIMPLIFY_SIGNS = "SIMPLIFY_SIGNS", + + // e.g. 15/6 -> (5*3)/(2*3) + FIND_GCD = "FIND_GCD", + + // e.g. (5*3)/(2*3) -> 5/2 + CANCEL_GCD = "CANCEL_GCD", + + // e.g. 1 2/3 -> 5/3 + CONVERT_MIXED_NUMBER_TO_IMPROPER_FRACTION = "CONVERT_MIXED_NUMBER_TO_IMPROPER_FRACTION", + + // e.g. 1 2/3 -> ((1 * 3) + 2) / 3 + IMPROPER_FRACTION_NUMERATOR = "IMPROPER_FRACTION_NUMERATOR", + + /** + * ADDING FRACTIONS + * */ + + // e.g. 1/2 + 1/3 -> 5/6 + ADD_FRACTIONS = "ADD_FRACTIONS", + + // e.g. (1 + 2)/3 -> 3/3 + ADD_NUMERATORS = "ADD_NUMERATORS", + + // e.g. (2+1)/5 + COMBINE_NUMERATORS = "COMBINE_NUMERATORS", + + // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) + COMMON_DENOMINATOR = "COMMON_DENOMINATOR", + + // e.g. 3 + 1/2 -> 6/2 + 1/2 (for addition) + CONVERT_INTEGER_TO_FRACTION = "CONVERT_INTEGER_TO_FRACTION", + + // e.g. 1.2 + 1/2 -> 1.2 + 0.5 + DIVIDE_FRACTION_FOR_ADDITION = "DIVIDE_FRACTION_FOR_ADDITION", + + // e.g. (2*2)/(6*2) + (1*3)/(4*3) -> (2*2)/12 + (1*3)/12 + MULTIPLY_DENOMINATORS = "MULTIPLY_DENOMINATORS", + + // e.g. (2*2)/12 + (1*3)/12 -> 4/12 + 3/12 + MULTIPLY_NUMERATORS = "MULTIPLY_NUMERATORS", + + /** + * MULTIPLYING FRACTIONS + * */ + + // e.g. 1/2 * 2/3 -> 2/6 + MULTIPLY_FRACTIONS = "MULTIPLY_FRACTIONS", + + /** + * DIVISION + * */ + + // e.g. 2/3/4 -> 2/(3*4) + SIMPLIFY_DIVISION = "SIMPLIFY_DIVISION", + + // e.g. x/(2/3) -> x * 3/2 + MULTIPLY_BY_INVERSE = "MULTIPLY_BY_INVERSE", + + /** + * DISTRIBUTION + * */ + + // e.g. 2(x + y) -> 2x + 2y + DISTRIBUTE = "DISTRIBUTE", + + // e.g. -(2 + x) -> -2 - x + DISTRIBUTE_NEGATIVE_ONE = "DISTRIBUTE_NEGATIVE_ONE", + + // e.g. 2 * 4x + 2*5 --> 8x + 10 (as part of distribution) + SIMPLIFY_TERMS = "SIMPLIFY_TERMS", + + // e.g. (nthRoot(x, 2))^2 -> nthRoot(x, 2) * nthRoot(x, 2) + // e.g. (2x + 3)^2 -> (2x + 3) (2x + 3) + EXPAND_EXPONENT = "EXPAND_EXPONENT", + + /** + * ABSOLUTE + * */ + // e.g. |-3| -> 3 + ABSOLUTE_VALUE = "ABSOLUTE_VALUE", + + /** + * ROOTS + * */ + + // e.g. nthRoot(x ^ 2, 4) -> nthRoot(x, 2) + CANCEL_EXPONENT = "CANCEL_EXPONENT", + + // e.g. nthRoot(x ^ 2, 2) -> x + CANCEL_EXPONENT_AND_ROOT = "CANCEL_EXPONENT_AND_ROOT", + + // e.g. nthRoot(x ^ 4, 2) -> x ^ 2 + CANCEL_ROOT = "CANCEL_ROOT", + + // e.g. nthRoot(2, 2) * nthRoot(3, 2) -> nthRoot(2 * 3, 2) + COMBINE_UNDER_ROOT = "COMBINE_UNDER_ROOT", + + // e.g. 2 * 2 * 2 -> 2 ^ 3 + CONVERT_MULTIPLICATION_TO_EXPONENT = "CONVERT_MULTIPLICATION_TO_EXPONENT", + + // e.g. nthRoot(2 * x) -> nthRoot(2) * nthRoot(x) + DISTRIBUTE_NTH_ROOT = "DISTRIBUTE_NTH_ROOT", + + // e.g. nthRoot(4) * nthRoot(x^2) -> 2 * x + EVALUATE_DISTRIBUTED_NTH_ROOT = "EVALUATE_DISTRIBUTED_NTH_ROOT", + + // e.g. 12 -> 2 * 2 * 3 + FACTOR_INTO_PRIMES = "FACTOR_INTO_PRIMES", + + // e.g. nthRoot(2 * 2 * 2, 2) -> nthRoot((2 * 2) * 2) + GROUP_TERMS_BY_ROOT = "GROUP_TERMS_BY_ROOT", + + // e.g. nthRoot(4) -> 2 + NTH_ROOT_VALUE = "NTH_ROOT_VALUE", + + // e.g. nthRoot(4) + nthRoot(4) = 2*nthRoot(4) + ADD_NTH_ROOTS = "ADD_NTH_ROOTS", + + // e.g. nthRoot(x, 2) * nthRoot(x, 2) -> nthRoot(x^2, 2) + MULTIPLY_NTH_ROOTS = "MULTIPLY_NTH_ROOTS", + + /** + * SOLVING FOR A VARIABLE + * */ + + // e.g. x - 3 = 2 -> x - 3 + 3 = 2 + 3 + ADD_TO_BOTH_SIDES = "ADD_TO_BOTH_SIDES", + + // e.g. 2x = 1 -> (2x)/2 = 1/2 + DIVIDE_FROM_BOTH_SIDES = "DIVIDE_FROM_BOTH_SIDES", + + // e.g. (2/3)x = 1 -> (2/3)x * (3/2) = 1 * (3/2) + MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION = "MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION", + + // e.g. -x = 2 -> -1 * -x = -1 * 2 + MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE = "MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE", + + // e.g. x/2 = 1 -> (x/2) * 2 = 1 * 2 + MULTIPLY_TO_BOTH_SIDES = "MULTIPLY_TO_BOTH_SIDES", + + // e.g. x + 2 - 1 = 3 -> x + 1 = 3 + SIMPLIFY_LEFT_SIDE = "SIMPLIFY_LEFT_SIDE", + + // e.g. x = 3 - 1 -> x = 2 + SIMPLIFY_RIGHT_SIDE = "SIMPLIFY_RIGHT_SIDE", + + // e.g. x + 3 = 2 -> x + 3 - 3 = 2 - 3 + SUBTRACT_FROM_BOTH_SIDES = "SUBTRACT_FROM_BOTH_SIDES", + + // e.g. 2 = x -> x = 2 + SWAP_SIDES = "SWAP_SIDES", + + // e.g. (x - 2) (x + 2) = 0 => x = [-2, 2] + FIND_ROOTS = "FIND_ROOTS", + + /** + * CONSTANT EQUATION + * */ + + // e.g. 2 = 2 + STATEMENT_IS_TRUE = "STATEMENT_IS_TRUE", + + // e.g. 2 = 3 + STATEMENT_IS_FALSE = "STATEMENT_IS_FALSE", + + /** + * FACTORING + * */ + + // e.g. x^2 - 4x -> x(x - 4) + FACTOR_SYMBOL = "FACTOR_SYMBOL", + + // e.g. x^2 - 4 -> (x - 2)(x + 2) + FACTOR_DIFFERENCE_OF_SQUARES = "FACTOR_DIFFERENCE_OF_SQUARES", + + // e.g. x^2 + 2x + 1 -> (x + 1)^2 + FACTOR_PERFECT_SQUARE = "FACTOR_PERFECT_SQUARE", + + // e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) + FACTOR_SUM_PRODUCT_RULE = "FACTOR_SUM_PRODUCT_RULE", + + // e.g. 2x^2 + 4x + 2 -> 2x^2 + 2x + 2x + 2 + BREAK_UP_TERM = "BREAK_UP_TERM", +} diff --git a/lib/src/Negative.ts b/lib/src/Negative.ts new file mode 100644 index 00000000..1890a5b8 --- /dev/null +++ b/lib/src/Negative.ts @@ -0,0 +1,96 @@ +import { NodeCreator } from "./node/Creator"; +import { NodeType } from "./node/NodeType"; +import { PolynomialTerm } from "./node/PolynomialTerm"; + +class NegativeClass { + /** + * Returns if the given node is negative. Treats a unary minus as a negative, + * as well as a negative constant value or a constant fraction that would + * evaluate to a negative number + * */ + isNegative(node) { + if (NodeType.isUnaryMinus(node)) { + return !Negative.isNegative(node.args[0]); + } else if (NodeType.isConstant(node)) { + return parseFloat(node.value) < 0; + } else if (NodeType.isConstantFraction(node)) { + const numeratorValue = parseFloat(node.args[0].value); + const denominatorValue = parseFloat(node.args[1].value); + if (numeratorValue < 0 || denominatorValue < 0) { + return !(numeratorValue < 0 && denominatorValue < 0); + } + } else if (PolynomialTerm.isPolynomialTerm(node)) { + const polyNode = new PolynomialTerm(node); + return Negative.isNegative(polyNode.getCoeffNode(true)); + } + + return false; + } + + /** + * Given a node, returns the negated node + * If naive is true, then we just add an extra unary minus to the expression + * otherwise, we do the actual negation + * E.g. + * not naive: -3 -> 3, x -> -x + * naive: -3 -> --3, x -> -x + * */ + negate(node, naive = false) { + if (NodeType.isConstantFraction(node)) { + node.args[0] = Negative.negate(node.args[0], naive); + return node; + } else if (PolynomialTerm.isPolynomialTerm(node)) { + return Negative.negatePolynomialTerm(node, naive); + } else if (!naive) { + if (NodeType.isUnaryMinus(node)) { + return node.args[0]; + } else if (NodeType.isConstant(node)) { + return NodeCreator.constant(0 - parseFloat(node.value)); + } + } + return NodeCreator.unaryMinus(node); + } + + /** + * Multiplies a polynomial term by -1 and returns the new node + * If naive is true, then we just add an extra unary minus to the expression + * otherwise, we do the actual negation + * E.g. + * not naive: -3x -> 3x, x -> -x + * naive: -3x -> --3x, x -> -x + * */ + negatePolynomialTerm(node, naive = false) { + if (!PolynomialTerm.isPolynomialTerm(node)) { + throw Error("node is not a polynomial term"); + } + const polyNode = new PolynomialTerm(node); + + let newCoeff; + if (!polyNode.hasCoeff()) { + newCoeff = NodeCreator.constant(-1); + } else { + const oldCoeff = polyNode.getCoeffNode(); + if (oldCoeff.value === "-1") { + newCoeff = null; + } else if (polyNode.hasFractionCoeff()) { + let numerator = oldCoeff.args[0]; + numerator = Negative.negate(numerator, naive); + + const denominator = oldCoeff.args[1]; + newCoeff = NodeCreator.operator("/", [numerator, denominator]); + } else { + newCoeff = Negative.negate(oldCoeff, naive); + if (newCoeff.value === "1") { + newCoeff = null; + } + } + } + return NodeCreator.polynomialTerm( + polyNode.getSymbolNode(), + polyNode.getExponentNode(), + newCoeff + ); + } +} + +export const Negative = new NegativeClass(); diff --git a/lib/src/Symbols.ts b/lib/src/Symbols.ts new file mode 100644 index 00000000..5187a7e3 --- /dev/null +++ b/lib/src/Symbols.ts @@ -0,0 +1,130 @@ +import { PolynomialTerm } from "./node/PolynomialTerm"; +import { NodeType } from "./node/NodeType"; + +export class Symbols { + // returns the set of all the symbols in an equation + static getSymbolsInEquation(equation) { + const leftSymbols = Symbols.getSymbolsInExpression(equation.leftNode); + const rightSymbols = Symbols.getSymbolsInExpression(equation.rightNode); + const symbols = new Set([...leftSymbols, ...rightSymbols]); + return symbols; + } + + // return the set of symbols in the expression tree + static getSymbolsInExpression(expression) { + const symbolNodes = expression.filter((node) => node.isSymbolNode); // all the symbol nodes + const symbols = symbolNodes.map((node) => node.name); // all the symbol nodes' names + const symbolSet = new Set(symbols); // to get rid of duplicates + return symbolSet; + } + + // Iterates through a node and returns the last term with the symbol name + // Returns null if no terms with the symbol name are in the node. + // e.g. 4x^2 + 2x + y + 2 with `symbolName=x` would return 2x + static getLastSymbolTerm(node, symbolName) { + // First check if the node itself is a polyomial term with symbolName + if (isSymbolTerm(node, symbolName)) { + return node; + } + // If it's a sum of terms, look through the operands for a term + // with `symbolName` + else if (NodeType.isOperator(node, "+")) { + for (let i = node.args.length - 1; i >= 0; i--) { + const child = node.args[i]; + if (NodeType.isOperator(child, "+")) { + return Symbols.getLastSymbolTerm(child, symbolName); + } else if (isSymbolTerm(child, symbolName)) { + return child; + } + } + } else if (NodeType.isParenthesis(node)) { + return Symbols.getLastSymbolTerm(node.content, symbolName); + } + + return null; + } + + /** + * Iterates through a node and returns the last term that does not have the + * symbolName including other polynomial terms, and constants or constant + * fractions + * e.g. 4x^2 with `symbolName=x` would return 4 + * e.g. 4x^2 + 2x + 2/4 with `symbolName=x` would return 2/4 + * e.g. 4x^2 + 2x + y with `symbolName=x` would return y + * */ + static getLastNonSymbolTerm(node, symbolName) { + if (isPolynomialTermWithSymbol(node, symbolName)) { + return new PolynomialTerm(node).getCoeffNode(); + } else if (hasDenominatorSymbol(node, symbolName)) { + return null; + } else if (NodeType.isOperator(node)) { + for (let i = node.args.length - 1; i >= 0; i--) { + const child = node.args[i]; + if (NodeType.isOperator(child, "+")) { + return Symbols.getLastNonSymbolTerm(child, symbolName); + } else if (!isSymbolTerm(child, symbolName)) { + return child; + } + } + } + + return null; + } + + // Iterates through a node and returns the denominator if it has a + // symbolName in its denominator + // e.g. 1/(2x) with `symbolName=x` would return 2x + // e.g. 1/(x+2) with `symbolName=x` would return x+2 + // e.g. 1/(x+2) + (x+1)/(2x+3) with `symbolName=x` would return (2x+3) + static getLastDenominatorWithSymbolTerm(node, symbolName) { + // First check if the node itself has a symbol in the denominator + if (hasDenominatorSymbol(node, symbolName)) { + return node.args[1]; + } + // Otherwise, it's a sum of terms. e.g. 1/x + 1(2+x) + // Look through the operands for a + // denominator term with `symbolName` + else if (NodeType.isOperator(node, "+")) { + for (let i = node.args.length - 1; i >= 0; i--) { + const child = node.args[i]; + if (NodeType.isOperator(child, "+")) { + return Symbols.getLastDenominatorWithSymbolTerm(child, symbolName); + } else if (hasDenominatorSymbol(child, symbolName)) { + return child.args[1]; + } + } + } + return null; + } +} + +// Returns if `node` is a term with symbol `symbolName` +function isSymbolTerm(node, symbolName) { + return ( + isPolynomialTermWithSymbol(node, symbolName) || + hasDenominatorSymbol(node, symbolName) + ); +} + +function isPolynomialTermWithSymbol(node, symbolName) { + if (PolynomialTerm.isPolynomialTerm(node)) { + const polyTerm = new PolynomialTerm(node); + if (polyTerm.getSymbolName() === symbolName) { + return true; + } + } + + return false; +} + +// Return if `node` has a symbol in its denominator +// e.g. true for 1/(2x) +// e.g. false for 5x +function hasDenominatorSymbol(node, symbolName) { + if (NodeType.isOperator(node) && node.op === "/") { + const allSymbols = Symbols.getSymbolsInExpression(node.args[1]); + return allSymbols.has(symbolName); + } + + return false; +} diff --git a/lib/src/TreeSearch.ts b/lib/src/TreeSearch.ts new file mode 100644 index 00000000..edd45957 --- /dev/null +++ b/lib/src/TreeSearch.ts @@ -0,0 +1,67 @@ +import { NodeStatus } from "./node/NodeStatus"; +import { NodeType } from "./node/NodeType"; + +export function search(simplificationFunction, node, preOrder) { + let status; + + if (preOrder) { + status = simplificationFunction(node); + if (status.hasChanged) { + return status; + } + } + + if (NodeType.isConstant(node) || NodeType.isSymbol(node)) { + return NodeStatus.noChange(node); + } else if (NodeType.isUnaryMinus(node)) { + status = search(simplificationFunction, node.args[0], preOrder); + if (status.hasChanged) { + return NodeStatus.childChanged(node, status); + } + } else if (NodeType.isOperator(node) || NodeType.isFunction(node)) { + for (let i = 0; i < node.args.length; i++) { + const child = node.args[i]; + const childNodeStatus = search(simplificationFunction, child, preOrder); + if (childNodeStatus.hasChanged) { + return NodeStatus.childChanged(node, childNodeStatus, i); + } + } + } else if (NodeType.isParenthesis(node)) { + status = search(simplificationFunction, node.content, preOrder); + if (status.hasChanged) { + return NodeStatus.childChanged(node, status); + } + } else { + throw Error("Unsupported node type: " + node); + } + + if (!preOrder) { + return simplificationFunction(node); + } else { + return NodeStatus.noChange(node); + } +} + +class TreeSearchImpl { + /** + * Returns a function that performs a preorder search on the tree for the given + * simplification function + * */ + preOrder(simplificationFunction) { + return function (node) { + return search(simplificationFunction, node, true); + }; + } + + /** + * Returns a function that performs a postorder search on the tree for the given + * simplification function + * */ + postOrder(simplificationFunction) { + return function (node) { + return search(simplificationFunction, node, false); + }; + } +} + +export const TreeSearch = new TreeSearchImpl(); diff --git a/lib/src/checks/canAddLikeTerms.ts b/lib/src/checks/canAddLikeTerms.ts new file mode 100644 index 00000000..17b9a9f7 --- /dev/null +++ b/lib/src/checks/canAddLikeTerms.ts @@ -0,0 +1,49 @@ +import { NodeType } from "../node/NodeType"; +import { Term } from "../node/Term"; +import { NthRootTerm } from "../node/NthRootTerm"; +import { PolynomialTerm } from "../node/PolynomialTerm"; + +/** + * Returns true if the nodes are terms that can be added together. + * The nodes need to have the same base and exponent + * e.g. 2x + 5x, 6x^2 + x^2, nthRoot(4,2) + nthRoot(4,2) + * */ +export function canAddLikeTermNodes(node, termSubclass) { + if (!NodeType.isOperator(node, "+")) { + return false; + } + const args = node.args; + if (!args.every((n) => Term.isTerm(n, termSubclass.baseNodeFunc))) { + return false; + } + if (args.length === 1) { + return false; + } + + const termList = args.map((n) => new termSubclass(n)); + + // to add terms, they must have the same base *and* exponent + const firstTerm = termList[0]; + const sharedBase = firstTerm.getBaseNode(); + const sharedExponentNode = firstTerm.getExponentNode(true); + + const restTerms = termList.slice(1); + return restTerms.every((term) => { + const haveSameBase = sharedBase.equals(term.getBaseNode()); + const exponentNode = term.getExponentNode(true); + const haveSameExponent = exponentNode.equals(sharedExponentNode); + return haveSameBase && haveSameExponent; + }); +} + +/** + * Returns true if the nodes are nth roots that can be added together + * */ +export function canAddLikeTermNthRootNodes(node) { + return canAddLikeTermNodes(node, NthRootTerm); +} + +// Returns true if the nodes are polynomial terms that can be added together. +export function canAddLikeTermPolynomialNodes(node) { + return canAddLikeTermNodes(node, PolynomialTerm); +} diff --git a/lib/src/checks/canFindRoots.ts b/lib/src/checks/canFindRoots.ts new file mode 100644 index 00000000..8a73f076 --- /dev/null +++ b/lib/src/checks/canFindRoots.ts @@ -0,0 +1,34 @@ +import { resolvesToConstant } from "./resolvesToConstant"; +import { NodeType } from "../node/NodeType"; + +/** + * Return true if the equation is of the form factor * factor = 0 or factor^power = 0 + * e.g (x - 2)^2 = 0, x(x + 2)(x - 2) = 0 + */ +export function canFindRoots(equation) { + const left = equation.leftNode; + const right = equation.rightNode; + + const zeroRightSide = + NodeType.isConstant(right) && parseFloat(right.value) === 0; + + const isMulOrPower = + NodeType.isOperator(left, "*") || NodeType.isOperator(left, "^"); + + if (!(zeroRightSide && isMulOrPower)) { + return false; + } + + // If the left side of the equation is multiplication, filter out all the factors + // that do evaluate to constants because they do not have roots. If the + // resulting array is empty, there is no roots to be found. Do a similiar check + // for when the left side is a power node. + // e.g 2^7 and (33 + 89) do not have solutions when set equal to 0 + + if (NodeType.isOperator(left, "*")) { + const factors = left.args.filter((arg) => !resolvesToConstant(arg)); + return factors.length >= 1; + } else if (NodeType.isOperator(left, "^")) { + return !resolvesToConstant(left); + } +} diff --git a/lib/src/checks/canMultiplyLikeTermConstantNodes.ts b/lib/src/checks/canMultiplyLikeTermConstantNodes.ts new file mode 100644 index 00000000..e8c76245 --- /dev/null +++ b/lib/src/checks/canMultiplyLikeTermConstantNodes.ts @@ -0,0 +1,32 @@ +import { + getBaseNode, + isConstantOrConstantPower, +} from "../simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower"; +import { NodeType } from "../node/NodeType"; + +/** + * Returns true if node is a multiplication of constant power nodes + * where you can combine their exponents, e.g. 10^2 * 10^4 * 10 can become 10^7. + * The node can either be on form c^n or c, as long as c is the same for all. + */ +export function canMultiplyLikeTermConstantNodes(node) { + if (!NodeType.isOperator(node) || node.op !== "*") { + return false; + } + const args = node.args; + if (!args.every((n) => isConstantOrConstantPower(n))) { + return false; + } + + // if none of the terms have exponents, return false here, + // else e.g. 6*6 will become 6^1 * 6^1 => 6^2 + if (args.every((arg) => !NodeType.isOperator(arg, "^"))) { + return false; + } + + const constantTermBaseList = args.map((n) => getBaseNode(n)); + const firstTerm = constantTermBaseList[0]; + const restTerms = constantTermBaseList.slice(1); + // they're considered like terms if they have the same base value + return restTerms.every((term) => firstTerm.value === term.value); +} diff --git a/lib/src/checks/canMultiplyLikeTermPolynomialNodes.ts b/lib/src/checks/canMultiplyLikeTermPolynomialNodes.ts new file mode 100644 index 00000000..9c980477 --- /dev/null +++ b/lib/src/checks/canMultiplyLikeTermPolynomialNodes.ts @@ -0,0 +1,32 @@ +import { NodeType } from "../node/NodeType"; +import { PolynomialTerm } from "../node/PolynomialTerm"; + +/** + * Returns true if the nodes are symbolic terms with the same symbol and no + * coefficients. + * */ +export function canMultiplyLikeTermPolynomialNodes(node) { + if (!NodeType.isOperator(node) || node.op !== "*") { + return false; + } + const args = node.args; + if (!args.every((n) => PolynomialTerm.isPolynomialTerm(n))) { + return false; + } + if (args.length === 1) { + return false; + } + + const polynomialTermList = node.args.map((n) => new PolynomialTerm(n)); + if (!polynomialTermList.every((polyTerm) => !polyTerm.hasCoeff())) { + return false; + } + + const firstTerm = polynomialTermList[0]; + const restTerms = polynomialTermList.slice(1); + + // they're considered like terms if they have the same symbol name + return restTerms.every( + (term) => firstTerm.getSymbolName() === term.getSymbolName() + ); +} diff --git a/lib/src/checks/canMultiplyLikeTermsNthRoots.ts b/lib/src/checks/canMultiplyLikeTermsNthRoots.ts new file mode 100644 index 00000000..b276619e --- /dev/null +++ b/lib/src/checks/canMultiplyLikeTermsNthRoots.ts @@ -0,0 +1,25 @@ +import { NodeType } from "../node/NodeType"; +import { getRootNode } from "../simplifyExpression/functionsSearch/nthRoot"; + +/** + * Function to check if nthRoot nodes can be multiplied + * e.g. nthRoot(x, 2) * nthRoot(x, 2) -> true + * e.g. nthRoot(x, 2) * nthRoot(x, 3) -> false + * */ +export function canMultiplyLikeTermsNthRoots(node) { + // checks if node is a multiplication of nthRoot nodes + // all the terms has to have the same root node to be multiplied + + if ( + !NodeType.isOperator(node, "*") || + !node.args.every((term) => NodeType.isFunction(term, "nthRoot")) + ) { + return false; + } + + // Take arbitrary root node + const firstTerm = node.args[0]; + const rootNode = getRootNode(firstTerm); + + return node.args.every((term) => getRootNode(term).equals(rootNode)); +} diff --git a/lib/src/checks/canRearrangeCoefficient.ts b/lib/src/checks/canRearrangeCoefficient.ts new file mode 100644 index 00000000..08a3c69e --- /dev/null +++ b/lib/src/checks/canRearrangeCoefficient.ts @@ -0,0 +1,26 @@ +import { NodeType } from "../node/NodeType"; +import { PolynomialTerm } from "../node/PolynomialTerm"; + +/** + * Returns true if the expression is a multiplication between a constant + * and polynomial without a coefficient. + * */ +export function canRearrangeCoefficient(node) { + // implicit multiplication doesn't count as multiplication here, since it + // represents a single term. + if (node.op !== "*" || node.implicit) { + return false; + } + if (node.args.length !== 2) { + return false; + } + if (!NodeType.isConstantOrConstantFraction(node.args[1])) { + return false; + } + if (!PolynomialTerm.isPolynomialTerm(node.args[0])) { + return false; + } + + const polyNode = new PolynomialTerm(node.args[0]); + return !polyNode.hasCoeff(); +} diff --git a/lib/src/checks/canSimplifyPolynomialTerms.ts b/lib/src/checks/canSimplifyPolynomialTerms.ts new file mode 100644 index 00000000..bfbcde1d --- /dev/null +++ b/lib/src/checks/canSimplifyPolynomialTerms.ts @@ -0,0 +1,15 @@ +import { canAddLikeTermPolynomialNodes } from "./canAddLikeTerms"; +import { canMultiplyLikeTermPolynomialNodes } from "./canMultiplyLikeTermPolynomialNodes"; +import { canRearrangeCoefficient } from "./canRearrangeCoefficient"; + +/** + * Returns true if the node is an operation node with parameters that are + * polynomial terms that can be combined in some way. + * */ +export function canSimplifyPolynomialTerms(node) { + return ( + canAddLikeTermPolynomialNodes(node) || + canMultiplyLikeTermPolynomialNodes(node) || + canRearrangeCoefficient(node) + ); +} diff --git a/lib/src/checks/hasUnsupportedNodes.ts b/lib/src/checks/hasUnsupportedNodes.ts new file mode 100644 index 00000000..a4649aa4 --- /dev/null +++ b/lib/src/checks/hasUnsupportedNodes.ts @@ -0,0 +1,40 @@ +import { resolvesToConstant } from "./resolvesToConstant"; +import { NodeType } from "../node/NodeType"; + +export function hasUnsupportedNodes(node, debug = false): boolean { + if (NodeType.isParenthesis(node)) { + return hasUnsupportedNodes(node.content); + } else if (NodeType.isUnaryMinus(node)) { + return hasUnsupportedNodes(node.args[0]); + } else if (NodeType.isOperator(node)) { + return node.args.some(hasUnsupportedNodes); + } else if (NodeType.isSymbol(node) || NodeType.isConstant(node)) { + return false; + } else if (NodeType.isFunction(node, "abs")) { + if (node.args.length !== 1) { + return isUnsupported(node, debug); + } + if (node.args.some(hasUnsupportedNodes)) { + return isUnsupported(node, debug); + } + return !resolvesToConstant(node.args[0]); + } else if (NodeType.isFunction(node, "nthRoot")) { + return node.args.some(hasUnsupportedNodes) || node.args.length < 1; + } else if (NodeType.isFunction(node, "fraction")) { + return isUnsupported(node, debug); + + // TODO: would be nice if we had support for fractions like this as well + // return node.args.some(hasUnsupportedNodes) || node.args.length < 1; + } else { + return isUnsupported(node, debug); + } +} + +function isUnsupported(node, debug): boolean { + if (debug) { + console.error("UNSUPPORTED_NODE"); + console.error(`node:`, node); + console.error(`node.type:`, node.type); + } + return true; +} diff --git a/lib/src/checks/isQuadratic.ts b/lib/src/checks/isQuadratic.ts new file mode 100644 index 00000000..1b339f84 --- /dev/null +++ b/lib/src/checks/isQuadratic.ts @@ -0,0 +1,64 @@ +import { NodeType } from "../node/NodeType"; +import { Symbols } from "../Symbols"; +import { PolynomialTerm } from "../node/PolynomialTerm"; + +/** + * Given a node, will determine if the expression is in the form of a quadratic + * e.g. `x^2 + 2x + 1` OR `x^2 - 1` but not `x^3 + x^2 + x + 1` + * */ +export function isQuadratic(node) { + if (!NodeType.isOperator(node, "+")) { + return false; + } + + if (node.args.length > 3) { + return false; + } + + // make sure only one symbol appears in the expression + const symbolSet = Symbols.getSymbolsInExpression(node); + if (symbolSet.size !== 1) { + return false; + } + + const secondDegreeTerms = node.args.filter(isPolynomialTermOfDegree(2)); + const firstDegreeTerms = node.args.filter(isPolynomialTermOfDegree(1)); + const constantTerms = node.args.filter(NodeType.isConstant); + + // Check that there is one second degree term and at most one first degree + // term and at most one constant term + if ( + secondDegreeTerms.length !== 1 || + firstDegreeTerms.length > 1 || + constantTerms.length > 1 + ) { + return false; + } + + // check that there are no terms that don't fall into these groups + if ( + secondDegreeTerms.length + + firstDegreeTerms.length + + constantTerms.length !== + node.args.length + ) { + return false; + } + + return true; +} + +/** + * Given a degree, returns a function that checks if a node + * is a polynomial term of the given degree. + * */ +function isPolynomialTermOfDegree(degree) { + return function (node) { + if (PolynomialTerm.isPolynomialTerm(node)) { + const polyTerm = new PolynomialTerm(node); + const exponent = polyTerm.getExponentNode(true); + return exponent && parseFloat(exponent.value) === degree; + } + return false; + }; +} diff --git a/lib/src/checks/resolvesToConstant.ts b/lib/src/checks/resolvesToConstant.ts new file mode 100644 index 00000000..ad73ed3e --- /dev/null +++ b/lib/src/checks/resolvesToConstant.ts @@ -0,0 +1,22 @@ +import { NodeType } from "../node/NodeType"; + +/** + * Returns true if the node is a constant or can eventually be resolved to + * a constant. + * e.g. 2, 2+4, (2+4)^2 would all return true. x + 4 would return false + * */ +export function resolvesToConstant(node) { + if (NodeType.isOperator(node) || NodeType.isFunction(node)) { + return node.args.every((child) => resolvesToConstant(child)); + } else if (NodeType.isParenthesis(node)) { + return resolvesToConstant(node.content); + } else if (NodeType.isConstant(node, true)) { + return true; + } else if (NodeType.isSymbol(node)) { + return false; + } else if (NodeType.isUnaryMinus(node)) { + return resolvesToConstant(node.args[0]); + } else { + throw Error("Unsupported node type: " + NodeType); + } +} diff --git a/lib/src/equation/Equation.ts b/lib/src/equation/Equation.ts new file mode 100644 index 00000000..96a55736 --- /dev/null +++ b/lib/src/equation/Equation.ts @@ -0,0 +1,53 @@ +import * as math from "mathjs"; +import { printAscii, printLatex } from "../util/print"; + +/** + * This represents an equation, made up of the leftNode (LHS), the + * rightNode (RHS) and a comparator (=, <, >, <=, or >=) + * */ +export class Equation { + constructor(public leftNode, public rightNode, public comparator) { + this.leftNode = leftNode; + this.rightNode = rightNode; + this.comparator = comparator; + } + + // Prints an Equation properly using the print module + ascii(showPlusMinus = false) { + const leftSide = printAscii(this.leftNode, showPlusMinus); + const rightSide = printAscii(this.rightNode, showPlusMinus); + const comparator = this.comparator; + + return `${leftSide} ${comparator} ${rightSide}`; + } + + // Prints an Equation properly using LaTeX + latex(showPlusMinus = false) { + const leftSide = printLatex(this.leftNode, showPlusMinus); + const rightSide = printLatex(this.rightNode, showPlusMinus); + const comparator = this.comparator; + + return `${leftSide} ${comparator} ${rightSide}`; + } + + clone() { + const newLeft = this.leftNode.cloneDeep(); + const newRight = this.rightNode.cloneDeep(); + return new Equation(newLeft, newRight, this.comparator); + } + + // Splits a string on the given comparator and returns a new Equation object + // from the left and right hand sides + static createEquationFromString(str, comparator) { + const sides = str.split(comparator); + if (sides.length !== 2) { + throw Error( + "Expected two sides of an equation using comparator: " + comparator + ); + } + const leftNode = math.parse(sides[0]); + const rightNode = math.parse(sides[1]); + + return new Equation(leftNode, rightNode, comparator); + } +} diff --git a/lib/src/equation/Status.ts b/lib/src/equation/Status.ts new file mode 100644 index 00000000..612e482f --- /dev/null +++ b/lib/src/equation/Status.ts @@ -0,0 +1,86 @@ +import { ChangeTypes } from "../ChangeTypes"; +import { NodeStatus } from "../node/NodeStatus"; +import { Equation } from "./Equation"; + +/** + * This represents the current equation we're solving. + * As we move step by step, an equation might be updated. Functions return this + * status object to pass on the updated equation and information on if/how it was + * changed. + * */ +export class EquationStatus { + get hasChanged() { + return this.changeType !== ChangeTypes.NO_CHANGE; + } + + constructor( + private changeType, + private oldEquation, + private newEquation, + private substeps = [] + ) { + if (!newEquation) { + throw Error("new equation isn't defined"); + } + if (changeType === undefined || typeof changeType !== "string") { + throw Error("changetype isn't valid"); + } + + this.changeType = changeType; + this.oldEquation = oldEquation; + this.newEquation = newEquation; + this.substeps = substeps; + } + + // A wrapper around the Status constructor for the case where equation + // hasn't been changed. + static noChange(equation) { + return new EquationStatus(ChangeTypes.NO_CHANGE, null, equation); + } + + static addLeftStep(equation, leftStep) { + const substeps = []; + leftStep.substeps.forEach((substep) => { + substeps.push(EquationStatus.addLeftStep(equation, substep)); + }); + let oldEquation = null; + if (leftStep.oldNode) { + oldEquation = equation.clone(); + oldEquation.leftNode = leftStep.oldNode; + } + const newEquation = equation.clone(); + newEquation.leftNode = leftStep.newNode; + return new EquationStatus( + leftStep.changeType, + oldEquation, + newEquation, + substeps + ); + } + + static addRightStep(equation, rightStep) { + const substeps = []; + rightStep.substeps.forEach((substep) => { + substeps.push(this.addRightStep(equation, substep)); + }); + let oldEquation = null; + if (rightStep.oldNode) { + oldEquation = equation.clone(); + oldEquation.rightNode = rightStep.oldNode; + } + const newEquation = equation.clone(); + newEquation.rightNode = rightStep.newNode; + return new EquationStatus( + rightStep.changeType, + oldEquation, + newEquation, + substeps + ); + } + + static resetChangeGroups(equation) { + const leftNode = NodeStatus.resetChangeGroups(equation.leftNode); + const rightNode = NodeStatus.resetChangeGroups(equation.rightNode); + return new Equation(leftNode, rightNode, equation.comparator); + } +} diff --git a/lib/src/factor/ConstantFactors.ts b/lib/src/factor/ConstantFactors.ts new file mode 100644 index 00000000..19963828 --- /dev/null +++ b/lib/src/factor/ConstantFactors.ts @@ -0,0 +1,61 @@ +/** + * This module deals with getting constant factors, including prime factors + * and factor pairs of a number + * */ +class ConstantFactorsImpl { + // Given a number, will return all the prime factors of that number as a list + // sorted from smallest to largest + getPrimeFactors(number) { + let factors = []; + if (number < 0) { + factors = [-1]; + factors = factors.concat(ConstantFactors.getPrimeFactors(-1 * number)); + return factors; + } + + const root = Math.sqrt(number); + let candidate = 2; + if (number % 2) { + candidate = 3; // assign first odd + while (number % candidate && candidate <= root) { + candidate = candidate + 2; + } + } + + // if no factor found then the number is prime + if (candidate > root) { + factors.push(number); + } + // if we find a factor, make a recursive call on the quotient of the number and + // our newly found prime factor in order to find more factors + else { + factors.push(candidate); + factors = factors.concat( + ConstantFactors.getPrimeFactors(number / candidate) + ); + } + + return factors; + } + + // Given a number, will return all the factor pairs for that number as a list + // of 2-item lists + getFactorPairs(number) { + const factors = []; + + const bound = Math.floor(Math.sqrt(Math.abs(number))); + for (var divisor = -bound; divisor <= bound; divisor++) { + if (divisor === 0) { + continue; + } + if (number % divisor === 0) { + const quotient = number / divisor; + factors.push([divisor, quotient]); + } + } + + return factors; + } +} + +export const ConstantFactors = new ConstantFactorsImpl(); diff --git a/lib/src/factor/FactorString.ts b/lib/src/factor/FactorString.ts new file mode 100644 index 00000000..11a4641a --- /dev/null +++ b/lib/src/factor/FactorString.ts @@ -0,0 +1,16 @@ +import * as math from "mathjs"; +import { stepThrough } from "./stepThrough"; +import { emptyResponse } from "../util/empty-response"; + +export function factorString(expressionString, debug = false) { + try { + const node = math.parse(expressionString); + if (node != null) { + return stepThrough(node, debug); + } else { + return emptyResponse(); + } + } catch (err) { + return emptyResponse(); + } +} diff --git a/lib/src/factor/factorQuadratic.ts b/lib/src/factor/factorQuadratic.ts new file mode 100644 index 00000000..84e68f5e --- /dev/null +++ b/lib/src/factor/factorQuadratic.ts @@ -0,0 +1,447 @@ +import * as math from "mathjs"; + +import { ConstantFactors } from "./ConstantFactors"; + +import { ChangeTypes } from "../ChangeTypes"; +import { evaluate } from "../util/evaluate"; +import { Negative } from "../Negative"; +import { NodeStatus } from "../node/NodeStatus"; +import { NodeType } from "../node/NodeType"; +import { PolynomialTerm } from "../node/PolynomialTerm"; +import { NodeCreator } from "../node/Creator"; + +const FACTOR_FUNCTIONS = [ + // factor just the symbol e.g. x^2 + 2x -> x(x + 2) + factorSymbol, + + // factor difference of squares e.g. x^2 - 4 + factorDifferenceOfSquares, + + // factor perfect square e.g. x^2 + 2x + 1 + factorPerfectSquare, + + // factor sum product rule e.g. x^2 + 3x + 2 + factorSumProductRule, +]; + +/** + * Given a node, will check if it's in the form of a quadratic equation + * `ax^2 + bx + c`, and + * if it is, will factor it using one of the following rules: + * - Factor out the symbol e.g. x^2 + 2x -> x(x + 2) + * - Difference of squares e.g. x^2 - 4 -> (x+2)(x-2) + * - Perfect square e.g. x^2 + 2x + 1 -> (x+1)^2 + * - Sum/product rule e.g. x^2 + 3x + 2 -> (x+1)(x+2) + * - TODO: quadratic formula + * requires us simplify the following only within the parens: + * a(x - (-b + sqrt(b^2 - 4ac)) / 2a)(x - (-b - sqrt(b^2 - 4ac)) / 2a) + * */ +export function factorQuadratic(node) { + // get a, b and c + let symbol, + aValue = 0, + bValue = 0, + cValue = 0; + for (const term of node.args) { + if (NodeType.isConstant(term)) { + cValue = evaluate(term); + } else if (PolynomialTerm.isPolynomialTerm(term)) { + const polyTerm = new PolynomialTerm(term); + const exponent = polyTerm.getExponentNode(true); + if (exponent.value === "2") { + symbol = polyTerm.getSymbolNode(); + aValue = polyTerm.getCoeffValue(); + } else if (exponent.value === "1") { + bValue = polyTerm.getCoeffValue(); + } else { + return NodeStatus.noChange(node); + } + } else { + return NodeStatus.noChange(node); + } + } + + if (!symbol || !aValue) { + return NodeStatus.noChange(node); + } + + let negate = false; + if (aValue < 0) { + negate = true; + aValue = -aValue; + bValue = -bValue; + cValue = -cValue; + } + + for (let i = 0; i < FACTOR_FUNCTIONS.length; i++) { + const nodeStatus = FACTOR_FUNCTIONS[i]( + node, + symbol, + aValue, + bValue, + cValue, + negate + ); + if (nodeStatus.hasChanged) { + return nodeStatus; + } + } + + return NodeStatus.noChange(node); +} + +// Will factor the node if it's in the form of ax^2 + bx +function factorSymbol(node, symbol, aValue, bValue, cValue, negate) { + if (!bValue || cValue) { + return NodeStatus.noChange(node); + } + + const gcd = math.gcd(aValue, bValue); + const gcdNode = NodeCreator.constant(gcd); + const aNode = NodeCreator.constant(aValue / gcd); + const bNode = NodeCreator.constant(bValue / gcd); + + const factoredNode = NodeCreator.polynomialTerm(symbol, null, gcdNode); + const polyTerm = NodeCreator.polynomialTerm(symbol, null, aNode); + const paren = NodeCreator.parenthesis( + NodeCreator.operator("+", [polyTerm, bNode]) + ); + + let newNode = NodeCreator.operator("*", [factoredNode, paren], true); + if (negate) { + newNode = Negative.negate(newNode); + } + + return NodeStatus.nodeChanged(ChangeTypes.FACTOR_SYMBOL, node, newNode); +} + +/** + * Will factor the node if it's in the form of ax^2 - c, and the aValue + * and cValue are perfect squares + * e.g. 4x^2 - 4 -> (2x + 2)(2x - 2) + * */ +function factorDifferenceOfSquares( + node, + symbol, + aValue, + bValue, + cValue, + negate +) { + // check if difference of squares: + // (i) abs(a) and abs(c) are squares, + // (ii) b = 0, + // (iii) c is negative + if (bValue || !cValue) { + return NodeStatus.noChange(node); + } + + // we factor out the gcd first, providing us with a modified expression to + // factor with new a and c values + const gcd = math.gcd(aValue, cValue); + aValue = aValue / gcd; + cValue = cValue / gcd; + const aRootValue = Math.sqrt(Math.abs(aValue)); + const cRootValue = Math.sqrt(Math.abs(cValue)); + + // must be a difference of squares + if ( + Number.isInteger(aRootValue) && + Number.isInteger(cRootValue) && + cValue < 0 + ) { + const aRootNode = NodeCreator.constant(aRootValue); + const cRootNode = NodeCreator.constant(cRootValue); + + const polyTerm = NodeCreator.polynomialTerm(symbol, null, aRootNode); + const firstParen = NodeCreator.parenthesis( + NodeCreator.operator("+", [polyTerm, cRootNode]) + ); + const secondParen = NodeCreator.parenthesis( + NodeCreator.operator("-", [polyTerm, cRootNode]) + ); + + // create node in difference of squares form + let newNode = NodeCreator.operator("*", [firstParen, secondParen], true); + if (gcd !== 1) { + const gcdNode = NodeCreator.constant(gcd); + newNode = NodeCreator.operator("*", [gcdNode, newNode], true); + } + if (negate) { + newNode = Negative.negate(newNode); + } + + return NodeStatus.nodeChanged( + ChangeTypes.FACTOR_DIFFERENCE_OF_SQUARES, + node, + newNode + ); + } + + return NodeStatus.noChange(node); +} + +/** + * Will factor the node if it's in the form of ax^2 + bx + c, where a and c + * are perfect squares and b = 2*sqrt(a)*sqrt(c) + * e.g. x^2 + 2x + 1 -> (x + 1)^2 + * */ +function factorPerfectSquare(node, symbol, aValue, bValue, cValue, negate) { + // check if perfect square: (i) a and c squares, (ii) b = 2*sqrt(a)*sqrt(c) + if (!bValue || !cValue) { + return NodeStatus.noChange(node); + } + + // we factor out the gcd first, providing us with a modified expression to + // factor with new a and c values + const gcd = math.gcd(aValue, bValue, cValue); + aValue = aValue / gcd; + cValue = cValue / gcd; + const aRootValue = Math.sqrt(Math.abs(aValue)); + let cRootValue = Math.sqrt(Math.abs(cValue)); + + // if the second term is negative, then the constant in the parens is + // subtracted: e.g. x^2 - 2x + 1 -> (x - 1)^2 + if (bValue < 0) { + cRootValue = cRootValue * -1; + } + + // apply the perfect square test + const perfectProduct = 2 * aRootValue * cRootValue; + if ( + Number.isInteger(aRootValue) && + Number.isInteger(cRootValue) && + bValue / gcd === perfectProduct + ) { + const aRootNode = NodeCreator.constant(aRootValue); + const cRootNode = NodeCreator.constant(cRootValue); + + const polyTerm = NodeCreator.polynomialTerm(symbol, null, aRootNode); + const paren = NodeCreator.parenthesis( + NodeCreator.operator("+", [polyTerm, cRootNode]) + ); + const exponent = NodeCreator.constant(2); + + // create node in perfect square form + let newNode = NodeCreator.operator("^", [paren, exponent]); + if (gcd !== 1) { + const gcdNode = NodeCreator.constant(gcd); + newNode = NodeCreator.operator("*", [gcdNode, newNode], true); + } + if (negate) { + newNode = Negative.negate(newNode); + } + + return NodeStatus.nodeChanged( + ChangeTypes.FACTOR_PERFECT_SQUARE, + node, + newNode + ); + } + + return NodeStatus.noChange(node); +} + +/** + * Will factor the node if it's in the form of ax^2 + bx + c, by + * applying the sum product rule: finding factors of a*c that add up to b. + * e.g. x^2 + 3x + 2 -> (x + 1)(x + 2) or + * or 2x^2 + 5x + 3 -> (2x - 1)(x + 3) + * */ +function factorSumProductRule(node, symbol, aValue, bValue, cValue, negate) { + let newNode; + + if (bValue && cValue) { + // we factor out the gcd first, providing us with a modified expression to + // factor with new a, b and c values + const gcd = math.gcd(aValue, bValue, cValue); + const gcdNode = NodeCreator.constant(gcd); + + aValue = aValue / gcd; + bValue = bValue / gcd; + cValue = cValue / gcd; + + // try sum/product rule: find a factor pair of a*c that adds up to b + const product = aValue * cValue; + const factorPairs = ConstantFactors.getFactorPairs(product); + for (const pair of factorPairs) { + if (pair[0] + pair[1] === bValue) { + // To factor, we go through some transformations + // 1. Break apart the middle term into two terms using our factor pair + // (p and q): e.g. ax^2 + bx + c -> ax^2 + px + qx + c + // 2. Consider the first two terms together and the second two terms + // together (this doesn't require any actual change to the expression) + // e.g. first group: [ax^2 + px] and second group: [qx + c] + // 3. Factor both groups separately + // e.g first group: [ux(rx + s)] and second group [v(rx + s)] + // 4. Finish factoring by combining the factored terms through grouping: + // e.g. (ux + v)(rx + s) + const substeps = []; + let status; + + const a = NodeCreator.constant(aValue); + const b = NodeCreator.constant(bValue); + const c = NodeCreator.constant(cValue); + const ax2 = NodeCreator.polynomialTerm( + symbol, + NodeCreator.constant(2), + a + ); + const bx = NodeCreator.polynomialTerm(symbol, null, b); + + // OPTIONAL SUBSTEP (this happens iff a is negative) + // ax^2 + bx + c -> -(-ax^2 - bx - c) + if (negate) { + newNode = NodeCreator.operator("+", [ax2, bx, c], true); + newNode = Negative.negate(newNode); + status = NodeStatus.nodeChanged( + ChangeTypes.REARRANGE_COEFF, + node, + newNode + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + } + + // SUBSTEP 1: ax^2 + bx + c -> ax^2 + px + qx + c + const pValue = pair[0]; + const qValue = pair[1]; + const p = NodeCreator.constant(pValue); + const q = NodeCreator.constant(qValue); + const px = NodeCreator.polynomialTerm(symbol, null, p); + const qx = NodeCreator.polynomialTerm(symbol, null, q); + + newNode = NodeCreator.operator("+", [ax2, px, qx, c], true); + if (negate) { + newNode = Negative.negate(newNode); + } + status = NodeStatus.nodeChanged( + ChangeTypes.BREAK_UP_TERM, + node, + newNode + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // STEP 2: ax^2 + px + qx + c -> (ax^2 + px) + (qx + c) + const firstTerm = NodeCreator.parenthesis( + NodeCreator.operator("+", [ax2, px]) + ); + const secondTerm = NodeCreator.parenthesis( + NodeCreator.operator("+", [qx, c]) + ); + + newNode = NodeCreator.operator("+", [firstTerm, secondTerm], true); + if (negate) { + newNode = Negative.negate(newNode); + } + status = NodeStatus.nodeChanged( + ChangeTypes.COLLECT_LIKE_TERMS, + node, + newNode + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // SUBSTEP 3A: (ax^2 + px) + (qx + c) -> ux(rx + s) + (qx + c) + const u = NodeCreator.constant(math.gcd(aValue, pValue)); + const r = NodeCreator.constant(aValue / u); + const s = NodeCreator.constant(pValue / u); + const ux = NodeCreator.polynomialTerm(symbol, null, u); + + // create the first group's part that's in parentheses: (rx + s) + const rx = NodeCreator.polynomialTerm(symbol, null, r); + const firstParen = NodeCreator.parenthesis( + NodeCreator.operator("+", [rx, s]) + ); + + const firstFactoredGroup = NodeCreator.operator( + "*", + [ux, firstParen], + true + ); + newNode = NodeCreator.operator( + "+", + [firstFactoredGroup, secondTerm], + true + ); + if (negate) { + newNode = Negative.negate(newNode); + } + status = NodeStatus.nodeChanged( + ChangeTypes.FACTOR_SYMBOL, + node, + newNode + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // STEP 3B: ux(rx + s) + (qx + c) -> ux(rx + s) + v(rx + s) + let vValue = math.gcd(cValue, qValue); + if (qValue < 0) { + vValue = vValue * -1; + } + const v = NodeCreator.constant(vValue); + + // create the second parenthesis + const secondParen = NodeCreator.parenthesis( + NodeCreator.operator("+", [ux, v]) + ); + + const secondFactoredGroup = NodeCreator.operator( + "*", + [v, firstParen], + true + ); + newNode = NodeCreator.operator( + "+", + [firstFactoredGroup, secondFactoredGroup], + true + ); + if (negate) { + newNode = Negative.negate(newNode); + } + status = NodeStatus.nodeChanged( + ChangeTypes.FACTOR_SYMBOL, + node, + newNode + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // STEP 4: ux(rx + s) + v(rx + s) -> (ux + v)(rx + s) + if (gcd === 1) { + newNode = NodeCreator.operator("*", [firstParen, secondParen], true); + } else { + newNode = NodeCreator.operator( + "*", + [gcdNode, firstParen, secondParen], + true + ); + } + + if (negate) { + newNode = Negative.negate(newNode); + } + + status = NodeStatus.nodeChanged( + ChangeTypes.FACTOR_SUM_PRODUCT_RULE, + node, + newNode + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + return NodeStatus.nodeChanged( + ChangeTypes.FACTOR_SUM_PRODUCT_RULE, + node, + newNode, + true, + substeps + ); + } + } + } + + return NodeStatus.noChange(node); +} diff --git a/lib/src/factor/stepThrough.ts b/lib/src/factor/stepThrough.ts new file mode 100644 index 00000000..b5a3e2c2 --- /dev/null +++ b/lib/src/factor/stepThrough.ts @@ -0,0 +1,36 @@ +import { factorQuadratic } from "./factorQuadratic"; + +import { flattenOperands } from "../util/flattenOperands"; +import { removeUnnecessaryParens } from "../util/removeUnnecessaryParens"; +import { printAscii } from "../util/print"; +import { hasUnsupportedNodes } from "../checks/hasUnsupportedNodes"; +import { isQuadratic } from "../checks/isQuadratic"; + +/** + * Given a mathjs expression node, steps through factoring the expression. + * Currently only supports factoring quadratics. + * Returns a list of details about each step. + * */ +export function stepThrough(node, debug = false) { + if (debug) { + console.log("\n\nFactoring: " + printAscii(node, false)); + } + + if (hasUnsupportedNodes(node)) { + return []; + } + + const steps = []; + + node = flattenOperands(node); + node = removeUnnecessaryParens(node, true); + if (isQuadratic(node)) { + const nodeStatus = factorQuadratic(node); + if (nodeStatus.hasChanged) { + steps.push(nodeStatus); + } + } + // Add factoring higher order polynomials... + + return steps; +} diff --git a/lib/src/index.ts b/lib/src/index.ts new file mode 100644 index 00000000..5d1e7516 --- /dev/null +++ b/lib/src/index.ts @@ -0,0 +1,2 @@ +export { solveEquation } from "./solveEquation"; +export { simplifyExpression } from "./simplifyExpression"; diff --git a/lib/src/migrate-to-ts.sh b/lib/src/migrate-to-ts.sh new file mode 100644 index 00000000..2d6887ef --- /dev/null +++ b/lib/src/migrate-to-ts.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +OLD_EXTENSION="js" +NEW_EXTENSION="ts" + +for i in $(find `pwd` -name "*.${OLD_EXTENSION}"); +do + mv "$i" "${i%.$OLD_EXTENSION}.${NEW_EXTENSION}" +done diff --git a/lib/src/node/Creator.ts b/lib/src/node/Creator.ts new file mode 100644 index 00000000..4d8314d5 --- /dev/null +++ b/lib/src/node/Creator.ts @@ -0,0 +1,102 @@ +/** + * Functions to generate any mathJS node supported by the stepper + * see http://mathjs.org/docs/expressions/expression_trees.html#nodes for more + * information on nodes in mathJS + * */ + +import * as math from "mathjs"; +import { NodeType } from "./NodeType"; + +class NodeCreatorClass { + operator(op, args, implicit = false) { + switch (op) { + case "+": + return new (math.expression as any).node.OperatorNode("+", "add", args); + case "-": + return new (math.expression as any).node.OperatorNode( + "-", + "subtract", + args + ); + case "/": + return new (math.expression as any).node.OperatorNode( + "/", + "divide", + args + ); + case "*": + return new (math.expression as any).node.OperatorNode( + "*", + "multiply", + args, + implicit + ); + case "^": + return new (math.expression as any).node.OperatorNode("^", "pow", args); + default: + throw Error("Unsupported operation: " + op); + } + } + + // In almost all cases, use Negative.negate (with naive = true) to add a + // unary minus to your node, rather than calling this constructor directly + unaryMinus(content) { + return new (math.expression as any).node.OperatorNode("-", "unaryMinus", [ + content, + ]); + } + + constant(val) { + return new (math.expression as any).node.ConstantNode(val); + } + + symbol(name) { + return new (math.expression as any).node.SymbolNode(name); + } + + parenthesis(content) { + return new (math.expression as any).node.ParenthesisNode(content); + } + + list(content) { + return new (math.expression as any).node.ArrayNode(content); + } + + // exponent might be null, which means there's no exponent node. + // similarly, coefficient might be null, which means there's no coefficient + // the base node can never be null. + term(base, exponent, coeff, explicitCoeff = false) { + let term = base; + if (exponent) { + term = this.operator("^", [term, exponent]); + } + if (coeff && (explicitCoeff || parseFloat(coeff.value) !== 1)) { + if ( + NodeType.isConstant(coeff) && + parseFloat(coeff.value) === -1 && + !explicitCoeff + ) { + // if you actually want -1 as the coefficient, set explicitCoeff to true + term = this.unaryMinus(term); + } else { + term = this.operator("*", [coeff, term], true); + } + } + return term; + } + + polynomialTerm(symbol, exponent, coeff, explicitCoeff = false) { + return this.term(symbol, exponent, coeff, explicitCoeff); + } + + // Given a root value and a radicand (what is under the radical) + nthRoot(radicandNode, rootNode) { + const symbol = NodeCreator.symbol("nthRoot"); + return new (math.expression as any).node.FunctionNode(symbol, [ + radicandNode, + rootNode, + ]); + } +} + +export const NodeCreator = new NodeCreatorClass(); diff --git a/lib/src/node/CustomType.ts b/lib/src/node/CustomType.ts new file mode 100644 index 00000000..f866734a --- /dev/null +++ b/lib/src/node/CustomType.ts @@ -0,0 +1,114 @@ +import { Negative } from "../Negative"; +import { NodeCreator } from "./Creator"; +import { NodeType } from "./NodeType"; +import { MathNode } from "mathjs"; + +class NodeCustomTypeImpl { + /** + * Returns true if `node` belongs to the type specified by boolean `isTypeFunc`. + * If `allowUnaryMinus/allowParens` is true, we allow for the node to be nested. + * */ + isType( + node: MathNode, + isTypeFunc, + allowUnaryMinus = true, + allowParens = true + ) { + if (isTypeFunc(node)) { + return true; + } else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { + return this.isType( + node.args[0], + isTypeFunc, + allowUnaryMinus, + allowParens + ); + } else if (allowParens && NodeType.isParenthesis(node)) { + return this.isType( + node.content, + isTypeFunc, + allowUnaryMinus, + allowParens + ); + } + + return false; + } + + /** + * Returns `node` if `node` belongs to the type specified by boolean `isTypeFunc`. + * If `allowUnaryMinus/allowParens` is true, we check for an inner node of this type. + * `moveUnaryMinus` should be defined if `allowUnaryMinus` is true, and should + * move the unaryMinus into the inside of the type + * e.g. for fractions, this function will negate the numerator + * */ + getType( + node: MathNode, + isTypeFunc, + allowUnaryMinus = true, + allowParens = true, + moveUnaryMinus = undefined + ): MathNode { + if (allowUnaryMinus === true && moveUnaryMinus === undefined) { + throw Error("Error in `getType`: moveUnaryMinus is undefined"); + } + + if (isTypeFunc(node)) { + return node; + } else if (allowUnaryMinus && NodeType.isUnaryMinus(node)) { + return moveUnaryMinus( + this.getType( + node.args[0], + isTypeFunc, + allowUnaryMinus, + allowParens, + moveUnaryMinus + ) + ); + } else if (allowParens && NodeType.isParenthesis(node)) { + return this.getType( + node.content, + isTypeFunc, + allowUnaryMinus, + allowParens, + moveUnaryMinus + ); + } + + throw Error( + "`getType` called on a node that does not belong to specified type" + ); + } + + isFraction(node: MathNode, allowUnaryMinus = true, allowParens = true) { + return this.isType( + node, + (node) => NodeType.isOperator(node, "/"), + allowUnaryMinus, + allowParens + ); + } + + getFraction(node: MathNode, allowUnaryMinus = true, allowParens = true) { + const moveUnaryMinus = function (node) { + if (!NodeType.isOperator(node, "/")) { + throw Error("Expected a fraction"); + } + + const numerator = node.args[0]; + const denominator = node.args[1]; + const newNumerator = Negative.negate(numerator); + return NodeCreator.operator("/", [newNumerator, denominator]); + }; + + return this.getType( + node, + (node) => NodeType.isOperator(node, "/"), + allowParens, + allowUnaryMinus, + moveUnaryMinus + ); + } +} + +export const NodeCustomType = new NodeCustomTypeImpl(); diff --git a/lib/src/node/MixedNumber.ts b/lib/src/node/MixedNumber.ts new file mode 100644 index 00000000..3f487200 --- /dev/null +++ b/lib/src/node/MixedNumber.ts @@ -0,0 +1,128 @@ +import { Negative } from "../Negative"; +import { NodeType } from "./NodeType"; +import { MathNode } from "mathjs"; + +export class NodeMixedNumberImpl { + /** + * Returns true if `node` is a mixed number + * e.g. 2 1/2, 19 2/3 + * Right now mathjs cannot parse the above examples; + * instead it expects the input to look like e.g. 2(1)/(2), + * which is division with implicit multiplication in the numerator + * TODO: Add better support for mixed numbers in the future + * */ + isMixedNumber(node: MathNode) { + if (!NodeType.isOperator(node, "/")) { + return false; + } + + if (node.args.length !== 2) { + return false; + } + + const numerator = node.args[0]; + const denominator = node.args[1]; + + // check for implicit multiplication between two constants in the numerator + // first can be wrapped in unary minus + // second one can be optionally wrapped in parenthesis + if (!(NodeType.isOperator(numerator, "*") && (numerator as any).implicit)) { + return false; + } + + const numeratorFirstArg = NodeType.isUnaryMinus(numerator.args[0]) + ? Negative.negate(numerator.args[0].args[0]) + : numerator.args[0]; + + const numeratorSecondArg = NodeType.isParenthesis(numerator.args[1]) + ? numerator.args[1].content + : numerator.args[1]; + + if ( + !( + NodeType.isConstant(numeratorFirstArg) && + NodeType.isConstant(numeratorSecondArg) + ) + ) { + return false; + } + + // check for a constant in the denominator, + // optionally wrapped in parenthesis + const denominatorValue = NodeType.isParenthesis(denominator) + ? denominator.content + : denominator; + + if (!NodeType.isConstant(denominatorValue)) { + return false; + } + + return true; + } + + /** + * Returns true if the mixed number is negative, + * in which case we have to ignore the negative while converting to an + * improper fraction, and instead we negate the whole thing at the end + * e.g. -1 2/3 !== ((-1 * 3) + 2)/3 = -1/3 + * -1 2/3 == -((1 * 3) + 2)/3 = -5/2 + * */ + isNegativeMixedNumber = (node: MathNode) => { + if (!this.isMixedNumber(node)) { + throw Error("Expected a mixed number"); + } + + return NodeType.isUnaryMinus(node.args[0].args[0]); + }; + + /** + * Get the whole number part of a mixed number + * e.g. 1 2/3 -> 1 + * Negatives are ignored; e.g. -1 2/3 -> 1 + * */ + getWholeNumberValue = (node: MathNode) => { + if (!this.isMixedNumber(node)) { + throw Error("Expected a mixed number"); + } + + const wholeNumberNode = NodeType.isUnaryMinus(node.args[0].args[0]) + ? node.args[0].args[0].args[0] + : node.args[0].args[0]; + + return parseInt(wholeNumberNode.value); + }; + + /** + * Get the numerator part of a mixed number + * e.g. 1 2/3 -> 2 + * */ + getNumeratorValue = (node: MathNode) => { + if (!this.isMixedNumber(node)) { + throw Error("Expected a mixed number"); + } + + const numeratorNode = NodeType.isParenthesis(node.args[0].args[1]) + ? node.args[0].args[1].content + : node.args[0].args[1]; + + return parseInt(numeratorNode.value); + }; + + /** + * Get the denominator part of a mixed number + * e.g. 1 2/3 -> 3 + * */ + getDenominatorValue = (node: MathNode) => { + if (!this.isMixedNumber(node)) { + throw Error("Expected a mixed number"); + } + + const denominatorNode = NodeType.isParenthesis(node.args[1]) + ? node.args[1].content + : node.args[1]; + + return parseInt(denominatorNode.value); + }; +} + +export const NodeMixedNumber = new NodeMixedNumberImpl(); diff --git a/lib/src/node/NodeStatus.ts b/lib/src/node/NodeStatus.ts new file mode 100644 index 00000000..17ae90c3 --- /dev/null +++ b/lib/src/node/NodeStatus.ts @@ -0,0 +1,144 @@ +import { ChangeTypes } from "../ChangeTypes"; +import { NodeType } from "./NodeType"; +import { MathNode } from "mathjs"; + +/** + * This represents the current (sub)expression we're simplifying. + * As we move step by step, a node might be updated. Functions return this + * status object to pass on the updated node and information on if/how it was + * changed. + * Status(node) creates a Status object that signals no change + * */ +export class NodeStatus { + get hasChanged() { + return this.changeType !== ChangeTypes.NO_CHANGE; + } + + constructor( + public changeType, + private oldNode, + public newNode, + public substeps = [] + ) { + if (!newNode) { + throw Error("node is not defined"); + } + if (changeType === undefined || typeof changeType !== "string") { + throw Error("changetype isn't valid"); + } + + this.changeType = changeType; + this.oldNode = oldNode; + this.newNode = newNode; + this.substeps = substeps; + } + + static resetChangeGroups(node) { + node = node.cloneDeep(); + node + .filter((node) => node.changeGroup) + .forEach((change) => { + delete change.changeGroup; + }); + return node; + } + + /** + * A wrapper around the Status constructor for the case where node hasn't + * been changed. + * */ + static noChange(node: MathNode) { + return new NodeStatus(ChangeTypes.NO_CHANGE, null, node); + } + + /** + * A wrapper around the Status constructor for the case of a change + * that is happening at the level of oldNode + newNode + * e.g. 2 + 2 --> 4 (an addition node becomes a constant node) + * */ + static nodeChanged( + changeType, + oldNode, + newNode, + defaultChangeGroup = true, + steps = [] + ) { + if (defaultChangeGroup) { + oldNode.changeGroup = 1; + newNode.changeGroup = 1; + } + + return new NodeStatus(changeType, oldNode, newNode, steps); + } + + /** + * A wrapper around the Status constructor for the case where there was + * a change that happened deeper `node`'s tree, and `node`'s children must be + * updated to have the newNode/oldNode metadata (changeGroups) + * e.g. (2 + 2) + x --> 4 + x has to update the left argument + * */ + static childChanged(node, childStatus, childArgIndex = null) { + const oldNode = node.cloneDeep(); + const newNode = node.cloneDeep(); + let substeps = childStatus.substeps; + + if (!childStatus.oldNode) { + throw Error( + "Expected old node for changeType: " + childStatus.changeType + ); + } + + function updateSubsteps(substeps, fn) { + substeps.map((step) => { + step = fn(step); + step.substeps = updateSubsteps(step.substeps, fn); + }); + return substeps; + } + + if (NodeType.isParenthesis(node)) { + oldNode.content = childStatus.oldNode; + newNode.content = childStatus.newNode; + substeps = updateSubsteps(substeps, (step) => { + const oldNode = node.cloneDeep(); + const newNode = node.cloneDeep(); + oldNode.content = step.oldNode; + newNode.content = step.newNode; + step.oldNode = oldNode; + step.newNode = newNode; + return step; + }); + } else if ( + NodeType.isOperator(node) || + (NodeType.isFunction(node) && childArgIndex !== null) + ) { + oldNode.args[childArgIndex] = childStatus.oldNode; + newNode.args[childArgIndex] = childStatus.newNode; + substeps = updateSubsteps(substeps, (step) => { + const oldNode = node.cloneDeep(); + const newNode = node.cloneDeep(); + oldNode.args[childArgIndex] = step.oldNode; + newNode.args[childArgIndex] = step.newNode; + step.oldNode = oldNode; + step.newNode = newNode; + return step; + }); + } else if (NodeType.isUnaryMinus(node)) { + oldNode.args[0] = childStatus.oldNode; + newNode.args[0] = childStatus.newNode; + substeps = updateSubsteps(substeps, (step) => { + const oldNode = node.cloneDeep(); + const newNode = node.cloneDeep(); + oldNode.args[0] = step.oldNode; + newNode.args[0] = step.newNode; + step.oldNode = oldNode; + step.newNode = newNode; + return step; + }); + } else { + throw Error("Unexpected node type: " + NodeType); + } + + return new NodeStatus(childStatus.changeType, oldNode, newNode, substeps); + } +} diff --git a/lib/src/node/NodeType.ts b/lib/src/node/NodeType.ts new file mode 100644 index 00000000..e7d6dc9a --- /dev/null +++ b/lib/src/node/NodeType.ts @@ -0,0 +1,116 @@ +/** + * For determining the type of a mathJS node. + * */ + +import { MathNode } from "mathjs"; + +class NodeTypeImpl { + isOperator(node: MathNode, operator = null) { + return ( + node.type === NodeTypeEnum.OPERATOR_NODE && + node.fn !== "unaryMinus" && + "*+-/^".includes(node.op) && + (operator ? node.op === operator : true) + ); + } + + isParenthesis(node: MathNode) { + return node.type === NodeTypeEnum.PARENTHESIS_NODE; + } + + /** + * OPTIMIZE: it is a mess that this method is duplicated for static and non static use + * */ + isUnaryMinus = (node) => { + return node.type === NodeTypeEnum.OPERATOR_NODE && node.fn === "unaryMinus"; + }; + + static isUnaryMinus = (node) => { + return node.type === NodeTypeEnum.OPERATOR_NODE && node.fn === "unaryMinus"; + }; + + isFunction(node, functionName = null) { + if (node.type !== NodeTypeEnum.FUNCTION_NODE) { + return false; + } + if (functionName && node.fn.name !== functionName) { + return false; + } + return true; + } + + isSymbol(node, allowUnaryMinus = false) { + if (node.type === NodeTypeEnum.SYMBOL_NODE) { + return true; + } else if (allowUnaryMinus && NodeTypeImpl.isUnaryMinus(node)) { + return this.isSymbol(node.args[0], false); + } else { + return false; + } + } + + isConstant = (node, allowUnaryMinus = false) => { + if (node.type === NodeTypeEnum.CONSTANT_NODE) { + return true; + } else if (allowUnaryMinus && this.isUnaryMinus(node)) { + if (this.isConstant(node.args[0], false)) { + const value = parseFloat(node.args[0].value); + return value >= 0; + } else { + return false; + } + } else { + return false; + } + }; + + isConstantFraction(node, allowUnaryMinus = false) { + if (this.isOperator(node, "/")) { + return node.args.every((n) => this.isConstant(n, allowUnaryMinus)); + } else { + return false; + } + } + + isConstantOrConstantFraction(node, allowUnaryMinus = false) { + if ( + this.isConstant(node, allowUnaryMinus) || + this.isConstantFraction(node, allowUnaryMinus) + ) { + return true; + } else { + return false; + } + } + + isIntegerFraction = (node, allowUnaryMinus = false) => { + if (!this.isConstantFraction(node, allowUnaryMinus)) { + return false; + } + let numerator = node.args[0]; + let denominator = node.args[1]; + if (allowUnaryMinus) { + if (this.isUnaryMinus(numerator)) { + numerator = numerator.args[0]; + } + if (this.isUnaryMinus(denominator)) { + denominator = denominator.args[0]; + } + } + return ( + Number.isInteger(parseFloat(numerator.value)) && + Number.isInteger(parseFloat(denominator.value)) + ); + }; +} + +export const NodeType = new NodeTypeImpl(); + +export enum NodeTypeEnum { + CONSTANT_NODE = "ConstantNode", + SYMBOL_NODE = "SymbolNode", + FUNCTION_NODE = "FunctionNode", + OPERATOR_NODE = "OperatorNode", + PARENTHESIS_NODE = "ParenthesisNode", + FRACTION_NODE = "fraction", +} diff --git a/lib/src/node/NthRootTerm.ts b/lib/src/node/NthRootTerm.ts new file mode 100644 index 00000000..c7652170 --- /dev/null +++ b/lib/src/node/NthRootTerm.ts @@ -0,0 +1,33 @@ +import { NodeType } from "./NodeType"; +import { Term } from "./Term"; + +/** + * For storing nth root terms, which are a subclass of Term + * where the base node is an nth root + * */ +export class NthRootTerm extends Term { + constructor(node, onlyImplicitMultiplication = false) { + super(node, NthRootTerm.baseNodeFunc, onlyImplicitMultiplication); + } + + /** + * Returns true if the term has a base node that makes it an nth root term + * e.g. 4x^2 has a base of x, so it is not an nth root term + * 4*sqrt(x)^2 has a base of sqrt(x), so it is an nth root term + * */ + static baseNodeFunc(node) { + return NodeType.isFunction(node, "nthRoot"); + } + + /** + * Returns true if the node represents an nth root term. + * e.g. nthRoot(4), nthRoot(x^2), 4*nthRoot(10)^2 + * */ + static isNthRootTerm(node, onlyImplicitMultiplication = false) { + return Term.isTerm( + node, + NthRootTerm.baseNodeFunc, + onlyImplicitMultiplication + ); + } +} diff --git a/lib/src/node/PolynomialTerm.ts b/lib/src/node/PolynomialTerm.ts new file mode 100644 index 00000000..55b1dcbf --- /dev/null +++ b/lib/src/node/PolynomialTerm.ts @@ -0,0 +1,43 @@ +import { NodeType } from "./NodeType"; +import { Term } from "./Term"; + +/** + * For storing polynomial terms, which are a subclass of Term + * where the base node is a symbol + * */ +export class PolynomialTerm extends Term { + constructor(node, onlyImplicitMultiplication = false) { + super(node, PolynomialTerm.baseNodeFunc, onlyImplicitMultiplication); + } + + getSymbolNode() { + return this.base; + } + + getSymbolName() { + return this.base.name; + } + + /** + * Returns true if the term has a base node that makes it a polynomial term + * e.g. 4x^2 has a base of x, so it is a polynomial + * 4*sqrt(x)^2 has a base of sqrt(x), so it is not + */ + static baseNodeFunc(node) { + return NodeType.isSymbol(node); + } + + /** + * Returns true if the node is a polynomial term. + * e.g. x^2, 2y, z, 3x/5 are all polynomial terms. + * 4, 2+x, 3*7, x-z are all not polynomial terms. + * See the tests for some more thorough examples. + */ + static isPolynomialTerm(node, onlyImplicitMultiplication = false) { + return Term.isTerm( + node, + PolynomialTerm.baseNodeFunc, + onlyImplicitMultiplication + ); + } +} diff --git a/lib/src/node/Term.ts b/lib/src/node/Term.ts new file mode 100644 index 00000000..9d07e1bd --- /dev/null +++ b/lib/src/node/Term.ts @@ -0,0 +1,225 @@ +import { NodeCreator } from "./Creator"; +import { NodeType } from "./NodeType"; + +import { evaluate } from "../util/evaluate"; + +/** + * For storing term that have a base node, maybe an exponent, + * and maybe a coefficient. + * These expressions are Terms: + * -- x^2, 2y, z, 3x/5 (PolynomialTerm) + * -- nthRoot(4), 5*nthRoot(x) (NthRootTerm) + * These expressions are not: 4, x^(3+4), 2+x, 3*7, x-z + * Fields: + * - coeff: either a constant node or a fraction of two constant nodes + * (might be null if no coefficient) + * - base: the base node (e.g. in x^2, the node x) + * - exponent: a node that can take any form, e.g. x^(2+x^2) + * (might be null if no exponent) + */ +export class Term { + base: any; + exponent: any; + coeff: any; + + /** + * Params: + * -- node: The node from which to construct the Term + * -- baseNodeFunc: A boolean function returning true if the base node + * is of the right type + * e.g., for PolynomialTerms, baseNodeFunc checks if the base is a symbol + * for NthRootTerms, baseNodeFunc checks if the base node is an nth root + * -- onlyImplicitMultiplication: If onlyImplicitMultiplication is true, + * we throw an error if `node` is a term without implicit multiplication + * (i.e. 2*x instead of 2x) and therefore isTerm will return false. + * */ + constructor(node, baseNodeFunc, onlyImplicitMultiplication = false) { + const values = Term.parseNode( + node, + baseNodeFunc, + onlyImplicitMultiplication + ); + this.base = values.base; + this.exponent = values.exponent; + this.coeff = values.coeff; + } + + getBaseNode() { + return this.base; + } + + getCoeffNode(defaultOne = false) { + if (!this.coeff && defaultOne) { + return NodeCreator.constant(1); + } else { + return this.coeff; + } + } + + getCoeffValue() { + if (this.coeff) { + return evaluate(this.coeff); + } else { + return 1; // no coefficient is like a coeff of 1 + } + } + + getExponentNode(defaultOne = false) { + if (!this.exponent && defaultOne) { + return NodeCreator.constant(1); + } else { + return this.exponent; + } + } + + /** + * note: there is no exponent value getter function because the exponent + * can be any expression and not necessarily a number. + * + * CHECKER FUNCTIONS (returns true / false for certain conditions) + * + * Returns true if the coefficient is a fraction + * */ + hasFractionCoeff() { + // coeffNode is either a constant or a division operation. + return this.coeff && NodeType.isOperator(this.coeff); + } + + hasCoeff() { + return !!this.coeff; + } + + /** + * Returns if the node represents an expression that can be considered + * a term with a coefficient. + * */ + static isTerm(node, baseNodeFunc, onlyImplicitMultiplication = false) { + try { + // will throw error if node isn't term with coefficient + new Term(node, baseNodeFunc, onlyImplicitMultiplication); + return true; + } catch (err) { + return false; + } + } + + static parseNode(node, baseNodeFunc, onlyImplicitMultiplication) { + let base, exponent, coeff; + if (NodeType.isOperator(node)) { + if (node.op === "^") { + const baseNode = node.args[0]; + if (!baseNodeFunc(baseNode)) { + throw Error("Expected base term, got " + baseNode); + } + base = baseNode; + exponent = node.args[1]; + } + // it's '*' ie it has a coefficient + else if (node.op === "*") { + if (onlyImplicitMultiplication && !node.implicit) { + throw Error("Expected implicit multiplication"); + } + if (node.args.length !== 2) { + throw Error("Expected two arguments to *"); + } + const coeffNode = node.args[0]; + if (!NodeType.isConstantOrConstantFraction(coeffNode)) { + throw Error( + "Expected coefficient to be constant or fraction of " + + "constants term, got " + + coeffNode + ); + } + coeff = coeffNode; + const nonCoefficientTerm = new Term( + node.args[1], + baseNodeFunc, + onlyImplicitMultiplication + ); + if (nonCoefficientTerm.hasCoeff()) { + throw Error( + "Cannot have two coefficients " + + coeffNode + + " and " + + nonCoefficientTerm.getCoeffNode() + ); + } + base = nonCoefficientTerm.getBaseNode(); + exponent = nonCoefficientTerm.getExponentNode(); + } + // this means there's a fraction coefficient + else if (node.op === "/") { + const denominatorNode = node.args[1]; + if (!NodeType.isConstant(denominatorNode)) { + throw Error( + "denominator must be constant node, instead of " + denominatorNode + ); + } + const numeratorNode = new Term( + node.args[0], + baseNodeFunc, + onlyImplicitMultiplication + ); + if (numeratorNode.hasFractionCoeff()) { + throw Error("Terms with coefficients cannot have nested fractions"); + } + exponent = numeratorNode.getExponentNode(); + base = numeratorNode.getBaseNode(); + const numeratorConstantNode = numeratorNode.getCoeffNode(true); + coeff = NodeCreator.operator("/", [ + numeratorConstantNode, + denominatorNode, + ]); + } else { + throw Error( + "Unsupported operatation for term with coefficent: " + node.op + ); + } + } else if (NodeType.isUnaryMinus(node)) { + var arg = node.args[0]; + if (NodeType.isParenthesis(arg)) { + arg = arg.content; + } + const termNode = new Term(arg, baseNodeFunc, onlyImplicitMultiplication); + exponent = termNode.getExponentNode(); + base = termNode.getBaseNode(); + if (!termNode.hasCoeff()) { + coeff = NodeCreator.constant(-1); + } else { + coeff = negativeCoefficient(termNode.getCoeffNode()); + } + } else if (baseNodeFunc(node)) { + base = node; + } else if (NodeType.isParenthesis(node)) { + return this.parseNode( + node.content, + baseNodeFunc, + onlyImplicitMultiplication + ); + } else { + throw Error("Unsupported node type: " + node); + } + + return { + base, + exponent, + coeff, + }; + } +} + +/** + * Multiplies `node`, a constant or fraction of two constant nodes, by -1 + * Returns a node + * */ +function negativeCoefficient(node) { + if (NodeType.isConstant(node)) { + // Node is a constant + node = NodeCreator.constant(0 - parseFloat(node.value)); + } else { + // Node is a constant fraction + const numeratorValue = 0 - parseFloat(node.args[0].value); + node.args[0] = NodeCreator.constant(numeratorValue); + } + return node; +} diff --git a/lib/src/simplifyExpression/arithmeticSearch/ArithmeticSearch.ts b/lib/src/simplifyExpression/arithmeticSearch/ArithmeticSearch.ts new file mode 100644 index 00000000..9b9cf27b --- /dev/null +++ b/lib/src/simplifyExpression/arithmeticSearch/ArithmeticSearch.ts @@ -0,0 +1,71 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { evaluate } from "../../util/evaluate"; +import { NodeType } from "../../node/NodeType"; +import { TreeSearch } from "../../TreeSearch"; +import { NodeCreator } from "../../node/Creator"; +import { NodeStatus } from "../../node/NodeStatus"; + +/** + * Searches through the tree, prioritizing deeper nodes, and evaluates + * arithmetic (e.g. 2+2 or 3*5*2) on an operation node if possible. + * Returns a Status object. + * */ +export const arithmeticSearch = TreeSearch.postOrder(arithmetic); + +/** + * evaluates arithmetic (e.g. 2+2 or 3*5*2) on an operation node. + * Returns a Status object. + * */ +function arithmetic(node) { + if (!NodeType.isOperator(node)) { + return NodeStatus.noChange(node); + } + if (!node.args.every((child) => NodeType.isConstant(child, true))) { + return NodeStatus.noChange(node); + } + + // we want to eval each arg so unary minuses around constant nodes become + // constant nodes with negative values + node.args.forEach((arg, i) => { + node.args[i] = NodeCreator.constant(evaluate(arg)); + }); + + // Only resolve division of integers if we get an integer result. + // Note that a fraction of decimals will be divided out. + if (NodeType.isIntegerFraction(node)) { + const numeratorValue = parseInt(node.args[0]); + const denominatorValue = parseInt(node.args[1]); + if (numeratorValue % denominatorValue === 0) { + const newNode = NodeCreator.constant(numeratorValue / denominatorValue); + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_ARITHMETIC, + node, + newNode + ); + } else { + return NodeStatus.noChange(node); + } + } else { + const evaluatedValue = evaluateAndRound(node); + const newNode = NodeCreator.constant(evaluatedValue); + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_ARITHMETIC, + node, + newNode + ); + } +} + +/** + * Evaluates a math expression to a constant, e.g. 3+4 -> 7 and rounds if + * necessary + * */ +function evaluateAndRound(node) { + let result = evaluate(node); + if (Math.abs(result) < 1) { + result = parseFloat(result.toPrecision(4)); + } else { + result = parseFloat(result.toFixed(4)); + } + return result; +} diff --git a/lib/src/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.ts b/lib/src/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.ts new file mode 100644 index 00000000..5639f71b --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.ts @@ -0,0 +1,187 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeMixedNumber } from "../../node/MixedNumber"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; + +/** + * Converts a mixed number to an improper fraction + * e.g. 1 2/3 -> 5/3 + * All comments in the function are based on this example + * */ +export function convertMixedNumberToImproperFraction(node) { + if (!NodeMixedNumber.isMixedNumber(node)) { + return NodeStatus.noChange(node); + } + + const substeps = []; + let newNode = node.cloneDeep(); + + // e.g. 1 2/3 + const wholeNumber = NodeMixedNumber.getWholeNumberValue(node); // 1 + const numerator = NodeMixedNumber.getNumeratorValue(node); // 2 + const denominator = NodeMixedNumber.getDenominatorValue(node); // 3 + const isNegativeMixedNumber = NodeMixedNumber.isNegativeMixedNumber(node); + + // STEP 1: Convert to unsimplified improper fraction + // e.g. 1 2/3 -> ((1 * 3) + 2) / 3 + let status = convertToUnsimplifiedImproperFraction( + newNode, + wholeNumber, + numerator, + denominator, + isNegativeMixedNumber + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // STEP 2: Simplify multiplication in numerator + // e.g. ((1 * 3) + 2) / 3 -> (3 + 2) / 3 + status = simplifyMultiplicationInImproperFraction( + newNode, + wholeNumber, + numerator, + denominator, + isNegativeMixedNumber + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // STEP 3: Simplify addition in numerator + // e.g. (3 + 2) / 3 -> 5/3 + status = simplifyAdditionInImproperFraction( + newNode, + wholeNumber, + numerator, + denominator, + isNegativeMixedNumber + ); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + return NodeStatus.nodeChanged( + ChangeTypes.CONVERT_MIXED_NUMBER_TO_IMPROPER_FRACTION, + node, + newNode, + true, + substeps + ); +} + +/** + * Convert a mixed number to an unsimplified proper fraction + * e.g. 1 2/3 -> ((1 * 3) + 2) / 3 + * */ +function convertToUnsimplifiedImproperFraction( + oldNode, + wholeNumber, + numerator, + denominator, + isNegativeMixedNumber +) { + // (wholeNumber * denominator) + // e.g. (1 * 3) + const newNumeratorMultiplication = NodeCreator.parenthesis( + NodeCreator.operator("*", [ + NodeCreator.constant(wholeNumber), + NodeCreator.constant(denominator), + ]) + ); + + // (wholeNumber * denominator) + numerator + // e.g. (1 * 3) + 2 + const newNumerator = NodeCreator.operator("+", [ + newNumeratorMultiplication, + NodeCreator.constant(numerator), + ]); + oldNode.args[0].args[0].changeGroup = 1; + newNumerator.changeGroup = 1; + + // e.g. 3 + const newDenominator = NodeCreator.constant(denominator); + + let newNode = NodeCreator.operator("/", [newNumerator, newDenominator]); + + if (isNegativeMixedNumber) { + newNode = NodeCreator.unaryMinus(newNode); + } + + return NodeStatus.nodeChanged( + ChangeTypes.IMPROPER_FRACTION_NUMERATOR, + oldNode, + newNode, + false + ); +} + +/** + * Simplify multiplication in the numerator of an improper fraction + * e.g. ((1 * 3) + 2) / 3 -> (3 + 2) / 3 + * */ +function simplifyMultiplicationInImproperFraction( + oldNode, + wholeNumber, + numerator, + denominator, + isNegativeMixedNumber +) { + // (wholeNumber * denominator) + numerator + // e.g. 3 + 2 + const newNumerator = NodeCreator.operator("+", [ + NodeCreator.constant(wholeNumber * denominator), + NodeCreator.constant(numerator), + ]); + oldNode.args[0].changeGroup = 1; + newNumerator.changeGroup = 1; + + // e.g. 3 + const newDenominator = NodeCreator.constant(denominator); + + let newNode = NodeCreator.operator("/", [newNumerator, newDenominator]); + + if (isNegativeMixedNumber) { + newNode = NodeCreator.unaryMinus(newNode); + } + + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_ARITHMETIC, + oldNode, + newNode, + false + ); +} + +/** + * Simplify addition in the numerator of an improper fraction + * e.g. (3 + 2) / 3 -> 5/3 + * */ +function simplifyAdditionInImproperFraction( + oldNode, + wholeNumber, + numerator, + denominator, + isNegativeMixedNumber +) { + // (wholeNumber * denominator) + numerator + // e.g. 5 + const newNumerator = NodeCreator.constant( + wholeNumber * denominator + numerator + ); + oldNode.args[0].changeGroup = 1; + newNumerator.changeGroup = 1; + + // e.g. 3 + const newDenominator = NodeCreator.constant(denominator); + + let newNode = NodeCreator.operator("/", [newNumerator, newDenominator]); + + if (isNegativeMixedNumber) { + newNode = NodeCreator.unaryMinus(newNode); + } + + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_ARITHMETIC, + oldNode, + newNode, + false + ); +} diff --git a/lib/simplifyExpression/basicsSearch/index.js b/lib/src/simplifyExpression/basicsSearch/index.ts similarity index 52% rename from lib/simplifyExpression/basicsSearch/index.js rename to lib/src/simplifyExpression/basicsSearch/index.ts index 093d704f..cccd7757 100644 --- a/lib/simplifyExpression/basicsSearch/index.js +++ b/lib/src/simplifyExpression/basicsSearch/index.ts @@ -1,66 +1,76 @@ -/* +/** * Performs simpifications that are more basic and overaching like (...)^0 => 1 * These are always the first simplifications that are attempted. - */ - -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const convertMixedNumberToImproperFraction = require('./convertMixedNumberToImproperFraction'); -const rearrangeCoefficient = require('./rearrangeCoefficient'); -const reduceExponentByZero = require('./reduceExponentByZero'); -const reduceMultiplicationByZero = require('./reduceMultiplicationByZero'); -const reduceZeroDividedByAnything = require('./reduceZeroDividedByAnything'); -const removeAdditionOfZero = require('./removeAdditionOfZero'); -const removeDivisionByOne = require('./removeDivisionByOne'); -const removeExponentBaseOne = require('./removeExponentBaseOne'); -const removeExponentByOne = require('./removeExponentByOne'); -const removeMultiplicationByNegativeOne = require('./removeMultiplicationByNegativeOne'); -const removeMultiplicationByOne = require('./removeMultiplicationByOne'); -const simplifyDoubleUnaryMinus = require('./simplifyDoubleUnaryMinus'); + * */ + +import { TreeSearch } from "../../TreeSearch"; + +import { rearrangeCoefficient } from "./rearrangeCoefficient"; +import { convertMixedNumberToImproperFraction } from "./convertMixedNumberToImproperFraction"; +import { reduceMultiplicationByZero } from "./reduceMultiplicationByZero"; +import { reduceZeroDividedByAnything } from "./reduceZeroDividedByAnything"; +import { reduceExponentByZero } from "./reduceExponentByZero"; +import { removeExponentByOne } from "./removeExponentByOne"; +import { removeExponentBaseOne } from "./removeExponentBaseOne"; +import { simplifyDoubleUnaryMinus } from "./simplifyDoubleUnaryMinus"; +import { removeAdditionOfZero } from "./removeAdditionOfZero"; +import { removeMultiplicationByOne } from "./removeMultiplicationByOne"; +import { removeMultiplicationByNegativeOne } from "./removeMultiplicationByNegativeOne"; +import { removeDivisionByOne } from "./removeDivisionByOne"; +import { NodeStatus } from "../../node/NodeStatus"; const SIMPLIFICATION_FUNCTIONS = [ // convert mixed numbers to improper fractions convertMixedNumberToImproperFraction, + // multiplication by 0 yields 0 reduceMultiplicationByZero, + // division of 0 by something yields 0 reduceZeroDividedByAnything, + // ____^0 --> 1 reduceExponentByZero, + // Check for x^1 which should be reduced to x removeExponentByOne, + // Check for 1^x which should be reduced to 1 // if x can be simplified to a constant removeExponentBaseOne, + // - - becomes + simplifyDoubleUnaryMinus, + // If this is a + node and one of the operands is 0, get rid of the 0 removeAdditionOfZero, + // If this is a * node and one of the operands is 1, get rid of the 1 removeMultiplicationByOne, + // In some cases, remove multiplying by -1 removeMultiplicationByNegativeOne, + // If this is a / node and the denominator is 1 or -1, get rid of it removeDivisionByOne, + // e.g. x*5 -> 5x rearrangeCoefficient, ]; -const search = TreeSearch.preOrder(basics); +export const basicsSearch = TreeSearch.preOrder(basics); -// Look for basic step(s) to perform on a node. Returns a Node.Status object. +/** + * Look for basic step(s) to perform on a node. Returns a Status object. + * */ function basics(node) { for (let i = 0; i < SIMPLIFICATION_FUNCTIONS.length; i++) { const nodeStatus = SIMPLIFICATION_FUNCTIONS[i](node); - if (nodeStatus.hasChanged()) { + if (nodeStatus.hasChanged) { return nodeStatus; - } - else { + } else { node = nodeStatus.newNode; } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } - -module.exports = search; diff --git a/lib/src/simplifyExpression/basicsSearch/rearrangeCoefficient.ts b/lib/src/simplifyExpression/basicsSearch/rearrangeCoefficient.ts new file mode 100644 index 00000000..ee88b5a2 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/rearrangeCoefficient.ts @@ -0,0 +1,29 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeStatus } from "../../node/NodeStatus"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { NodeCreator } from "../../node/Creator"; +import { canRearrangeCoefficient } from "../../checks/canRearrangeCoefficient"; + +/** + * Rearranges something of the form x * 5 to be 5x, ie putting the coefficient + * in the right place. + * Returns a Status object + * */ +export function rearrangeCoefficient(node) { + if (!canRearrangeCoefficient(node)) { + return NodeStatus.noChange(node); + } + + let newNode = node.cloneDeep(); + + const polyNode = new PolynomialTerm(newNode.args[0]); + const constNode = newNode.args[1]; + const exponentNode = polyNode.getExponentNode(); + newNode = NodeCreator.polynomialTerm( + polyNode.getSymbolNode(), + exponentNode, + constNode + ); + + return NodeStatus.nodeChanged(ChangeTypes.REARRANGE_COEFF, node, newNode); +} diff --git a/lib/src/simplifyExpression/basicsSearch/reduceExponentByZero.ts b/lib/src/simplifyExpression/basicsSearch/reduceExponentByZero.ts new file mode 100644 index 00000000..282c344a --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/reduceExponentByZero.ts @@ -0,0 +1,25 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeType } from "../../node/NodeType"; +import { NodeCreator } from "../../node/Creator"; + +/** + * If `node` is an exponent of something to 0, we can reduce that to just 1. + * Returns a Status object. + * */ +export function reduceExponentByZero(node) { + if (node.op !== "^") { + return NodeStatus.noChange(node); + } + const exponent = node.args[1]; + if (NodeType.isConstant(exponent) && exponent.value === "0") { + const newNode = NodeCreator.constant(1); + return NodeStatus.nodeChanged( + ChangeTypes.REDUCE_EXPONENT_BY_ZERO, + node, + newNode + ); + } else { + return NodeStatus.noChange(node); + } +} diff --git a/lib/src/simplifyExpression/basicsSearch/reduceMultiplicationByZero.ts b/lib/src/simplifyExpression/basicsSearch/reduceMultiplicationByZero.ts new file mode 100644 index 00000000..1cfd5050 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/reduceMultiplicationByZero.ts @@ -0,0 +1,32 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { NodeCreator } from "../../node/Creator"; + +/** + * If `node` is a multiplication node with 0 as one of its operands, + * reduce the node to 0. Returns a Status object. + * */ +export function reduceMultiplicationByZero(node) { + if (node.op !== "*") { + return NodeStatus.noChange(node); + } + const zeroIndex = node.args.findIndex((arg) => { + if (NodeType.isConstant(arg) && arg.value === "0") { + return true; + } + if (PolynomialTerm.isPolynomialTerm(arg)) { + const polyTerm = new PolynomialTerm(arg); + return polyTerm.getCoeffValue() === 0; + } + return false; + }); + if (zeroIndex >= 0) { + // reduce to just the 0 node + const newNode = NodeCreator.constant(0); + return NodeStatus.nodeChanged(ChangeTypes.MULTIPLY_BY_ZERO, node, newNode); + } else { + return NodeStatus.noChange(node); + } +} diff --git a/lib/src/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.ts b/lib/src/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.ts new file mode 100644 index 00000000..a0167ee3 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.ts @@ -0,0 +1,23 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; + +/** + * If `node` is a fraction with 0 as the numerator, reduce the node to 0. + * Returns a Status object. + * */ +export function reduceZeroDividedByAnything(node) { + if (node.op !== "/") { + return NodeStatus.noChange(node); + } + if (node.args[0].value === "0") { + const newNode = NodeCreator.constant(0); + return NodeStatus.nodeChanged( + ChangeTypes.REDUCE_ZERO_NUMERATOR, + node, + newNode + ); + } else { + return NodeStatus.noChange(node); + } +} diff --git a/lib/src/simplifyExpression/basicsSearch/removeAdditionOfZero.ts b/lib/src/simplifyExpression/basicsSearch/removeAdditionOfZero.ts new file mode 100644 index 00000000..649e5818 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/removeAdditionOfZero.ts @@ -0,0 +1,32 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeType } from "../../node/NodeType"; + +/** + * If `node` is an addition node with 0 as one of its operands, + * remove 0 from the operands list. Returns a Status object. + * */ +export function removeAdditionOfZero(node) { + if (node.op !== "+") { + return NodeStatus.noChange(node); + } + const zeroIndex = node.args.findIndex((arg) => { + return NodeType.isConstant(arg) && arg.value === "0"; + }); + let newNode = node.cloneDeep(); + if (zeroIndex >= 0) { + // remove the 0 node + newNode.args.splice(zeroIndex, 1); + // if there's only one operand left, there's nothing left to add it to, + // so move it up the tree + if (newNode.args.length === 1) { + newNode = newNode.args[0]; + } + return NodeStatus.nodeChanged( + ChangeTypes.REMOVE_ADDING_ZERO, + node, + newNode + ); + } + return NodeStatus.noChange(node); +} diff --git a/lib/src/simplifyExpression/basicsSearch/removeDivisionByOne.ts b/lib/src/simplifyExpression/basicsSearch/removeDivisionByOne.ts new file mode 100644 index 00000000..686cb5c3 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/removeDivisionByOne.ts @@ -0,0 +1,41 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { Negative } from "../../Negative"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeType } from "../../node/NodeType"; +import { NodeCreator } from "../../node/Creator"; + +/** + * If `node` is a division operation of something by 1 or -1, we can remove the + * denominator. Returns a Status object. + * */ +export function removeDivisionByOne(node) { + if (node.op !== "/") { + return NodeStatus.noChange(node); + } + const denominator = node.args[1]; + if (!NodeType.isConstant(denominator)) { + return NodeStatus.noChange(node); + } + // It's taken 40ms on average to pass distribution test, + // TODO: see if we should keep using utils/clone here + let numerator = node.args[0].cloneDeep(); + + // if denominator is -1, we make the numerator negative + if (parseFloat(denominator.value) === -1) { + // If the numerator was an operation, wrap it in parens before adding - + // to the front. + // e.g. 2+3 / -1 ---> -(2+3) + if (NodeType.isOperator(numerator)) { + numerator = NodeCreator.parenthesis(numerator); + } + const changeType = Negative.isNegative(numerator) + ? ChangeTypes.RESOLVE_DOUBLE_MINUS + : ChangeTypes.DIVISION_BY_NEGATIVE_ONE; + numerator = Negative.negate(numerator); + return NodeStatus.nodeChanged(changeType, node, numerator); + } else if (parseFloat(denominator.value) === 1) { + return NodeStatus.nodeChanged(ChangeTypes.DIVISION_BY_ONE, node, numerator); + } else { + return NodeStatus.noChange(node); + } +} diff --git a/lib/src/simplifyExpression/basicsSearch/removeExponentBaseOne.ts b/lib/src/simplifyExpression/basicsSearch/removeExponentBaseOne.ts new file mode 100644 index 00000000..067a7fd7 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/removeExponentBaseOne.ts @@ -0,0 +1,26 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { resolvesToConstant } from "../../checks/resolvesToConstant"; + +/** + * If `node` is of the form 1^x, reduces it to a node of the form 1. + * Returns a Status object. + * */ +export function removeExponentBaseOne(node) { + if ( + node.op === "^" && // an exponent with + resolvesToConstant(node.args[1]) && // a power not a symbol and + NodeType.isConstant(node.args[0]) && // a constant base + node.args[0].value === "1" + ) { + // of value 1 + const newNode = node.args[0].cloneDeep(); + return NodeStatus.nodeChanged( + ChangeTypes.REMOVE_EXPONENT_BASE_ONE, + node, + newNode + ); + } + return NodeStatus.noChange(node); +} diff --git a/lib/src/simplifyExpression/basicsSearch/removeExponentByOne.ts b/lib/src/simplifyExpression/basicsSearch/removeExponentByOne.ts new file mode 100644 index 00000000..80ae6bd9 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/removeExponentByOne.ts @@ -0,0 +1,24 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; + +/** + * If `node` is of the form x^1, reduces it to a node of the form x. + * Returns a Status object. + * */ +export function removeExponentByOne(node) { + if ( + node.op === "^" && // exponent of anything + NodeType.isConstant(node.args[1]) && // to a constant + node.args[1].value === "1" + ) { + // of value 1 + const newNode = node.args[0].cloneDeep(); + return NodeStatus.nodeChanged( + ChangeTypes.REMOVE_EXPONENT_BY_ONE, + node, + newNode + ); + } + return NodeStatus.noChange(node); +} diff --git a/lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.js b/lib/src/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.ts similarity index 51% rename from lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.js rename to lib/src/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.ts index 06564f5f..c5416cda 100644 --- a/lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.js +++ b/lib/src/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.ts @@ -1,20 +1,23 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); - -// If `node` is a multiplication node with -1 as one of its operands, -// and a non constant as the next operand, remove -1 from the operands -// list and make the next term have a unary minus. -// Returns a Node.Status object. -function removeMultiplicationByNegativeOne(node) { - if (node.op !== '*') { - return Node.Status.noChange(node); +import { ChangeTypes } from "../../ChangeTypes"; +import { Negative } from "../../Negative"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeType } from "../../node/NodeType"; + +/** + * If `node` is a multiplication node with -1 as one of its operands, + * and a non constant as the next operand, remove -1 from the operands + * list and make the next term have a unary minus. + * Returns a Status object. + * */ +export function removeMultiplicationByNegativeOne(node) { + if (node.op !== "*") { + return NodeStatus.noChange(node); } - const minusOneIndex = node.args.findIndex(arg => { - return Node.Type.isConstant(arg) && arg.value === '-1'; + const minusOneIndex = node.args.findIndex((arg) => { + return NodeType.isConstant(arg) && arg.value === "-1"; }); if (minusOneIndex < 0) { - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // We might merge/combine the negative one into another node. This stores @@ -23,15 +26,14 @@ function removeMultiplicationByNegativeOne(node) { // If minus one is the last term, maybe combine with the term before if (minusOneIndex + 1 === node.args.length) { nodeToCombineIndex = minusOneIndex - 1; - } - else { + } else { nodeToCombineIndex = minusOneIndex + 1; } let nodeToCombine = node.args[nodeToCombineIndex]; // If it's a constant, the combining of those terms is handled elsewhere. - if (Node.Type.isConstant(nodeToCombine)) { - return Node.Status.noChange(node); + if (NodeType.isConstant(nodeToCombine)) { + return NodeStatus.noChange(node); } let newNode = node.cloneDeep(); @@ -47,8 +49,9 @@ function removeMultiplicationByNegativeOne(node) { if (newNode.args.length === 1) { newNode = newNode.args[0]; } - return Node.Status.nodeChanged( - ChangeTypes.REMOVE_MULTIPLYING_BY_NEGATIVE_ONE, node, newNode); + return NodeStatus.nodeChanged( + ChangeTypes.REMOVE_MULTIPLYING_BY_NEGATIVE_ONE, + node, + newNode + ); } - -module.exports = removeMultiplicationByNegativeOne; diff --git a/lib/src/simplifyExpression/basicsSearch/removeMultiplicationByOne.ts b/lib/src/simplifyExpression/basicsSearch/removeMultiplicationByOne.ts new file mode 100644 index 00000000..1fd4e0c6 --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/removeMultiplicationByOne.ts @@ -0,0 +1,32 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeType } from "../../node/NodeType"; + +/** + * If `node` is a multiplication node with 1 as one of its operands, + * remove 1 from the operands list. Returns a Status object. + * */ +export function removeMultiplicationByOne(node) { + if (node.op !== "*") { + return NodeStatus.noChange(node); + } + const oneIndex = node.args.findIndex((arg) => { + return NodeType.isConstant(arg) && arg.value === "1"; + }); + if (oneIndex >= 0) { + let newNode = node.cloneDeep(); + // remove the 1 node + newNode.args.splice(oneIndex, 1); + // if there's only one operand left, there's nothing left to multiply it + // to, so move it up the tree + if (newNode.args.length === 1) { + newNode = newNode.args[0]; + } + return NodeStatus.nodeChanged( + ChangeTypes.REMOVE_MULTIPLYING_BY_ONE, + node, + newNode + ); + } + return NodeStatus.noChange(node); +} diff --git a/lib/src/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.ts b/lib/src/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.ts new file mode 100644 index 00000000..68b817cd --- /dev/null +++ b/lib/src/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.ts @@ -0,0 +1,49 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; + +/** + * Simplifies two unary minuses in a row by removing both of them. + * e.g. -(- 4) --> 4 + * */ +export function simplifyDoubleUnaryMinus(node) { + if (!NodeType.isUnaryMinus(node)) { + return NodeStatus.noChange(node); + } + const unaryArg = node.args[0]; + + // e.g. in - -x, -x is the unary arg, and we'd want to reduce to just x + if (NodeType.isUnaryMinus(unaryArg)) { + const newNode = unaryArg.args[0].cloneDeep(); + return NodeStatus.nodeChanged( + ChangeTypes.RESOLVE_DOUBLE_MINUS, + node, + newNode + ); + } + + // e.g. - -4, -4 could be a constant with negative value + else if (NodeType.isConstant(unaryArg) && parseFloat(unaryArg.value) < 0) { + const newNode = NodeCreator.constant(parseFloat(unaryArg.value) * -1); + return NodeStatus.nodeChanged( + ChangeTypes.RESOLVE_DOUBLE_MINUS, + node, + newNode + ); + } + // e.g. -(-(5+2)) + else if (NodeType.isParenthesis(unaryArg)) { + const parenthesisNode = unaryArg; + const parenthesisContent = parenthesisNode.content; + if (NodeType.isUnaryMinus(parenthesisContent)) { + const newNode = NodeCreator.parenthesis(parenthesisContent.args[0]); + return NodeStatus.nodeChanged( + ChangeTypes.RESOLVE_DOUBLE_MINUS, + node, + newNode + ); + } + } + return NodeStatus.noChange(node); +} diff --git a/lib/src/simplifyExpression/breakUpNumeratorSearch/index.ts b/lib/src/simplifyExpression/breakUpNumeratorSearch/index.ts new file mode 100644 index 00000000..b9e7ce16 --- /dev/null +++ b/lib/src/simplifyExpression/breakUpNumeratorSearch/index.ts @@ -0,0 +1,49 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { TreeSearch } from "../../TreeSearch"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; + +// Breaks up any fraction (deeper nodes getting priority) that has a numerator +// that is a sum. e.g. (2+x)/5 -> (2/5 + x/5) +// This step must happen after things have been collected and combined, or +// else things will infinite loop, so it's a tree search of its own. +// Returns a Status object +export const breakUpNumeratorSearch = TreeSearch.postOrder(breakUpNumerator); + +// If `node` is a fraction with a numerator that is a sum, breaks up the +// fraction e.g. (2+x)/5 -> (2/5 + x/5) +// Returns a Status object +function breakUpNumerator(node) { + if (!NodeType.isOperator(node) || node.op !== "/") { + return NodeStatus.noChange(node); + } + let numerator = node.args[0]; + if (NodeType.isParenthesis(numerator)) { + numerator = numerator.content; + } + if (!NodeType.isOperator(numerator) || numerator.op !== "+") { + return NodeStatus.noChange(node); + } + + // At this point, we know that node is a fraction and its numerator is a sum + // of terms that can't be collected or combined, so we should break it up. + const fractionList = []; + const denominator = node.args[1]; + numerator.args.forEach((arg) => { + const newFraction = NodeCreator.operator("/", [arg, denominator]); + newFraction.changeGroup = 1; + fractionList.push(newFraction); + }); + + let newNode = NodeCreator.operator("+", fractionList); + // Wrap in parens for cases like 2*(2+3)/5 => 2*(2/5 + 3/5) + newNode = NodeCreator.parenthesis(newNode); + node.changeGroup = 1; + return NodeStatus.nodeChanged( + ChangeTypes.BREAK_UP_FRACTION, + node, + newNode, + false + ); +} diff --git a/lib/simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower.js b/lib/src/simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower.ts similarity index 72% rename from lib/simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower.js rename to lib/src/simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower.ts index 7fbe07fb..001f040a 100644 --- a/lib/simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower.js +++ b/lib/src/simplifyExpression/collectAndCombineSearch/ConstantOrConstantPower.ts @@ -1,5 +1,5 @@ -const NodeCreator = require('../../node/Creator'); -const NodeType = require('../../node/Type'); +import { NodeCreator } from "../../node/Creator"; +import { NodeType } from "../../node/NodeType"; // This module is needed when simplifying multiplication of constant powers // as it contains functions to get different parts of the node instead of @@ -13,11 +13,10 @@ const NodeType = require('../../node/Type'); // else returns the node as it is constant. // e.g 2^4 returns 2 // e.g 3 returns 3, since 3 is equal to 3^1 which has a base of 3 -function getBaseNode(node) { +export function getBaseNode(node) { if (node.args) { return node.args[0]; - } - else { + } else { return node; } } @@ -26,25 +25,19 @@ function getBaseNode(node) { // value 1 if there's no exponent. // e.g. on the node representing 2^3, returns a constant node with value 3 // e.g 3 returns 1, since 3 is equal to 3^1 which has an exponent of 1 -function getExponentNode(node) { +export function getExponentNode(node) { if (NodeType.isConstant(node)) { return NodeCreator.constant(1); - } - else { + } else { return node.args[1]; } } // Checks if the node is an constant or a power with constant base. // e.g. 2^3 is a constant power node, 5 is a constant node, x and x^2 are not -function isConstantOrConstantPower(node) { - return ((NodeType.isOperator(node, '^') && - NodeType.isConstant(node.args[0])) || - NodeType.isConstant(node)); +export function isConstantOrConstantPower(node) { + return ( + (NodeType.isOperator(node, "^") && NodeType.isConstant(node.args[0])) || + NodeType.isConstant(node) + ); } - -module.exports = { - getBaseNode, - getExponentNode, - isConstantOrConstantPower -}; diff --git a/lib/src/simplifyExpression/collectAndCombineSearch/LikeTermCollector.ts b/lib/src/simplifyExpression/collectAndCombineSearch/LikeTermCollector.ts new file mode 100644 index 00000000..5e3d70b1 --- /dev/null +++ b/lib/src/simplifyExpression/collectAndCombineSearch/LikeTermCollector.ts @@ -0,0 +1,295 @@ +import { printAscii } from "../../util/print"; + +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { NodeType } from "../../node/NodeType"; +import { appendToArrayInObject } from "../../util/Util"; +import { NthRootTerm } from "../../node/NthRootTerm"; +import { getRootNode } from "../functionsSearch/nthRoot"; + +const CONSTANT = "constant"; +const CONSTANT_FRACTION = "constantFraction"; +const NTH_ROOT = "nthRoot"; +const OTHER = "other"; + +export class LikeTermCollector { + // Given an expression tree, returns true if there are terms that can be + // collected + static canCollectLikeTerms(node) { + // We can collect like terms through + or through * + // Note that we never collect like terms with - or /, those expressions will + // always be manipulated in flattenOperands so that the top level operation is + // + or *. + if (!(NodeType.isOperator(node, "+") || NodeType.isOperator(node, "*"))) { + return false; + } + + let terms; + if (node.op === "+") { + terms = getTermsForCollectingAddition(node); + } else if (node.op === "*") { + terms = getTermsForCollectingMultiplication(node); + } else { + throw Error("Operation not supported: " + node.op); + } + + // Conditions we need to meet to decide to to reorganize (collect) the terms: + // - more than 1 term type + // - more than 1 of at least one type (not including other) + // (note that this means x^2 + x + x + 2 -> x^2 + (x + x) + 2, + // which will be recorded as a step, but doesn't change the order of terms) + const termTypes = Object.keys(terms); + const filteredTermTypes = termTypes.filter((x) => x !== OTHER); + return ( + termTypes.length > 1 && filteredTermTypes.some((x) => terms[x].length > 1) + ); + } + + // Collects like terms for an operation node and returns a Status object. + static collectLikeTerms(node) { + if (!this.canCollectLikeTerms(node)) { + return NodeStatus.noChange(node); + } + + const op = node.op; + let terms; + if (op === "+") { + terms = getTermsForCollectingAddition(node); + } else if (op === "*") { + terms = getTermsForCollectingMultiplication(node); + } else { + throw Error("Operation not supported: " + op); + } + + // List the symbols alphabetically + const termTypesSorted = Object.keys(terms) + .filter((x) => x !== CONSTANT && x !== CONSTANT_FRACTION && x !== OTHER) + .sort(sortTerms); + + // Then add const + if (terms[CONSTANT]) { + // at the end for addition (since we'd expect x^2 + (x + x) + 4) + if (op === "+") { + termTypesSorted.push(CONSTANT); + } + // for multipliation it should be at the front (e.g. (3*4) * x^2) + if (op === "*") { + termTypesSorted.unshift(CONSTANT); + } + } + if (terms[CONSTANT_FRACTION]) { + termTypesSorted.push(CONSTANT_FRACTION); + } + + // Collect the new operands under op. + let newOperands = []; + let changeGroup = 1; + termTypesSorted.forEach((termType) => { + const termsOfType = terms[termType]; + if (termsOfType.length === 1) { + const singleTerm = termsOfType[0].cloneDeep(); + singleTerm.changeGroup = changeGroup; + newOperands.push(singleTerm); + } + // Any like terms should be wrapped in parens. + else { + const termList = NodeCreator.parenthesis( + NodeCreator.operator(op, termsOfType) + ).cloneDeep(); + termList.changeGroup = changeGroup; + newOperands.push(termList); + } + termsOfType.forEach((term) => { + term.changeGroup = changeGroup; + }); + changeGroup++; + }); + + // then stick anything else (paren nodes, operator nodes) at the end + if (terms[OTHER]) { + newOperands = newOperands.concat(terms[OTHER]); + } + + const newNode = node.cloneDeep(); + newNode.args = newOperands; + return NodeStatus.nodeChanged( + ChangeTypes.COLLECT_LIKE_TERMS, + node, + newNode, + false + ); + } +} + +// Terms with coefficients are collected by categorizing them by their 'name' +// which is used to separate them into groups that can be combined. getTermName +// returns this group 'name' +function getTermName(node, termSubclass, op) { + const term = new termSubclass(node); + // we 'name' terms by their base node name + let termName = printAscii(term.getBaseNode()); + // when adding terms, the exponent matters too (e.g. 2x^2 + 5x^3 can't be combined) + if (op === "+") { + const exponent = printAscii(term.getExponentNode(true)); + termName += "^" + exponent; + } + return termName; +} + +// Collects like terms in an addition expression tree into categories. +// Returns a dictionary of termname to lists of nodes with that name +// e.g. 2x + 4 + 5x would return {'x': [2x, 5x], CONSTANT: [4]} +// (where 2x, 5x, and 4 would actually be expression trees) +function getTermsForCollectingAddition(node) { + let terms = {}; + + for (let i = 0; i < node.args.length; i++) { + const child = node.args[i]; + + if (PolynomialTerm.isPolynomialTerm(child)) { + const termName = getTermName(child, PolynomialTerm, "+"); + terms = appendToArrayInObject(terms, termName, child); + } else if (NthRootTerm.isNthRootTerm(child)) { + const termName = getTermName(child, NthRootTerm, "+"); + terms = appendToArrayInObject(terms, termName, child); + } else if (NodeType.isIntegerFraction(child)) { + terms = appendToArrayInObject(terms, CONSTANT_FRACTION, child); + } else if (NodeType.isConstant(child)) { + terms = appendToArrayInObject(terms, CONSTANT, child); + } else if ( + NodeType.isOperator(node) || + NodeType.isFunction(node) || + NodeType.isParenthesis(node) || + NodeType.isUnaryMinus(node) + ) { + terms = appendToArrayInObject(terms, OTHER, child); + } else { + // Note that we shouldn't get any symbol nodes in the switch statement + // since they would have been handled by isPolynomialTerm + throw Error("Unsupported node type: " + child.type); + } + } + // If there's exactly one constant and one fraction, we collect them + // to add them together. + // e.g. 2 + 1/3 + 5 would collect the constants (2+5) + 1/3 + // but 2 + 1/3 + x would collect (2 + 1/3) + x so we can add them together + if ( + terms[CONSTANT] && + terms[CONSTANT].length === 1 && + terms[CONSTANT_FRACTION] && + terms[CONSTANT_FRACTION].length === 1 + ) { + const fraction = terms[CONSTANT_FRACTION][0]; + terms = appendToArrayInObject(terms, CONSTANT, fraction); + delete terms[CONSTANT_FRACTION]; + } + + return terms; +} + +// Collects like terms in a multiplication expression tree into categories. +// For multiplication, polynomial terms with constants are separated into +// a symbolic term and a constant term. +// Returns a dictionary of termname to lists of nodes with that name +// e.g. 2x + 4 + 5x^2 would return {'x': [x, x^2], CONSTANT: [2, 4, 5]} +// (where x, x^2, 2, 4, and 5 would actually be expression trees) +function getTermsForCollectingMultiplication(node) { + let terms = {}; + + for (let i = 0; i < node.args.length; i++) { + let child = node.args[i]; + + if (NodeType.isUnaryMinus(child)) { + terms = appendToArrayInObject(terms, CONSTANT, NodeCreator.constant(-1)); + child = child.args[0]; + } + if (PolynomialTerm.isPolynomialTerm(child)) { + terms = addToTermsforPolynomialMultiplication(terms, child); + } else if (NodeType.isFunction(child, "nthRoot")) { + terms = addToTermsforNthRootMultiplication(terms, child); + } else if (NodeType.isIntegerFraction(child)) { + terms = appendToArrayInObject(terms, CONSTANT, child); + } else if (NodeType.isConstant(child)) { + terms = appendToArrayInObject(terms, CONSTANT, child); + } else if ( + NodeType.isOperator(node) || + NodeType.isFunction(node) || + NodeType.isParenthesis(node) || + NodeType.isUnaryMinus(node) + ) { + terms = appendToArrayInObject(terms, OTHER, child); + } else { + // Note that we shouldn't get any symbol nodes in the switch statement + // since they would have been handled by isPolynomialTerm + throw Error("Unsupported node type: " + child.type); + } + } + return terms; +} + +// A helper function for getTermsForCollectingMultiplication +// e.g. nthRoot(x, 2), append 'nthRoot2': nthRootNode to terms dictionary +// Takes the terms dictionary and the nthRoot node, and returns an updated +// terms dictionary. +function addToTermsforNthRootMultiplication(terms, node) { + const rootNode = getRootNode(node); + const rootNodeValue = rootNode.value; + + terms = appendToArrayInObject(terms, NTH_ROOT + rootNodeValue, node); + + return terms; +} + +// A helper function for getTermsForCollectingMultiplication +// Polynomial terms need to be divided into their coefficient + symbolic parts. +// e.g. 2x^4 -> 2 (coeffient) and x^4 (symbolic, named after the symbol node) +// Takes the terms list and the polynomial term node, and returns an updated +// terms list. +function addToTermsforPolynomialMultiplication(terms, node) { + const polyNode = new PolynomialTerm(node); + let termName; + + if (!polyNode.hasCoeff()) { + termName = getTermName(node, PolynomialTerm, "*"); + terms = appendToArrayInObject(terms, termName, node); + } else { + const coefficient = polyNode.getCoeffNode(); + let termWithoutCoefficient = polyNode.getSymbolNode(); + if (polyNode.getExponentNode()) { + termWithoutCoefficient = NodeCreator.operator("^", [ + termWithoutCoefficient, + polyNode.getExponentNode(), + ]); + } + + terms = appendToArrayInObject(terms, CONSTANT, coefficient); + termName = getTermName(termWithoutCoefficient, PolynomialTerm, "*"); + terms = appendToArrayInObject(terms, termName, termWithoutCoefficient); + } + return terms; +} + +// Sort function for termnames. Sort first by symbol name, and then by exponent. +function sortTerms(a, b) { + if (a === b) { + return 0; + } + // if no exponent, sort alphabetically + if (a.indexOf("^") === -1) { + return a < b ? -1 : 1; + } + // if exponent: sort by symbol, but then exponent decreasing + else { + const symbA = a.split("^")[0]; + const expA = a.split("^")[1]; + const symbB = b.split("^")[0]; + const expB = b.split("^")[1]; + if (symbA !== symbB) { + return symbA < symbB ? -1 : 1; + } else { + return expA > expB ? -1 : 1; + } + } +} diff --git a/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js b/lib/src/simplifyExpression/collectAndCombineSearch/addLikeTerms.ts similarity index 61% rename from lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js rename to lib/src/simplifyExpression/collectAndCombineSearch/addLikeTerms.ts index e7948985..9c709161 100644 --- a/lib/simplifyExpression/collectAndCombineSearch/addLikeTerms.js +++ b/lib/src/simplifyExpression/collectAndCombineSearch/addLikeTerms.ts @@ -1,54 +1,63 @@ -const checks = require('../../checks'); -const evaluateConstantSum = require('./evaluateConstantSum'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); - -// If possible, adds together a list of nodes . Returns a Node.Status object. -function addLikeTerms(node, polynomialOnly=false) { - if (!Node.Type.isOperator(node)) { - return Node.Status.noChange(node); +import { evaluateConstantSum } from "./evaluateConstantSum"; + +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { NthRootTerm } from "../../node/NthRootTerm"; +import { NodeCreator } from "../../node/Creator"; +import { + canAddLikeTermNthRootNodes, + canAddLikeTermPolynomialNodes, +} from "../../checks/canAddLikeTerms"; + +// If possible, adds together a list of nodes . Returns a Status object. +export function addLikeTerms(node, polynomialOnly = false) { + if (!NodeType.isOperator(node)) { + return NodeStatus.noChange(node); } let status; if (!polynomialOnly) { status = evaluateConstantSum(node); - if (status.hasChanged()) { + if (status.hasChanged) { return status; } } status = addLikePolynomialTerms(node); - if (status.hasChanged()) { + if (status.hasChanged) { return status; } status = addLikeNthRootTerms(node); - if (status.hasChanged()) { + if (status.hasChanged) { return status; } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // If possible, adds together a list of polynomial term nodes. function addLikePolynomialTerms(node) { - if (!checks.canAddLikeTerms.canAddLikeTermPolynomialNodes(node)) { - return Node.Status.noChange(node); + if (!canAddLikeTermPolynomialNodes(node)) { + return NodeStatus.noChange(node); } return addLikeTermNodes( - node, Node.PolynomialTerm, ChangeTypes.ADD_POLYNOMIAL_TERMS); + node, + PolynomialTerm, + ChangeTypes.ADD_POLYNOMIAL_TERMS + ); } // If possible, adds together a list of nth root term nodes. function addLikeNthRootTerms(node) { - if (!checks.canAddLikeTerms.canAddLikeTermNthRootNodes(node)) { - return Node.Status.noChange(node); + if (!canAddLikeTermNthRootNodes(node)) { + return NodeStatus.noChange(node); } - return addLikeTermNodes( - node, Node.NthRootTerm, ChangeTypes.ADD_NTH_ROOTS); + return addLikeTermNodes(node, NthRootTerm, ChangeTypes.ADD_NTH_ROOTS); } // Helper function for adding together a list of nodes @@ -61,39 +70,38 @@ function addLikeTermNodes(node, termSubclass, changeType) { // (this step only happens under certain conditions and later steps might // happen even if step 1 does not) let status = addPositiveOneCoefficient(newNode, termSubclass); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } // STEP 2: If any nodes have a unary minus, make it have coefficient -1 // (this step only happens under certain conditions and later steps might // happen even if step 2 does not) status = addNegativeOneCoefficient(newNode, termSubclass); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } // STEP 3: group the coefficients in a sum status = groupCoefficientsForAdding(newNode, termSubclass); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); // STEP 4: evaluate the sum (could include fractions) - status = evaluateCoefficientSum(newNode, termSubclass); + status = evaluateCoefficientSum(newNode); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); - return Node.Status.nodeChanged( - changeType, node, newNode, true, substeps); + return NodeStatus.nodeChanged(changeType, node, newNode, true, substeps); } // Given a sum of like terms, changes any term with no coefficient // into a term with an explicit coefficient of 1. This is for pedagogy, and // makes the adding coefficients step clearer. // e.g. 2x + x -> 2x + 1x -// Returns a Node.Status object. +// Returns a Status object. function addPositiveOneCoefficient(node, termSubclass) { const newNode = node.cloneDeep(); let change = false; @@ -102,11 +110,12 @@ function addPositiveOneCoefficient(node, termSubclass) { newNode.args.forEach((child, i) => { const term = new termSubclass(child); if (term.getCoeffValue() === 1) { - newNode.args[i] = Node.Creator.term( + newNode.args[i] = NodeCreator.term( term.getBaseNode(), term.getExponentNode(), - Node.Creator.constant(1), - true /* explicit coefficient */); + NodeCreator.constant(1), + true /* explicit coefficient */ + ); newNode.args[i].changeGroup = changeGroup; node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" @@ -117,11 +126,14 @@ function addPositiveOneCoefficient(node, termSubclass) { }); if (change) { - return Node.Status.nodeChanged( - ChangeTypes.ADD_COEFFICIENT_OF_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); + return NodeStatus.nodeChanged( + ChangeTypes.ADD_COEFFICIENT_OF_ONE, + node, + newNode, + false + ); + } else { + return NodeStatus.noChange(node); } } @@ -129,7 +141,7 @@ function addPositiveOneCoefficient(node, termSubclass) { // coefficient into a term with an explicit coefficient of -1. This is for // pedagogy, and makes the adding coefficients step clearer. // e.g. 2x - x -> 2x - 1x -// Returns a Node.Status object. +// Returns a Status object. function addNegativeOneCoefficient(node, termSubclass) { const newNode = node.cloneDeep(); let change = false; @@ -138,11 +150,12 @@ function addNegativeOneCoefficient(node, termSubclass) { newNode.args.forEach((child, i) => { const term = new termSubclass(child); if (term.getCoeffValue() === -1) { - newNode.args[i] = Node.Creator.term( + newNode.args[i] = NodeCreator.term( term.getBaseNode(), term.getExponentNode(), term.getCoeffNode(), - true /* explicit -1 coefficient */); + true /* explicit -1 coefficient */ + ); node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" newNode.args[i].changeGroup = changeGroup; @@ -153,24 +166,28 @@ function addNegativeOneCoefficient(node, termSubclass) { }); if (change) { - return Node.Status.nodeChanged( - ChangeTypes.UNARY_MINUS_TO_NEGATIVE_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); + return NodeStatus.nodeChanged( + ChangeTypes.UNARY_MINUS_TO_NEGATIVE_ONE, + node, + newNode, + false + ); + } else { + return NodeStatus.noChange(node); } } // Given a sum of like terms, groups the coefficients // e.g. 2x^2 + 3x^2 + 5x^2 -> (2+3+5)x^2 -// Returns a Node.Status object. +// Returns a Status object. function groupCoefficientsForAdding(node, termSubclass) { let newNode = node.cloneDeep(); - const termList = newNode.args.map(n => new termSubclass(n)); - const coefficientList = termList.map(term => term.getCoeffNode(true)); - const sumOfCoefficents = Node.Creator.parenthesis( - Node.Creator.operator('+', coefficientList)); + const termList = newNode.args.map((n) => new termSubclass(n)); + const coefficientList = termList.map((term) => term.getCoeffNode(true)); + const sumOfCoefficents = NodeCreator.parenthesis( + NodeCreator.operator("+", coefficientList) + ); // TODO: changegroups should also be on the before node, on all the // coefficients, but changegroups with term gets messy so let's tackle // that later. @@ -181,11 +198,14 @@ function groupCoefficientsForAdding(node, termSubclass) { const firstTerm = termList[0]; const exponentNode = firstTerm.getExponentNode(); const baseNode = firstTerm.getBaseNode(); - newNode = Node.Creator.term( - baseNode, exponentNode, sumOfCoefficents); - - return Node.Status.nodeChanged( - ChangeTypes.GROUP_COEFFICIENTS, node, newNode, false); + newNode = NodeCreator.term(baseNode, exponentNode, sumOfCoefficents); + + return NodeStatus.nodeChanged( + ChangeTypes.GROUP_COEFFICIENTS, + node, + newNode, + false + ); } // Given a node of the form (2 + 4 + 5)x -- ie the coefficients have been @@ -197,7 +217,5 @@ function evaluateCoefficientSum(node) { // so we want to evaluate args[0] const coefficientSum = node.cloneDeep().args[0]; const childStatus = evaluateConstantSum(coefficientSum); - return Node.Status.childChanged(node, childStatus, 0); + return NodeStatus.childChanged(node, childStatus, 0); } - -module.exports = addLikeTerms; diff --git a/lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.js b/lib/src/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.ts similarity index 55% rename from lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.js rename to lib/src/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.ts index 280f0aa3..b0d88bec 100644 --- a/lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.js +++ b/lib/src/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.ts @@ -1,22 +1,23 @@ -const addConstantAndFraction = require('../fractionsSearch/addConstantAndFraction'); -const addConstantFractions = require('../fractionsSearch/addConstantFractions'); -const arithmeticSearch = require('../arithmeticSearch'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); +import { ChangeTypes } from "../../ChangeTypes"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { arithmeticSearch } from "../arithmeticSearch/ArithmeticSearch"; +import { addConstantFractions } from "../fractionsSearch/addConstantFractions"; +import { addConstantAndFraction } from "../fractionsSearch/addConstantAndFraction"; +import { NodeCreator } from "../../node/Creator"; // Evaluates a sum of constant numbers and integer fractions to a single // constant number or integer fraction. e.g. e.g. 2/3 + 5 + 5/2 => 49/6 -// Returns a Node.Status object. -function evaluateConstantSum(node) { - if (Node.Type.isParenthesis(node)) { +// Returns a Status object. +export function evaluateConstantSum(node) { + if (NodeType.isParenthesis(node)) { node = node.content; } - if (!Node.Type.isOperator(node) || node.op !== '+') { - return Node.Status.noChange(node); + if (!NodeType.isOperator(node) || node.op !== "+") { + return NodeStatus.noChange(node); } - if (node.args.some(node => !Node.Type.isConstantOrConstantFraction(node))) { - return Node.Status.noChange(node); + if (node.args.some((node) => !NodeType.isConstantOrConstantFraction(node))) { + return NodeStatus.noChange(node); } // functions needed to evaluate the sum @@ -27,8 +28,8 @@ function evaluateConstantSum(node) { ]; for (let i = 0; i < summingFunctions.length; i++) { const status = summingFunctions[i](node); - if (status.hasChanged()) { - if (Node.Type.isConstantOrConstantFraction(status.newNode)) { + if (status.hasChanged) { + if (NodeType.isConstantOrConstantFraction(status.newNode)) { return status; } } @@ -41,29 +42,29 @@ function evaluateConstantSum(node) { // STEP 1: group fractions and constants separately status = groupConstantsAndFractions(newNode); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); const constants = newNode.args[0]; const fractions = newNode.args[1]; // STEP 2A: evaluate arithmetic IF there's > 1 constant // (which is the case if it's a list surrounded by parenthesis) - if (Node.Type.isParenthesis(constants)) { + if (NodeType.isParenthesis(constants)) { const constantList = constants.content; const evaluateStatus = arithmeticSearch(constantList); - status = Node.Status.childChanged(newNode, evaluateStatus, 0); + status = NodeStatus.childChanged(newNode, evaluateStatus, 0); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } // STEP 2B: add fractions IF there's > 1 fraction // (which is the case if it's a list surrounded by parenthesis) - if (Node.Type.isParenthesis(fractions)) { + if (NodeType.isParenthesis(fractions)) { const fractionList = fractions.content; const evaluateStatus = addConstantFractions(fractionList); - status = Node.Status.childChanged(newNode, evaluateStatus, 1); + status = NodeStatus.childChanged(newNode, evaluateStatus, 1); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } // STEP 3: combine the evaluated constant and fraction @@ -71,31 +72,36 @@ function evaluateConstantSum(node) { // so we just call evaluateConstantSum again to cycle through status = evaluateConstantSum(newNode); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode, true, substeps); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_ARITHMETIC, + node, + newNode, + true, + substeps + ); } // If we can't combine using one of those functions, there's a mix of > 2 // fractions and constants. So we need to group them together so we can later // add them. // Expects a node that is a sum of integer fractions and constants. -// Returns a Node.Status object. +// Returns a Status object. // e.g. 2/3 + 5 + 5/2 => (2/3 + 5/2) + 5 function groupConstantsAndFractions(node) { - let fractions = node.args.filter(Node.Type.isIntegerFraction); - let constants = node.args.filter(Node.Type.isConstant); + let fractions = node.args.filter(NodeType.isIntegerFraction); + let constants = node.args.filter(NodeType.isConstant); if (fractions.length === 0 || constants.length === 0) { - throw Error('expected both integer fractions and constants, got ' + node); + throw Error("expected both integer fractions and constants, got " + node); } if (fractions.length + constants.length !== node.args.length) { - throw Error('can only evaluate integer fractions and constants'); + throw Error("can only evaluate integer fractions and constants"); } - constants = constants.map(node => { + constants = constants.map((node) => { // set the changeGroup - this affects both the old and new node node.changeGroup = 1; // clone so that node and newNode aren't stored in the same memory @@ -103,13 +109,12 @@ function groupConstantsAndFractions(node) { }); // wrap in parenthesis if there's more than one, to group them if (constants.length > 1) { - constants = Node.Creator.parenthesis(Node.Creator.operator('+', constants)); - } - else { + constants = NodeCreator.parenthesis(NodeCreator.operator("+", constants)); + } else { constants = constants[0]; } - fractions = fractions.map(node => { + fractions = fractions.map((node) => { // set the changeGroup - this affects both the old and new node node.changeGroup = 2; // clone so that node and newNode aren't stored in the same memory @@ -117,15 +122,11 @@ function groupConstantsAndFractions(node) { }); // wrap in parenthesis if there's more than one, to group them if (fractions.length > 1) { - fractions = Node.Creator.parenthesis(Node.Creator.operator('+', fractions)); - } - else { + fractions = NodeCreator.parenthesis(NodeCreator.operator("+", fractions)); + } else { fractions = fractions[0]; } - const newNode = Node.Creator.operator('+', [constants, fractions]); - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_LIKE_TERMS, node, newNode); + const newNode = NodeCreator.operator("+", [constants, fractions]); + return NodeStatus.nodeChanged(ChangeTypes.COLLECT_LIKE_TERMS, node, newNode); } - -module.exports = evaluateConstantSum; diff --git a/lib/simplifyExpression/collectAndCombineSearch/index.js b/lib/src/simplifyExpression/collectAndCombineSearch/index.ts similarity index 64% rename from lib/simplifyExpression/collectAndCombineSearch/index.js rename to lib/src/simplifyExpression/collectAndCombineSearch/index.ts index 5ae4c1b3..0f67c60c 100644 --- a/lib/simplifyExpression/collectAndCombineSearch/index.js +++ b/lib/src/simplifyExpression/collectAndCombineSearch/index.ts @@ -1,44 +1,45 @@ // Collects and combines like terms -const addLikeTerms = require('./addLikeTerms'); -const checks = require('../../checks'); -const multiplyLikeTerms = require('./multiplyLikeTerms'); - -const ChangeTypes = require('../../ChangeTypes'); -const LikeTermCollector = require('./LikeTermCollector'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); +import { ChangeTypes } from "../../ChangeTypes"; +import { LikeTermCollector } from "./LikeTermCollector"; +import { TreeSearch } from "../../TreeSearch"; +import { addLikeTerms } from "./addLikeTerms"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeType } from "../../node/NodeType"; +import { canMultiplyLikeTermConstantNodes } from "../../checks/canMultiplyLikeTermConstantNodes"; +import { multiplyLikeTerms } from "./multiplyLikeTerms"; const termCollectorFunctions = { - '+': addLikeTerms, - '*': multiplyLikeTerms + "+": addLikeTerms, + "*": multiplyLikeTerms, }; // Iterates through the tree looking for like terms to collect and combine. -// Will prioritize deeper expressions. Returns a Node.Status object. -const search = TreeSearch.postOrder(collectAndCombineLikeTerms); +// Will prioritize deeper expressions. Returns a Status object. +export const collectAndCombineSearch = TreeSearch.postOrder( + collectAndCombineLikeTerms +); // Given an operator node, maybe collects and then combines if possible // e.g. 2x + 4x + y => 6x + y // e.g. 2x * x^2 * 5x => 10 x^4 function collectAndCombineLikeTerms(node) { - if (node.op === '+') { + if (node.op === "+") { const status = collectAndCombineOperation(node); - if (status.hasChanged()) { + if (status.hasChanged) { return status; } // we might also be able to just combine if they're all the same term // e.g. 2x + 4x + x (doesn't need collecting) return addLikeTerms(node, true); - } - else if (node.op === '*') { + } else if (node.op === "*") { // collect and combine involves there being coefficients pulled the front // e.g. 2x * x^2 * 5x => (2*5) * (x * x^2 * x) => ... => 10 x^4 - if (checks.canMultiplyLikeTermConstantNodes(node)) { + if (canMultiplyLikeTermConstantNodes(node)) { return multiplyLikeTerms(node, true); } const status = collectAndCombineOperation(node); - if (status.hasChanged()) { + if (status.hasChanged) { // make sure there's no * between the coefficient and the symbol part status.newNode.implicit = true; return status; @@ -46,9 +47,8 @@ function collectAndCombineLikeTerms(node) { // we might also be able to just combine polynomial terms // e.g. x * x^2 * x => ... => x^4 return multiplyLikeTerms(node, true); - } - else { - return Node.Status.noChange(node); + } else { + return NodeStatus.noChange(node); } } @@ -58,13 +58,13 @@ function collectAndCombineOperation(node) { let substeps = []; const status = LikeTermCollector.collectLikeTerms(node.cloneDeep()); - if (!status.hasChanged()) { + if (!status.hasChanged) { return status; } // STEP 1: collect like terms, e.g. 2x + 4x^2 + 5x => 4x^2 + (2x + 5x) substeps.push(status); - let newNode = Node.Status.resetChangeGroups(status.newNode); + let newNode = NodeStatus.resetChangeGroups(status.newNode); // STEP 2 onwards: combine like terms for each group that can be combined // e.g. (x + 3x) + (2 + 2) has two groups @@ -72,12 +72,16 @@ function collectAndCombineOperation(node) { if (combineSteps.length > 0) { substeps = substeps.concat(combineSteps); const lastStep = combineSteps[combineSteps.length - 1]; - newNode = Node.Status.resetChangeGroups(lastStep.newNode); + newNode = NodeStatus.resetChangeGroups(lastStep.newNode); } - return Node.Status.nodeChanged( + return NodeStatus.nodeChanged( ChangeTypes.COLLECT_AND_COMBINE_LIKE_TERMS, - node, newNode, true, substeps); + node, + newNode, + true, + substeps + ); } // step 2 onwards for collectAndCombineOperation @@ -91,19 +95,17 @@ function combineLikeTerms(node) { for (let i = 0; i < node.args.length; i++) { let child = node.args[i]; // All groups of terms will be surrounded by parenthesis - if (!Node.Type.isParenthesis(child)) { + if (!NodeType.isParenthesis(child)) { continue; } child = child.content; const childStatus = termCollectorFunctions[newNode.op](child); - if (childStatus.hasChanged()) { - const status = Node.Status.childChanged(newNode, childStatus, i); + if (childStatus.hasChanged) { + const status = NodeStatus.childChanged(newNode, childStatus, i); steps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } } return steps; } - -module.exports = search; diff --git a/lib/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.js b/lib/src/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.ts similarity index 50% rename from lib/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.js rename to lib/src/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.ts index 076849d7..a0fee6bd 100644 --- a/lib/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.js +++ b/lib/src/simplifyExpression/collectAndCombineSearch/multiplyLikeTerms.ts @@ -1,77 +1,88 @@ -const arithmeticSearch = require('../arithmeticSearch'); -const checks = require('../../checks'); -const ConstantOrConstantPower = require('./ConstantOrConstantPower'); -const multiplyFractionsSearch = require('../multiplyFractionsSearch'); - -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const NthRoot = require('../functionsSearch/nthRoot'); - // Multiplies a list of nodes that are polynomial or constant power like terms. // Returns a node. // The polynomial nodes should *not* have coefficients. (multiplying // coefficients is handled in collecting like terms for multiplication) -function multiplyLikeTerms(node, polynomialOnly=false) { - if (!Node.Type.isOperator(node)) { - return Node.Status.noChange(node); +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { arithmeticSearch } from "../arithmeticSearch/ArithmeticSearch"; +import { canMultiplyLikeTermConstantNodes } from "../../checks/canMultiplyLikeTermConstantNodes"; +import { ChangeTypes } from "../../ChangeTypes"; +import { multiplyFractionsSearch } from "../multiplyFractionsSearch"; +import { canMultiplyLikeTermsNthRoots } from "../../checks/canMultiplyLikeTermsNthRoots"; +import { getRadicandNode, getRootNode } from "../functionsSearch/nthRoot"; +import { NodeCreator } from "../../node/Creator"; +import { canMultiplyLikeTermPolynomialNodes } from "../../checks/canMultiplyLikeTermPolynomialNodes"; +import { getBaseNode, getExponentNode } from "./ConstantOrConstantPower"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; + +export function multiplyLikeTerms(node, polynomialOnly = false) { + if (!NodeType.isOperator(node)) { + return NodeStatus.noChange(node); } let status; - if (!polynomialOnly && !checks.canMultiplyLikeTermConstantNodes(node)) { + if (!polynomialOnly && !canMultiplyLikeTermConstantNodes(node)) { status = arithmeticSearch(node); - if (status.hasChanged()) { + if (status.hasChanged) { status.changeType = ChangeTypes.MULTIPLY_COEFFICIENTS; return status; } status = multiplyFractionsSearch(node); - if (status.hasChanged()) { + if (status.hasChanged) { status.changeType = ChangeTypes.MULTIPLY_COEFFICIENTS; return status; } } status = multiplyPolynomialTerms(node); - if (status.hasChanged()) { + if (status.hasChanged) { status.changeType = ChangeTypes.MULTIPLY_COEFFICIENTS; return status; } status = multiplyNthRoots(node); - if (status.hasChanged()) { + if (status.hasChanged) { return status; } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } function multiplyNthRoots(node) { - if (!checks.canMultiplyLikeTermsNthRoots(node)){ - return Node.Status.noChange(node); + if (!canMultiplyLikeTermsNthRoots(node)) { + return NodeStatus.noChange(node); } let newNode = node.cloneDeep(); // Array of radicands of all the nthRoot terms being multiplied - const radicands = node.args.map(term => NthRoot.getRadicandNode(term)); + const radicands = node.args.map((term) => getRadicandNode(term)); // Multiply them - const newRadicandNode = Node.Creator.operator('*', radicands); + const newRadicandNode = NodeCreator.operator("*", radicands); // All the args at this point have the same root, // so we arbitrarily take the first one const firstArg = node.args[0]; - const rootNode = NthRoot.getRootNode(firstArg); + const rootNode = getRootNode(firstArg); - newNode = Node.Creator.nthRoot(newRadicandNode, rootNode); + newNode = NodeCreator.nthRoot(newRadicandNode, rootNode); - return Node.Status.nodeChanged(ChangeTypes.MULTIPLY_NTH_ROOTS, node, newNode, false); + return NodeStatus.nodeChanged( + ChangeTypes.MULTIPLY_NTH_ROOTS, + node, + newNode, + false + ); } function multiplyPolynomialTerms(node) { - if (!checks.canMultiplyLikeTermPolynomialNodes(node) && - !checks.canMultiplyLikeTermConstantNodes(node)) { - return Node.Status.noChange(node); + if ( + !canMultiplyLikeTermPolynomialNodes(node) && + !canMultiplyLikeTermConstantNodes(node) + ) { + return NodeStatus.noChange(node); } const substeps = []; @@ -82,22 +93,21 @@ function multiplyPolynomialTerms(node) { // (this step only happens under certain conditions and later steps might // happen even if step 1 does not) let status = addOneExponent(newNode); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } // STEP 2: collect exponents to a single exponent sum // e.g. x^1 * x^3 -> x^(1+3) // e.g. 10^2 * 10^3 -> 10^(2+3) - if (checks.canMultiplyLikeTermConstantNodes(node)) { + if (canMultiplyLikeTermConstantNodes(node)) { status = collectConstantExponents(newNode); - } - else { + } else { status = collectPolynomialExponents(newNode); } substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); // STEP 3: add exponents together. // NOTE: This might not be a step if the exponents aren't all constants, @@ -106,19 +116,23 @@ function multiplyPolynomialTerms(node) { // TODO: handle fractions, combining and collecting like terms, etc, here const exponentSum = newNode.args[1].content; const sumStatus = arithmeticSearch(exponentSum); - if (sumStatus.hasChanged()) { - status = Node.Status.childChanged(newNode, sumStatus, 1); + if (sumStatus.hasChanged) { + status = NodeStatus.childChanged(newNode, sumStatus, 1); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } - if (substeps.length === 1) { // possible if only step 2 happens + if (substeps.length === 1) { + // possible if only step 2 happens return substeps[0]; - } - else { - return Node.Status.nodeChanged( + } else { + return NodeStatus.nodeChanged( ChangeTypes.MULTIPLY_POLYNOMIAL_TERMS, - node, newNode, true, substeps); + node, + newNode, + true, + substeps + ); } } @@ -128,18 +142,19 @@ function multiplyPolynomialTerms(node) { // makes the adding exponents step clearer. // e.g. x^2 * x -> x^2 * x^1 // e.g. 10^2 * 10 -> 10^2 * 10^1 -// Returns a Node.Status object. +// Returns a Status object. function addOneExponent(node) { const newNode = node.cloneDeep(); let change = false; let changeGroup = 1; - if (checks.canMultiplyLikeTermConstantNodes(node)) { + if (canMultiplyLikeTermConstantNodes(node)) { newNode.args.forEach((child, i) => { - if (Node.Type.isConstant(child)) { // true if child is a constant node, e.g 3 - const base = ConstantOrConstantPower.getBaseNode(child); - const exponent = Node.Creator.constant(1); - newNode.args[i] = Node.Creator.operator('^', [base, exponent]); + if (NodeType.isConstant(child)) { + // true if child is a constant node, e.g 3 + const base = getBaseNode(child); + const exponent = NodeCreator.constant(1); + newNode.args[i] = NodeCreator.operator("^", [base, exponent]); newNode.args[i].changeGroup = changeGroup; node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" @@ -148,15 +163,15 @@ function addOneExponent(node) { changeGroup++; } }); - } - else { + } else { newNode.args.forEach((child, i) => { - const polyTerm = new Node.PolynomialTerm(child); + const polyTerm = new PolynomialTerm(child); if (!polyTerm.getExponentNode()) { - newNode.args[i] = Node.Creator.polynomialTerm( + newNode.args[i] = NodeCreator.polynomialTerm( polyTerm.getSymbolNode(), - Node.Creator.constant(1), - polyTerm.getCoeffNode()); + NodeCreator.constant(1), + polyTerm.getCoeffNode() + ); newNode.args[i].changeGroup = changeGroup; node.args[i].changeGroup = changeGroup; // note that this is the "oldNode" @@ -168,37 +183,43 @@ function addOneExponent(node) { } if (change) { - return Node.Status.nodeChanged( - ChangeTypes.ADD_EXPONENT_OF_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); + return NodeStatus.nodeChanged( + ChangeTypes.ADD_EXPONENT_OF_ONE, + node, + newNode, + false + ); + } else { + return NodeStatus.noChange(node); } } // Given a product of constant terms, groups the exponents into a sum // e.g. 10^2 * 10^3 -> 10^(2+3) -// Returns a Node.Status object. +// Returns a Status object. function collectConstantExponents(node) { // If we're multiplying constant nodes together, they all share the same // base. Get that from the first node. - const baseNode = ConstantOrConstantPower.getBaseNode(node.args[0]); + const baseNode = getBaseNode(node.args[0]); // The new exponent will be a sum of exponents (an operation, wrapped in // parens) e.g. 10^(3+4+5) - const exponentNodeList = node.args.map( - ConstantOrConstantPower.getExponentNode); - const newExponent = Node.Creator.parenthesis( - Node.Creator.operator('+', exponentNodeList)); - const newNode = Node.Creator.operator('^', [baseNode, newExponent]); - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_CONSTANT_EXPONENTS, node, newNode); + const exponentNodeList = node.args.map(getExponentNode); + const newExponent = NodeCreator.parenthesis( + NodeCreator.operator("+", exponentNodeList) + ); + const newNode = NodeCreator.operator("^", [baseNode, newExponent]); + return NodeStatus.nodeChanged( + ChangeTypes.COLLECT_CONSTANT_EXPONENTS, + node, + newNode + ); } // Given a product of polynomial terms, groups the exponents into a sum // e.g. x^2 * x^3 * x^1 -> x^(2 + 3 + 1) -// Returns a Node.Status object. +// Returns a Status object. function collectPolynomialExponents(node) { - const polynomialTermList = node.args.map(n => new Node.PolynomialTerm(n)); + const polynomialTermList = node.args.map((n) => new PolynomialTerm(n)); // If we're multiplying polynomial nodes together, they all share the same // symbol. Get that from the first node. @@ -206,12 +227,16 @@ function collectPolynomialExponents(node) { // The new exponent will be a sum of exponents (an operation, wrapped in // parens) e.g. x^(3+4+5) - const exponentNodeList = polynomialTermList.map(p => p.getExponentNode(true)); - const newExponent = Node.Creator.parenthesis( - Node.Creator.operator('+', exponentNodeList)); - const newNode = Node.Creator.polynomialTerm(symbolNode, newExponent, null); - return Node.Status.nodeChanged( - ChangeTypes.COLLECT_POLYNOMIAL_EXPONENTS, node, newNode); + const exponentNodeList = polynomialTermList.map((p) => + p.getExponentNode(true) + ); + const newExponent = NodeCreator.parenthesis( + NodeCreator.operator("+", exponentNodeList) + ); + const newNode = NodeCreator.polynomialTerm(symbolNode, newExponent, null); + return NodeStatus.nodeChanged( + ChangeTypes.COLLECT_POLYNOMIAL_EXPONENTS, + node, + newNode + ); } - -module.exports = multiplyLikeTerms; diff --git a/lib/simplifyExpression/distributeSearch/index.js b/lib/src/simplifyExpression/distributeSearch/index.ts similarity index 54% rename from lib/simplifyExpression/distributeSearch/index.js rename to lib/src/simplifyExpression/distributeSearch/index.ts index b02d2dd6..26e24374 100644 --- a/lib/simplifyExpression/distributeSearch/index.js +++ b/lib/src/simplifyExpression/distributeSearch/index.ts @@ -1,118 +1,132 @@ -const arithmeticSearch = require('../arithmeticSearch'); -const collectAndCombineSearch = require('../collectAndCombineSearch'); -const rearrangeCoefficient = require('../basicsSearch/rearrangeCoefficient'); - -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const search = TreeSearch.postOrder(distribute); +import { ChangeTypes } from "../../ChangeTypes"; +import { Negative } from "../../Negative"; +import { TreeSearch } from "../../TreeSearch"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; +import { arithmeticSearch } from "../arithmeticSearch/ArithmeticSearch"; +import { rearrangeCoefficient } from "../basicsSearch/rearrangeCoefficient"; +import { collectAndCombineSearch } from "../collectAndCombineSearch"; + +export const distributeSearch = TreeSearch.postOrder(distribute); // Distributes through parenthesis. // e.g. 2(x+3) -> (2*x + 2*3) // e.g. -(x+5) -> (-x + -5) -// Returns a Node.Status object. +// Returns a Status object. function distribute(node) { - if (Node.Type.isUnaryMinus(node)) { + if (NodeType.isUnaryMinus(node)) { return distributeUnaryMinus(node); - } - else if (Node.Type.isOperator(node, '*')) { + } else if (NodeType.isOperator(node, "*")) { return distributeAndSimplifyMultiplication(node); - } - else if (Node.Type.isOperator(node, '^')) { + } else if (NodeType.isOperator(node, "^")) { return expandBase(node); - } - else { - return Node.Status.noChange(node); + } else { + return NodeStatus.noChange(node); } } // Expand a power node with a non-constant base and a positive exponent > 1 // e.g. (nthRoot(x, 2))^2 -> nthRoot(x, 2) * nthRoot(x, 2) // e.g. (2x + 3)^2 -> (2x + 3) (2x + 3) -function expandBase (node) { +function expandBase(node) { // Must be a power node and the exponent must be a constant // Base must either be an nthRoot or sum of terms - if (!Node.Type.isOperator(node, '^')) { - return Node.Status.noChange(node); + if (!NodeType.isOperator(node, "^")) { + return NodeStatus.noChange(node); } - const base = Node.Type.isParenthesis(node.args[0]) - ? node.args[0].content - : node.args[0]; + const base = NodeType.isParenthesis(node.args[0]) + ? node.args[0].content + : node.args[0]; - const exponent = Node.Type.isParenthesis(node.args[1]) - ? node.args[1].content - : node.args[1]; + const exponent = NodeType.isParenthesis(node.args[1]) + ? node.args[1].content + : node.args[1]; const exponentValue = parseFloat(exponent.value); // Exponent should be a positive integer if (!(Number.isInteger(exponentValue) && exponentValue > 1)) { - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } - if (!Node.Type.isFunction(base, 'nthRoot') && !Node.Type.isOperator(base, '+')) { - return Node.Status.noChange(node); + if ( + !NodeType.isFunction(base, "nthRoot") && + !NodeType.isOperator(base, "+") + ) { + return NodeStatus.noChange(node); } // If the base is an nthRoot node, it doesn't need the parenthesis - const expandedBase = Node.Type.isFunction(base, 'nthRoot') - ? base - : node.args[0]; - - const expandedNode = Node.Creator.operator('*', Array(parseFloat(exponent.value)).fill(expandedBase)); - - return Node.Status.nodeChanged( - ChangeTypes.EXPAND_EXPONENT, node, expandedNode, false); + const expandedBase = NodeType.isFunction(base, "nthRoot") + ? base + : node.args[0]; + + const expandedNode = NodeCreator.operator( + "*", + Array(parseFloat(exponent.value)).fill(expandedBase) + ); + + return NodeStatus.nodeChanged( + ChangeTypes.EXPAND_EXPONENT, + node, + expandedNode, + false + ); } // Distributes unary minus into a parenthesis node. // e.g. -(4*9*x^2) --> (-4 * 9 * x^2) // e.g. -(x + y - 5) --> (-x + -y + 5) -// Returns a Node.Status object. +// Returns a Status object. function distributeUnaryMinus(node) { - if (!Node.Type.isUnaryMinus(node)) { - return Node.Status.noChange(node); + if (!NodeType.isUnaryMinus(node)) { + return NodeStatus.noChange(node); } const unaryContent = node.args[0]; - if (!Node.Type.isParenthesis(unaryContent)) { - return Node.Status.noChange(node); + if (!NodeType.isParenthesis(unaryContent)) { + return NodeStatus.noChange(node); } const content = unaryContent.content; - if (!Node.Type.isOperator(content)) { - return Node.Status.noChange(node); + if (!NodeType.isOperator(content)) { + return NodeStatus.noChange(node); } const newContent = content.cloneDeep(); node.changeGroup = 1; // For multiplication and division, we can push the unary minus in to // the first argument. // e.g. -(2/3) -> (-2/3) -(4*9*x^2) --> (-4 * 9 * x^2) - if (content.op === '*' || content.op === '/') { + if (content.op === "*" || content.op === "/") { newContent.args[0] = Negative.negate(newContent.args[0]); newContent.args[0].changeGroup = 1; - const newNode = Node.Creator.parenthesis(newContent); - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE_NEGATIVE_ONE, node, newNode, false); - } - else if (content.op === '+') { + const newNode = NodeCreator.parenthesis(newContent); + return NodeStatus.nodeChanged( + ChangeTypes.DISTRIBUTE_NEGATIVE_ONE, + node, + newNode, + false + ); + } else if (content.op === "+") { // Now we know `node` is of the form -(x + y + ...). // We want to now return (-x + -y + ....) // If any term is negative, we make it positive it right away // e.g. -(2-4) => -2 + 4 - const newArgs = newContent.args.map(arg => { + const newArgs = newContent.args.map((arg) => { const newArg = Negative.negate(arg); newArg.changeGroup = 1; return newArg; }); newContent.args = newArgs; - const newNode = Node.Creator.parenthesis(newContent); - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE_NEGATIVE_ONE, node, newNode, false); - } - else { - return Node.Status.noChange(node); + const newNode = NodeCreator.parenthesis(newContent); + return NodeStatus.nodeChanged( + ChangeTypes.DISTRIBUTE_NEGATIVE_ONE, + node, + newNode, + false + ); + } else { + return NodeStatus.noChange(node); } } @@ -120,77 +134,89 @@ function distributeUnaryMinus(node) { // can be distributed. To be distributed, there must be two terms beside // each other, and at least one of them must be a parenthesis node. // e.g. 2*(3+x) or (4+x^2+x^3)*(x+3) -// Returns a Node.Status object with substeps +// Returns a Status object with substeps function distributeAndSimplifyMultiplication(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { - return Node.Status.noChange(node); + if (!NodeType.isOperator(node) || node.op !== "*") { + return NodeStatus.noChange(node); } // STEP 1: distribute with `distributeTwoNodes` // e.g. x*(2+x) -> x*2 + x*x // STEP 2: simplifications of each operand in the new sum with `simplify` // e.g. x*2 + x*x -> ... -> 2x + x^2 - for (let i = 0; i+1 < node.args.length; i++) { - if (!isParenthesisOfAddition(node.args[i]) && - !isParenthesisOfAddition(node.args[i+1])) { + for (let i = 0; i + 1 < node.args.length; i++) { + if ( + !isParenthesisOfAddition(node.args[i]) && + !isParenthesisOfAddition(node.args[i + 1]) + ) { continue; } let newNode = node.cloneDeep(); const substeps = []; let status; - const combinedNode = distributeTwoNodes(newNode.args[i], newNode.args[i+1]); + const combinedNode = distributeTwoNodes( + newNode.args[i], + newNode.args[i + 1] + ); node.args[i].changeGroup = 1; - node.args[i+1].changeGroup = 1; + node.args[i + 1].changeGroup = 1; combinedNode.changeGroup = 1; if (newNode.args.length > 2) { newNode.args.splice(i, 2, combinedNode); newNode.args[i].changeGroup = 1; - } - else { + } else { newNode = combinedNode; newNode.changeGroup = 1; } - status = Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE, node, newNode, false); + status = NodeStatus.nodeChanged( + ChangeTypes.DISTRIBUTE, + node, + newNode, + false + ); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); // case 1: there were more than two operands in this multiplication // e.g. 3*7*(2+x)*(3+x)*(4+x) is a multiplication node with 5 children // and the new node will be 3*(14+7x)*(3+x)*(4+x) with 4 children. - if (Node.Type.isOperator(newNode, '*')) { + if (NodeType.isOperator(newNode, "*")) { const childStatus = simplifyWithParens(newNode.args[i]); - if (childStatus.hasChanged()) { - status = Node.Status.childChanged(newNode, childStatus, i); + if (childStatus.hasChanged) { + status = NodeStatus.childChanged(newNode, childStatus, i); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } } // case 2: there were only two operands and we multiplied them together. // e.g. 7*(2+x) -> (7*2 + 7*x) // Now we can just simplify it. - else if (Node.Type.isParenthesis(newNode)){ + else if (NodeType.isParenthesis(newNode)) { status = simplifyWithParens(newNode); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } - } - else { - throw Error('Unsupported node type for distribution: ' + node); + } else { + throw Error("Unsupported node type for distribution: " + node); } if (substeps.length === 1) { return substeps[0]; } - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE, node, newNode, false, substeps); + return NodeStatus.nodeChanged( + ChangeTypes.DISTRIBUTE, + node, + newNode, + false, + substeps + ); } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Distributes two nodes together. At least one node must be parenthesis node @@ -202,15 +228,13 @@ function distributeTwoNodes(firstNode, secondNode) { let firstArgs, secondArgs; if (isParenthesisOfAddition(firstNode)) { firstArgs = firstNode.content.args; - } - else { + } else { firstArgs = [firstNode]; } if (isParenthesisOfAddition(secondNode)) { secondArgs = secondNode.content.args; - } - else { + } else { secondArgs = [secondNode]; } // the new operands under addition, now products of terms @@ -225,12 +249,14 @@ function distributeTwoNodes(firstNode, secondNode) { fractionNodes.forEach((node) => { let arg; if (isFraction(node)) { - let numerator = Node.Creator.operator('*', [node.args[0], nonFractionTerm]); - numerator = Node.Creator.parenthesis(numerator); - arg = Node.Creator.operator('/', [numerator, node.args[1]]); - } - else { - arg = Node.Creator.operator('*', [node, nonFractionTerm]); + let numerator = NodeCreator.operator("*", [ + node.args[0], + nonFractionTerm, + ]); + numerator = NodeCreator.parenthesis(numerator); + arg = NodeCreator.operator("/", [numerator, node.args[1]]); + } else { + arg = NodeCreator.operator("*", [node, nonFractionTerm]); } arg.changeGroup = 1; newArgs.push(arg); @@ -239,23 +265,22 @@ function distributeTwoNodes(firstNode, secondNode) { // e.g. (4+x)(x+y+z) will become 4(x+y+z) + x(x+y+z) as an intermediate // step. else if (firstArgs.length > 1 && secondArgs.length > 1) { - firstArgs.forEach(leftArg => { - const arg = Node.Creator.operator('*', [leftArg, secondNode]); + firstArgs.forEach((leftArg) => { + const arg = NodeCreator.operator("*", [leftArg, secondNode]); arg.changeGroup = 1; newArgs.push(arg); }); - } - else { + } else { // a list of all pairs of nodes between the two arg lists - firstArgs.forEach(leftArg => { - secondArgs.forEach(rightArg => { - const arg = Node.Creator.operator('*', [leftArg, rightArg]); + firstArgs.forEach((leftArg) => { + secondArgs.forEach((rightArg) => { + const arg = NodeCreator.operator("*", [leftArg, rightArg]); arg.changeGroup = 1; newArgs.push(arg); }); }); } - return Node.Creator.parenthesis(Node.Creator.operator('+', newArgs)); + return NodeCreator.parenthesis(NodeCreator.operator("+", newArgs)); } function hasFraction(args) { @@ -263,7 +288,7 @@ function hasFraction(args) { } function isFraction(node) { - return Node.Type.isOperator(node, '/'); + return NodeType.isOperator(node, "/"); } // Simplifies a sum of terms (a result of distribution) that's in parens @@ -271,31 +296,30 @@ function isFraction(node) { // e.g. 2x*(4 + x) distributes to (2x*4 + 2x*x) // This is a separate function from simplify to make the flow more readable, // but this is literally just a wrapper around 'simplify'. -// Returns a Node.Status object +// Returns a Status object function simplifyWithParens(node) { - if (!Node.Type.isParenthesis(node)) { - throw Error('expected ' + node + ' to be a parenthesis node'); + if (!NodeType.isParenthesis(node)) { + throw Error("expected " + node + " to be a parenthesis node"); } const status = simplify(node.content); - if (status.hasChanged()) { - return Node.Status.childChanged(node, status); - } - else { - return Node.Status.noChange(node); + if (status.hasChanged) { + return NodeStatus.childChanged(node, status); + } else { + return NodeStatus.noChange(node); } } // Simplifies a sum of terms that are a result of distribution. // e.g. (2x+3)*(4x+5) -distribute-> 2x*(4x+5) + 3*(4x+5) <- 2 terms to simplify // e.g. 2x*(4x+5) --distribute--> 2x*4x + 2x*5 --simplify--> 8x^2 + 10x -// Returns a Node.Status object. +// Returns a Status object. function simplify(node) { const substeps = []; const simplifyFunctions = [ - arithmeticSearch, // e.g. 2*9 -> 18 - rearrangeCoefficient, // e.g. x*5 -> 5x - collectAndCombineSearch, // e.g 2x*4x -> 8x^2 + arithmeticSearch, // e.g. 2*9 -> 18 + rearrangeCoefficient, // e.g. x*5 -> 5x + collectAndCombineSearch, // e.g 2x*4x -> 8x^2 distributeAndSimplifyMultiplication, // e.g. (2+x)(3+x) -> 2*(3+x) recurses ]; @@ -303,31 +327,33 @@ function simplify(node) { for (let i = 0; i < newNode.args.length; i++) { for (let j = 0; j < simplifyFunctions.length; j++) { const childStatus = simplifyFunctions[j](newNode.args[i]); - if (childStatus.hasChanged()) { - const status = Node.Status.childChanged(newNode, childStatus, i); + if (childStatus.hasChanged) { + const status = NodeStatus.childChanged(newNode, childStatus, i); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } } } // possible in cases like 2(x + y) -> 2x + 2y -> doesn't need simplifying if (substeps.length === 0) { - return Node.Status.noChange(node); - } - else { - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_TERMS, node, newNode, false, substeps); + return NodeStatus.noChange(node); + } else { + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_TERMS, + node, + newNode, + false, + substeps + ); } } // returns true if `node` is of the type (node + node + ...) function isParenthesisOfAddition(node) { - if (!Node.Type.isParenthesis(node)) { + if (!NodeType.isParenthesis(node)) { return false; } const content = node.content; - return Node.Type.isOperator(content, '+'); + return NodeType.isOperator(content, "+"); } - -module.exports = search; diff --git a/lib/simplifyExpression/divisionSearch/index.js b/lib/src/simplifyExpression/divisionSearch/index.ts similarity index 57% rename from lib/simplifyExpression/divisionSearch/index.js rename to lib/src/simplifyExpression/divisionSearch/index.ts index ffd78b92..0d96c3cf 100644 --- a/lib/simplifyExpression/divisionSearch/index.js +++ b/lib/src/simplifyExpression/divisionSearch/index.ts @@ -1,26 +1,28 @@ -const ChangeTypes = require('../../ChangeTypes'); -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); +import { ChangeTypes } from "../../ChangeTypes"; +import { TreeSearch } from "../../TreeSearch"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; // Searches for and simplifies any chains of division or nested division. -// Returns a Node.Status object -const search = TreeSearch.preOrder(division); +// Returns a Status object +export const divisionSearch = TreeSearch.preOrder(division); function division(node) { - if (!Node.Type.isOperator(node) || node.op !== '/') { - return Node.Status.noChange(node); + if (!NodeType.isOperator(node) || node.op !== "/") { + return NodeStatus.noChange(node); } // e.g. 2/(x/6) => 2 * 6/x - let nodeStatus = multiplyByInverse(node); - if (nodeStatus.hasChanged()) { + let nodeStatus = multiplyByInverse(node); + if (nodeStatus.hasChanged) { return nodeStatus; } // e.g. 2/x/6 -> 2/(x*6) nodeStatus = simplifyDivisionChain(node); - if (nodeStatus.hasChanged()) { + if (nodeStatus.hasChanged) { return nodeStatus; } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // If `node` is a fraction with a denominator that is also a fraction, multiply @@ -28,27 +30,28 @@ function division(node) { // e.g. x/(2/3) -> x * 3/2 function multiplyByInverse(node) { let denominator = node.args[1]; - if (Node.Type.isParenthesis(denominator)) { + if (NodeType.isParenthesis(denominator)) { denominator = denominator.content; } - if (!Node.Type.isOperator(denominator) || denominator.op !== '/') { - return Node.Status.noChange(node); + if (!NodeType.isOperator(denominator) || denominator.op !== "/") { + return NodeStatus.noChange(node); } // At this point, we know that node is a fraction and denonimator is the // fraction we need to inverse. const inverseNumerator = denominator.args[1]; const inverseDenominator = denominator.args[0]; - const inverseFraction = Node.Creator.operator( - '/', [inverseNumerator, inverseDenominator]); + const inverseFraction = NodeCreator.operator("/", [ + inverseNumerator, + inverseDenominator, + ]); - const newNode = Node.Creator.operator('*', [node.args[0], inverseFraction]); - return Node.Status.nodeChanged( - ChangeTypes.MULTIPLY_BY_INVERSE, node, newNode); + const newNode = NodeCreator.operator("*", [node.args[0], inverseFraction]); + return NodeStatus.nodeChanged(ChangeTypes.MULTIPLY_BY_INVERSE, node, newNode); } // Simplifies any chains of division into a single division operation. // e.g. 2/x/6 -> 2/(x*6) -// Returns a Node.Status object +// Returns a Status object function simplifyDivisionChain(node) { // check for a chain of division const denominatorList = getDenominatorList(node); @@ -57,13 +60,13 @@ function simplifyDivisionChain(node) { const numerator = denominatorList.shift(); // the new single denominator is all the chained denominators // multiplied together, in parentheses. - const denominator = Node.Creator.parenthesis( - Node.Creator.operator('*', denominatorList)); - const newNode = Node.Creator.operator('/', [numerator, denominator]); - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_DIVISION, node, newNode); + const denominator = NodeCreator.parenthesis( + NodeCreator.operator("*", denominatorList) + ); + const newNode = NodeCreator.operator("/", [numerator, denominator]); + return NodeStatus.nodeChanged(ChangeTypes.SIMPLIFY_DIVISION, node, newNode); } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Given a the denominator of a division node, returns all the nested @@ -72,7 +75,7 @@ function simplifyDivisionChain(node) { function getDenominatorList(denominator) { let node = denominator; const denominatorList = []; - while (node.op === '/') { + while (node.op === "/") { // unshift the denominator to the front of the list, and recurse on // the numerator denominatorList.unshift(node.args[1]); @@ -82,5 +85,3 @@ function getDenominatorList(denominator) { denominatorList.unshift(node); return denominatorList; } - -module.exports = search; diff --git a/lib/simplifyExpression/fractionsSearch/addConstantAndFraction.js b/lib/src/simplifyExpression/fractionsSearch/addConstantAndFraction.ts similarity index 54% rename from lib/simplifyExpression/fractionsSearch/addConstantAndFraction.js rename to lib/src/simplifyExpression/fractionsSearch/addConstantAndFraction.ts index 405bbb23..8862d509 100644 --- a/lib/simplifyExpression/fractionsSearch/addConstantAndFraction.js +++ b/lib/src/simplifyExpression/fractionsSearch/addConstantAndFraction.ts @@ -1,42 +1,41 @@ -const addConstantFractions = require('./addConstantFractions'); +import { addConstantFractions } from "./addConstantFractions"; -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); +import { ChangeTypes } from "../../ChangeTypes"; +import { evaluate } from "../../util/evaluate"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; +import { MathNode } from "mathjs"; // Adds a constant to a fraction by: // - collapsing the fraction to decimal if the constant is not an integer // e.g. 5.3 + 1/2 -> 5.3 + 0.2 // - turning the constant into a fraction with the same denominator if it is // an integer, e.g. 5 + 1/2 -> 10/2 + 1/2 -function addConstantAndFraction(node) { - if (!Node.Type.isOperator(node) || node.op !== '+' || node.args.length !== 2) { - return Node.Status.noChange(node); +export function addConstantAndFraction(node: MathNode): NodeStatus { + if (!NodeType.isOperator(node) || node.op !== "+" || node.args.length !== 2) { + return NodeStatus.noChange(node); } const firstArg = node.args[0]; const secondArg = node.args[1]; let constNode, fractionNode; - if (Node.Type.isConstant(firstArg)) { - if (Node.Type.isIntegerFraction(secondArg)) { + if (NodeType.isConstant(firstArg)) { + if (NodeType.isIntegerFraction(secondArg)) { constNode = firstArg; fractionNode = secondArg; + } else { + return NodeStatus.noChange(node); } - else { - return Node.Status.noChange(node); - } - } - else if (Node.Type.isConstant(secondArg)) { - if (Node.Type.isIntegerFraction(firstArg)) { + } else if (NodeType.isConstant(secondArg)) { + if (NodeType.isIntegerFraction(firstArg)) { constNode = secondArg; fractionNode = firstArg; + } else { + return NodeStatus.noChange(node); } - else { - return Node.Status.noChange(node); - } - } - else { - return Node.Status.noChange(node); + } else { + return NodeStatus.noChange(node); } let newNode = node.cloneDeep(); @@ -50,38 +49,38 @@ function addConstantAndFraction(node) { const denominatorNode = fractionNode.args[1]; const denominatorValue = parseInt(denominatorNode); const constNodeValue = parseInt(constNode.value); - const newNumeratorNode = Node.Creator.constant( - constNodeValue * denominatorValue); - newConstNode = Node.Creator.operator( - '/', [newNumeratorNode, denominatorNode]); + const newNumeratorNode = NodeCreator.constant( + constNodeValue * denominatorValue + ); + newConstNode = NodeCreator.operator("/", [ + newNumeratorNode, + denominatorNode, + ]); newFractionNode = fractionNode; changeType = ChangeTypes.CONVERT_INTEGER_TO_FRACTION; - } - else { + } else { // round to 4 decimal places let dividedValue = evaluate(fractionNode); if (dividedValue < 1) { dividedValue = parseFloat(dividedValue.toPrecision(4)); - } - else { + } else { dividedValue = parseFloat(dividedValue.toFixed(4)); } - newFractionNode = Node.Creator.constant(dividedValue); + newFractionNode = NodeCreator.constant(dividedValue); newConstNode = constNode; changeType = ChangeTypes.DIVIDE_FRACTION_FOR_ADDITION; } - if (Node.Type.isConstant(firstArg)) { + if (NodeType.isConstant(firstArg)) { newNode.args[0] = newConstNode; newNode.args[1] = newFractionNode; - } - else { + } else { newNode.args[0] = newFractionNode; newNode.args[1] = newConstNode; } - substeps.push(Node.Status.nodeChanged(changeType, node, newNode)); - newNode = Node.Status.resetChangeGroups(newNode); + substeps.push(NodeStatus.nodeChanged(changeType, node, newNode)); + newNode = NodeStatus.resetChangeGroups(newNode); // If we changed an integer to a fraction, we need to add the steps for // adding the fractions. @@ -91,16 +90,20 @@ function addConstantAndFraction(node) { } // Otherwise, add the two constants else { - const evalNode = Node.Creator.constant(evaluate(newNode)); - substeps.push(Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, newNode, evalNode)); + const evalNode = NodeCreator.constant(evaluate(newNode)); + substeps.push( + NodeStatus.nodeChanged(ChangeTypes.SIMPLIFY_ARITHMETIC, newNode, evalNode) + ); } const lastStep = substeps[substeps.length - 1]; - newNode = Node.Status.resetChangeGroups(lastStep.newNode); + newNode = NodeStatus.resetChangeGroups(lastStep.newNode); - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_ARITHMETIC, node, newNode, true, substeps); + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_ARITHMETIC, + node, + newNode, + true, + substeps + ); } - -module.exports = addConstantAndFraction; diff --git a/lib/src/simplifyExpression/fractionsSearch/addConstantFractions.ts b/lib/src/simplifyExpression/fractionsSearch/addConstantFractions.ts new file mode 100644 index 00000000..f983a36f --- /dev/null +++ b/lib/src/simplifyExpression/fractionsSearch/addConstantFractions.ts @@ -0,0 +1,186 @@ +import { divideByGCD } from "./divideByGCD"; +import * as math from "mathjs"; + +import { ChangeTypes } from "../../ChangeTypes"; +import { evaluate } from "../../util/evaluate"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; +import { MathNode } from "mathjs"; + +// Adds constant fractions -- can start from either step 1 or 2 +// 1A. Find the LCD if denominators are different and multiplies to make +// denominators equal, e.g. 2/3 + 4/6 --> (2*2)/(3*2) + 4/6 +// 1B. Multiplies out to make constant fractions again +// e.g. (2*2)/(3*2) + 4/6 -> 4/6 + 4/6 +// 2A. Combines numerators, e.g. 4/6 + 4/6 -> e.g. 2/5 + 4/5 --> (2+4)/5 +// 2B. Adds numerators together, e.g. (2+4)/5 -> 6/5 +// Returns a Status object with substeps +export function addConstantFractions(node: MathNode): NodeStatus { + let newNode = node.cloneDeep(); + + if (!NodeType.isOperator(node) || node.op !== "+") { + return NodeStatus.noChange(node); + } + if (!node.args.every((n) => NodeType.isIntegerFraction(n, true))) { + return NodeStatus.noChange(node); + } + const denominators = node.args.map((fraction) => { + return parseFloat(evaluate(fraction.args[1])); + }); + + const substeps = []; + let status; + + // 1A. First create the common denominator if needed + // e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) + if (!denominators.every((denominator) => denominator === denominators[0])) { + status = makeCommonDenominator(newNode); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // 1B. Multiply out the denominators + status = evaluateDenominators(newNode); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // 1B. Multiply out the numerators + status = evaluateNumerators(newNode); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + } + + // 2A. Now that they all have the same denominator, combine the numerators + // e.g. 2/3 + 5/3 -> (2+5)/3 + status = combineNumeratorsAboveCommonDenominator(newNode); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // 2B. Finally, add the numerators together + status = addNumeratorsTogether(newNode); + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + // 2C. If the numerator is 0, simplify to just 0 + status = reduceNumerator(newNode); + if (status.hasChanged) { + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + } + + // 2D. If we can simplify the fraction, do so + status = divideByGCD(newNode); + if (status.hasChanged) { + substeps.push(status); + newNode = NodeStatus.resetChangeGroups(status.newNode); + } + + return NodeStatus.nodeChanged( + ChangeTypes.ADD_FRACTIONS, + node, + newNode, + true, + substeps + ); +} + +// Given a + operation node with a list of fraction nodes as args that all have +// the same denominator, add them together. e.g. 2/3 + 5/3 -> (2+5)/3 +// Returns the new node. +function combineNumeratorsAboveCommonDenominator(node) { + let newNode = node.cloneDeep(); + + const commonDenominator = newNode.args[0].args[1]; + const numeratorArgs = []; + newNode.args.forEach((fraction) => { + numeratorArgs.push(fraction.args[0]); + }); + const newNumerator = NodeCreator.parenthesis( + NodeCreator.operator("+", numeratorArgs) + ); + + newNode = NodeCreator.operator("/", [newNumerator, commonDenominator]); + return NodeStatus.nodeChanged(ChangeTypes.COMBINE_NUMERATORS, node, newNode); +} + +// Given a node with a numerator that is an addition node, will add +// all the numerators and return the result +function addNumeratorsTogether(node) { + const newNode = node.cloneDeep(); + + newNode.args[0] = NodeCreator.constant(evaluate(newNode.args[0])); + return NodeStatus.nodeChanged(ChangeTypes.ADD_NUMERATORS, node, newNode); +} + +function reduceNumerator(node) { + let newNode = node.cloneDeep(); + + if (newNode.args[0].value === "0") { + newNode = NodeCreator.constant(0); + return NodeStatus.nodeChanged( + ChangeTypes.REDUCE_ZERO_NUMERATOR, + node, + newNode + ); + } + + return NodeStatus.noChange(node); +} + +// Takes `node`, a sum of fractions, and returns a node that's a sum of +// fractions with denominators that evaluate to the same common denominator +// e.g. 2/6 + 1/4 -> (2*2)/(6*2) + (1*3)/(4*3) +// Returns the new node. +function makeCommonDenominator(node) { + const newNode = node.cloneDeep(); + + const denominators = newNode.args.map((fraction) => { + return parseFloat(fraction.args[1].value); + }); + const commonDenominator = (math.lcm as any)(...denominators); + + newNode.args.forEach((child, i) => { + // missingFactor is what we need to multiply the top and bottom by + // so that the denominator is the LCD + const missingFactor = commonDenominator / denominators[i]; + if (missingFactor !== 1) { + const missingFactorNode = NodeCreator.constant(missingFactor); + const newNumerator = NodeCreator.parenthesis( + NodeCreator.operator("*", [child.args[0], missingFactorNode]) + ); + const newDeominator = NodeCreator.parenthesis( + NodeCreator.operator("*", [child.args[1], missingFactorNode]) + ); + newNode.args[i] = NodeCreator.operator("/", [ + newNumerator, + newDeominator, + ]); + } + }); + + return NodeStatus.nodeChanged(ChangeTypes.COMMON_DENOMINATOR, node, newNode); +} + +function evaluateDenominators(node) { + const newNode = node.cloneDeep(); + + newNode.args.map((fraction) => { + fraction.args[1] = NodeCreator.constant(evaluate(fraction.args[1])); + }); + + return NodeStatus.nodeChanged( + ChangeTypes.MULTIPLY_DENOMINATORS, + node, + newNode + ); +} + +function evaluateNumerators(node) { + const newNode = node.cloneDeep(); + + newNode.args.map((fraction) => { + fraction.args[0] = NodeCreator.constant(evaluate(fraction.args[0])); + }); + + return NodeStatus.nodeChanged(ChangeTypes.MULTIPLY_NUMERATORS, node, newNode); +} diff --git a/lib/simplifyExpression/fractionsSearch/cancelLikeTerms.js b/lib/src/simplifyExpression/fractionsSearch/cancelLikeTerms.ts similarity index 66% rename from lib/simplifyExpression/fractionsSearch/cancelLikeTerms.js rename to lib/src/simplifyExpression/fractionsSearch/cancelLikeTerms.ts index 9f4fb550..ea851ec0 100644 --- a/lib/simplifyExpression/fractionsSearch/cancelLikeTerms.js +++ b/lib/src/simplifyExpression/fractionsSearch/cancelLikeTerms.ts @@ -1,13 +1,21 @@ -const divideByGCD = require('./divideByGCD'); -const print = require('../../util/print'); +import { divideByGCD } from "./divideByGCD"; -const ChangeTypes = require('../../ChangeTypes'); -const Negative = require('../../Negative'); -const Node = require('../../node'); +import { ChangeTypes } from "../../ChangeTypes"; +import { Negative } from "../../Negative"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; +import { printAscii } from "../../util/print"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { MathNode } from "mathjs"; // Used for cancelTerms to return a (possibly updated) numerator and denominator class CancelOutStatus { - constructor(numerator, denominator, hasChanged=false) { + constructor( + private numerator, + private denominator, + private hasChanged = false + ) { this.numerator = numerator; this.denominator = denominator; this.hasChanged = hasChanged; @@ -16,10 +24,10 @@ class CancelOutStatus { // Cancels like terms in a fraction node // e.g. (2x^2 * 5) / 2x^2 => 5 / 1 -// Returns a Node.Status object -function cancelLikeTerms(node) { - if (!Node.Type.isOperator(node) || node.op !== '/') { - return Node.Status.noChange(node); +// Returns a Status object +export function cancelLikeTerms(node: MathNode): NodeStatus { + if (!NodeType.isOperator(node) || node.op !== "/") { + return NodeStatus.noChange(node); } let newNode = node.cloneDeep(); @@ -27,25 +35,24 @@ function cancelLikeTerms(node) { const denominator = newNode.args[1]; // case 1: neither the numerator or denominator is a multiplication of terms - if (!isMultiplicationOfTerms(numerator) && - !isMultiplicationOfTerms(denominator)) { + if ( + !isMultiplicationOfTerms(numerator) && + !isMultiplicationOfTerms(denominator) + ) { const cancelStatus = cancelTerms(numerator, denominator); if (cancelStatus.hasChanged) { - newNode.args[0] = cancelStatus.numerator || Node.Creator.constant(1); + newNode.args[0] = cancelStatus.numerator || NodeCreator.constant(1); if (cancelStatus.denominator) { newNode.args[1] = cancelStatus.denominator; - } - else { + } else { // If we cancelled out the denominator, the node is now its numerator // e.g. (2x*y) / 2x => y (note y isn't a fraction) newNode = newNode.args[0]; } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); - } - else { - return Node.Status.noChange(node); + return NodeStatus.nodeChanged(ChangeTypes.CANCEL_TERMS, node, newNode); + } else { + return NodeStatus.noChange(node); } } @@ -53,10 +60,13 @@ function cancelLikeTerms(node) { // e.g. (2x^2 * 5) / 2x^2 => 5 / 1 // e.g. (x^2*y) / x => x^(2 - 1) * y (<-- note that the denominator goes // away because we always adjust the exponent in the numerator) - else if (isMultiplicationOfTerms(numerator) && - !isMultiplicationOfTerms(denominator)) { - const numeratorArgs = Node.Type.isParenthesis(numerator) ? - numerator.content.args : numerator.args; + else if ( + isMultiplicationOfTerms(numerator) && + !isMultiplicationOfTerms(denominator) + ) { + const numeratorArgs = NodeType.isParenthesis(numerator) + ? numerator.content.args + : numerator.args; for (let i = 0; i < numeratorArgs.length; i++) { const cancelStatus = cancelTerms(numeratorArgs[i], denominator); if (cancelStatus.hasChanged) { @@ -75,30 +85,31 @@ function cancelLikeTerms(node) { } if (cancelStatus.denominator) { newNode.args[1] = cancelStatus.denominator; - } - else { + } else { // If we cancelled out the denominator, the node is now its numerator // e.g. (2x*y) / 2x => y (note y isn't a fraction) newNode = newNode.args[0]; } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); + return NodeStatus.nodeChanged(ChangeTypes.CANCEL_TERMS, node, newNode); } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // case 3: denominator is a multiplication of terms and numerator is not // e.g. 2x^2 / (2x^2 * 5) => 1 / 5 // e.g. x / (x^2*y) => x^(1-2) / y - else if (isMultiplicationOfTerms(denominator) - && !isMultiplicationOfTerms(numerator)) { - const denominatorArgs = Node.Type.isParenthesis(denominator) ? - denominator.content.args : denominator.args; + else if ( + isMultiplicationOfTerms(denominator) && + !isMultiplicationOfTerms(numerator) + ) { + const denominatorArgs = NodeType.isParenthesis(denominator) + ? denominator.content.args + : denominator.args; for (let i = 0; i < denominatorArgs.length; i++) { const cancelStatus = cancelTerms(numerator, denominatorArgs[i]); if (cancelStatus.hasChanged) { - newNode.args[0] = cancelStatus.numerator || Node.Creator.constant(1); + newNode.args[0] = cancelStatus.numerator || NodeCreator.constant(1); if (cancelStatus.denominator) { denominatorArgs[i] = cancelStatus.denominator; } @@ -112,19 +123,20 @@ function cancelLikeTerms(node) { newNode.args[1] = denominatorArgs[0]; } } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); + return NodeStatus.nodeChanged(ChangeTypes.CANCEL_TERMS, node, newNode); } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // case 4: the numerator and denominator are both multiplications of terms else { - const numeratorArgs = Node.Type.isParenthesis(numerator) ? - numerator.content.args : numerator.args; - const denominatorArgs = Node.Type.isParenthesis(denominator) ? - denominator.content.args : denominator.args; + const numeratorArgs = NodeType.isParenthesis(numerator) + ? numerator.content.args + : numerator.args; + const denominatorArgs = NodeType.isParenthesis(denominator) + ? denominator.content.args + : denominator.args; for (let i = 0; i < numeratorArgs.length; i++) { for (let j = 0; j < denominatorArgs.length; j++) { const cancelStatus = cancelTerms(numeratorArgs[i], denominatorArgs[j]); @@ -155,12 +167,15 @@ function cancelLikeTerms(node) { newNode.args[1] = denominatorArgs[0]; } } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_TERMS, node, newNode); + return NodeStatus.nodeChanged( + ChangeTypes.CANCEL_TERMS, + node, + newNode + ); } } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } } @@ -172,45 +187,41 @@ function cancelLikeTerms(node) { // returned as null. e.g. 4, 4x => null, x function cancelTerms(numerator, denominator) { // Deal with unary minuses by recursing on the argument - if (Node.Type.isUnaryMinus(numerator)) { + if (NodeType.isUnaryMinus(numerator)) { const cancelStatus = cancelTerms(numerator.args[0], denominator); if (!cancelStatus.numerator) { - numerator = Node.Creator.constant(-1); - } - else if (Negative.isNegative(cancelStatus.numerator)) { + numerator = NodeCreator.constant(-1); + } else if (Negative.isNegative(cancelStatus.numerator)) { numerator = Negative.negate(cancelStatus.numerator); - } - else { + } else { numerator.args[0] = cancelStatus.numerator; } - denominator = cancelTerms.denominator; + + denominator = (cancelTerms as any).denominator; // FIXME DRN: what is happening here? return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged); } - if (Node.Type.isUnaryMinus(denominator)) { + if (NodeType.isUnaryMinus(denominator)) { const cancelStatus = cancelTerms(numerator, denominator.args[0]); numerator = cancelStatus.numerator; if (cancelStatus.denominator) { denominator.args[0] = cancelStatus.denominator; - } - else { + } else { denominator = cancelStatus.denominator; if (numerator) { numerator = Negative.negate(numerator); - } - else { - numerator = Node.Creator.constant(-1); + } else { + numerator = NodeCreator.constant(-1); } } return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged); } // Deal with parens similarily - if (Node.Type.isParenthesis(numerator)) { + if (NodeType.isParenthesis(numerator)) { const cancelStatus = cancelTerms(numerator.content, denominator); if (cancelStatus.numerator) { numerator.content = cancelStatus.numerator; - } - else { + } else { // if the numerator was cancelled out, the numerator should be null // and not null in parens. numerator = cancelStatus.numerator; @@ -218,12 +229,11 @@ function cancelTerms(numerator, denominator) { denominator = cancelStatus.denominator; return new CancelOutStatus(numerator, denominator, cancelStatus.hasChanged); } - if (Node.Type.isParenthesis(denominator)) { + if (NodeType.isParenthesis(denominator)) { const cancelStatus = cancelTerms(numerator, denominator.content); if (cancelStatus.denominator) { denominator.content = cancelStatus.denominator; - } - else { + } else { // if the denominator was cancelled out, the denominator should be null // and not null in parens. denominator = cancelStatus.denominator; @@ -236,22 +246,25 @@ function cancelTerms(numerator, denominator) { // case 1: the numerator term and denominator term are the same, so we cancel // them out. e.g. (x+5)^100 / (x+5)^100 => null / null - if (print.ascii(numerator) === print.ascii(denominator)) { + if (printAscii(numerator) === printAscii(denominator)) { return new CancelOutStatus(null, null, true); } // case 2: they're both exponent nodes with the same base // e.g. (2x+5)^8 and (2x+5)^2 - if (Node.Type.isOperator(numerator, '^') && - Node.Type.isOperator(denominator, '^') && - print.ascii(numerator.args[0]) === print.ascii(denominator.args[0])) { + if ( + NodeType.isOperator(numerator, "^") && + NodeType.isOperator(denominator, "^") && + printAscii(numerator.args[0]) === printAscii(denominator.args[0]) + ) { const numeratorExponent = numerator.args[1]; - let denominatorExponent = denominator.args[1]; + let denominatorExponent = denominator.args[1]; // wrap the denominatorExponent in parens, in case it's complicated. // If the parens aren't needed, they'll be removed with // removeUnnecessaryParens at the end of this step. - denominatorExponent = Node.Creator.parenthesis(denominatorExponent); - const newExponent = Node.Creator.parenthesis( - Node.Creator.operator('-', [numeratorExponent, denominatorExponent])); + denominatorExponent = NodeCreator.parenthesis(denominatorExponent); + const newExponent = NodeCreator.parenthesis( + NodeCreator.operator("-", [numeratorExponent, denominatorExponent]) + ); numerator.args[1] = newExponent; return new CancelOutStatus(numerator, null, true); } @@ -262,36 +275,41 @@ function cancelTerms(numerator, denominator) { // e.g 20x / 40y => x / 2y // e.g 60x / 40y => 3x / 2y // e.g 4x / 2y => 2x / y - if (Node.PolynomialTerm.isPolynomialTerm(numerator) && - Node.PolynomialTerm.isPolynomialTerm(denominator)) { - const numeratorTerm = new Node.PolynomialTerm(numerator); - const denominatorTerm = new Node.PolynomialTerm(denominator); + if ( + PolynomialTerm.isPolynomialTerm(numerator) && + PolynomialTerm.isPolynomialTerm(denominator) + ) { + const numeratorTerm = new PolynomialTerm(numerator); + const denominatorTerm = new PolynomialTerm(denominator); if (numeratorTerm.getSymbolName() !== denominatorTerm.getSymbolName()) { - if (Node.Type.isOperator(numerator, '*') && Node.Type.isOperator(denominator, '*')) { + if ( + NodeType.isOperator(numerator, "*") && + NodeType.isOperator(denominator, "*") + ) { // case 3.1 return cancelCoeffs(numerator, denominator); - } - else { + } else { return new CancelOutStatus(numerator, denominator); } } const numeratorExponent = numeratorTerm.getExponentNode(true); - let denominatorExponent = denominatorTerm.getExponentNode(true); - if (print.ascii(numeratorExponent) === print.ascii(denominatorExponent)) { + let denominatorExponent = denominatorTerm.getExponentNode(true); + if (printAscii(numeratorExponent) === printAscii(denominatorExponent)) { // note this returns null if there's no coefficient (ie it's 1) numerator = numeratorTerm.getCoeffNode(); - } - else { + } else { // wrap the denominatorExponent in parens, in case it's complicated. // If the parens aren't needed, they'll be removed with // removeUnnecessaryParens at the end of this step. - denominatorExponent = Node.Creator.parenthesis(denominatorExponent); - const newExponent = Node.Creator.parenthesis( - Node.Creator.operator('-', [numeratorExponent, denominatorExponent])); - numerator = Node.Creator.polynomialTerm( + denominatorExponent = NodeCreator.parenthesis(denominatorExponent); + const newExponent = NodeCreator.parenthesis( + NodeCreator.operator("-", [numeratorExponent, denominatorExponent]) + ); + numerator = NodeCreator.polynomialTerm( numeratorTerm.getSymbolNode(), newExponent, - numeratorTerm.getCoeffNode()); + numeratorTerm.getCoeffNode() + ); } denominator = denominatorTerm.getCoeffNode(); return new CancelOutStatus(numerator, denominator, true); @@ -301,59 +319,59 @@ function cancelTerms(numerator, denominator) { // or is multiplication node // e.g. 2 / 4x -> 1 / 2x // e.g. ignore cases like: 2 / a and 2 / x^2 - if (Node.Type.isConstant(numerator) - && Node.Type.isOperator(denominator, '*') - && Node.PolynomialTerm.isPolynomialTerm(denominator)) { - const denominatorTerm = new Node.PolynomialTerm(denominator); + if ( + NodeType.isConstant(numerator) && + NodeType.isOperator(denominator, "*") && + PolynomialTerm.isPolynomialTerm(denominator) + ) { + const denominatorTerm = new PolynomialTerm(denominator); const coeff = denominatorTerm.getCoeffNode(); const variable = denominatorTerm.getSymbolNode(); const exponent = denominatorTerm.getExponentNode(); // simplify a constant fraction (e.g 2 / 4) - const frac = Node.Creator.operator('/', [numerator, coeff]); + const frac = NodeCreator.operator("/", [numerator, coeff]); let newCoeff = coeff.cloneDeep(); const reduceStatus = divideByGCD(frac); - if (!reduceStatus.hasChanged()) { + if (!reduceStatus.hasChanged) { return new CancelOutStatus(numerator, denominator, false); } // Sometimes the fraction reduces to a constant e.g. 6 / 2 -> 3, // in which case `newCoeff` (the denominator coefficient) should be null - if (Node.Type.isConstant(reduceStatus.newNode)) { + if (NodeType.isConstant(reduceStatus.newNode)) { numerator = reduceStatus.newNode; newCoeff = null; - } - else { + } else { [numerator, newCoeff] = reduceStatus.newNode.args; } - denominator = Node.Creator.polynomialTerm(variable, exponent, newCoeff); + denominator = NodeCreator.polynomialTerm(variable, exponent, newCoeff); return new CancelOutStatus(numerator, denominator, true); } // case 5: both numerator and denominator are numbers within a more complicated fraction // e.g. (35 * nthRoot (7)) / (5 * nthRoot(5)) -> (7 * nthRoot(7)) / nthRoot(5) - if (Node.Type.isConstant(numerator) && Node.Type.isConstant(denominator)) { - const frac = Node.Creator.operator('/', [numerator, denominator]); + if (NodeType.isConstant(numerator) && NodeType.isConstant(denominator)) { + const frac = NodeCreator.operator("/", [numerator, denominator]); const reduceStatus = divideByGCD(frac); - if (!reduceStatus.hasChanged()) { + if (!reduceStatus.hasChanged) { return new CancelOutStatus(numerator, denominator, false); } - if (Node.Type.isConstant(reduceStatus.newNode)) { + if (NodeType.isConstant(reduceStatus.newNode)) { // Denominator is a factor of numerator (e.g 4 / 2 -> 2) return new CancelOutStatus(reduceStatus.newNode, null, true); } // Sometimes the fraction reduces to a constant e.g. 6 / 2 -> 3, // in which case `newCoeff` (the denominator coefficient) should be null - if (Node.Type.isConstant(reduceStatus.newNode)) { + if (NodeType.isConstant(reduceStatus.newNode)) { numerator = reduceStatus.newNode; denominator = null; - } - else { + } else { [numerator, denominator] = reduceStatus.newNode.args; } @@ -369,16 +387,17 @@ function cancelTerms(numerator, denominator) { // e.g. (2 * 6^y) => true // e.g. 2x^2 => false (polynomial terms are considered as one single term) function isMultiplicationOfTerms(node) { - if (Node.Type.isParenthesis(node)) { + if (NodeType.isParenthesis(node)) { return isMultiplicationOfTerms(node.content); } - return (Node.Type.isOperator(node, '*') && - !Node.PolynomialTerm.isPolynomialTerm(node)); + return ( + NodeType.isOperator(node, "*") && !PolynomialTerm.isPolynomialTerm(node) + ); } -function cancelCoeffs(numerator, denominator){ - const denominatorTerm = new Node.PolynomialTerm(denominator); - const numeratorTerm = new Node.PolynomialTerm(numerator); +function cancelCoeffs(numerator, denominator) { + const denominatorTerm = new PolynomialTerm(denominator); + const numeratorTerm = new PolynomialTerm(numerator); const denominatorCoeff = denominatorTerm.getCoeffNode(); const denominatorVariable = denominatorTerm.getSymbolNode(); @@ -389,11 +408,11 @@ function cancelCoeffs(numerator, denominator){ const numeratorExponent = numeratorTerm.getExponentNode(); // simplify a constant fraction (e.g 2 / 4) - const frac = Node.Creator.operator('/', [numeratorCoeff, denominatorCoeff]); + const frac = NodeCreator.operator("/", [numeratorCoeff, denominatorCoeff]); const reduceStatus = divideByGCD(frac); - if (!reduceStatus.hasChanged()) { + if (!reduceStatus.hasChanged) { return new CancelOutStatus(numerator, denominator, false); } @@ -401,17 +420,26 @@ function cancelCoeffs(numerator, denominator){ // in which case the denominator coefficient should be null let newDenominatorCoeff = null; let newNumerator = null; - if (Node.Type.isConstant(reduceStatus.newNode)) { - newNumerator = Node.Creator.polynomialTerm(numeratorVariable, numeratorExponent, reduceStatus.newNode); + if (NodeType.isConstant(reduceStatus.newNode)) { + newNumerator = NodeCreator.polynomialTerm( + numeratorVariable, + numeratorExponent, + reduceStatus.newNode + ); newDenominatorCoeff = null; - } - else { - newNumerator = Node.Creator.polynomialTerm(numeratorVariable, numeratorExponent, reduceStatus.newNode.args[0]); + } else { + newNumerator = NodeCreator.polynomialTerm( + numeratorVariable, + numeratorExponent, + reduceStatus.newNode.args[0] + ); newDenominatorCoeff = reduceStatus.newNode.args[1]; } - const newDenominator = Node.Creator.polynomialTerm(denominatorVariable, denominatorExponent, newDenominatorCoeff); + const newDenominator = NodeCreator.polynomialTerm( + denominatorVariable, + denominatorExponent, + newDenominatorCoeff + ); return new CancelOutStatus(newNumerator, newDenominator, true); } - -module.exports = cancelLikeTerms; diff --git a/lib/simplifyExpression/fractionsSearch/divideByGCD.js b/lib/src/simplifyExpression/fractionsSearch/divideByGCD.ts similarity index 54% rename from lib/simplifyExpression/fractionsSearch/divideByGCD.js rename to lib/src/simplifyExpression/fractionsSearch/divideByGCD.ts index 3163dd46..bee89c3a 100644 --- a/lib/simplifyExpression/fractionsSearch/divideByGCD.js +++ b/lib/src/simplifyExpression/fractionsSearch/divideByGCD.ts @@ -1,8 +1,10 @@ -const math = require('mathjs'); +import * as math from "mathjs"; -const ChangeTypes = require('../../ChangeTypes'); -const evaluate = require('../../util/evaluate'); -const Node = require('../../node'); +import { ChangeTypes } from "../../ChangeTypes"; +import { evaluate } from "../../util/evaluate"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; // Simplifies a fraction (with constant numerator and denominator) by dividing // the top and bottom by the GCD, if possible. @@ -12,14 +14,14 @@ const Node = require('../../node'); // Note that -4/5 doesn't need to be simplified. // Note that our goal is for the denominator to always be positive. If it // isn't, we can simplify signs. -// Returns a Node.Status object -function divideByGCD(fraction) { - if (!Node.Type.isOperator(fraction) || fraction.op !== '/') { - return Node.Status.noChange(fraction); +// Returns a Status object +export function divideByGCD(fraction) { + if (!NodeType.isOperator(fraction) || fraction.op !== "/") { + return NodeStatus.noChange(fraction); } // If it's not an integer fraction, all we can do is simplify signs - if (!Node.Type.isIntegerFraction(fraction, true)) { - return Node.Status.noChange(fraction); + if (!NodeType.isIntegerFraction(fraction, true)) { + return NodeStatus.noChange(fraction); } const substeps = []; @@ -40,23 +42,28 @@ function divideByGCD(fraction) { } if (gcd === 1) { - return Node.Status.noChange(fraction); + return NodeStatus.noChange(fraction); } // STEP 1: Find GCD // e.g. 15/6 -> (5*3)/(2*3) let status = findGCD(newNode, gcd, numeratorValue, denominatorValue); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); // STEP 2: Cancel GCD // (5*3)/(2*3) -> 5/2 status = cancelGCD(newNode, gcd, numeratorValue, denominatorValue); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - - return Node.Status.nodeChanged( - ChangeTypes.SIMPLIFY_FRACTION, fraction, newNode, true, substeps); + newNode = NodeStatus.resetChangeGroups(status.newNode); + + return NodeStatus.nodeChanged( + ChangeTypes.SIMPLIFY_FRACTION, + fraction, + newNode, + true, + substeps + ); } // Returns a substep where the GCD is factored out of numerator and denominator @@ -65,37 +72,41 @@ function findGCD(node, gcd, numeratorValue, denominatorValue) { let newNode = node.cloneDeep(); // manually set change group of the GCD nodes to be the same - const gcdNode = Node.Creator.constant(gcd); + const gcdNode = NodeCreator.constant(gcd); gcdNode.changeGroup = 1; - const intermediateNumerator = Node.Creator.parenthesis(Node.Creator.operator( - '*', [Node.Creator.constant(numeratorValue/gcd), gcdNode])); - const intermediateDenominator = Node.Creator.parenthesis(Node.Creator.operator( - '*', [Node.Creator.constant(denominatorValue/gcd), gcdNode])); - newNode = Node.Creator.operator( - '/', [intermediateNumerator, intermediateDenominator]); - - return Node.Status.nodeChanged( - ChangeTypes.FIND_GCD, node, newNode, false); + const intermediateNumerator = NodeCreator.parenthesis( + NodeCreator.operator("*", [ + NodeCreator.constant(numeratorValue / gcd), + gcdNode, + ]) + ); + const intermediateDenominator = NodeCreator.parenthesis( + NodeCreator.operator("*", [ + NodeCreator.constant(denominatorValue / gcd), + gcdNode, + ]) + ); + newNode = NodeCreator.operator("/", [ + intermediateNumerator, + intermediateDenominator, + ]); + + return NodeStatus.nodeChanged(ChangeTypes.FIND_GCD, node, newNode, false); } // Returns a substep where the GCD is cancelled out of numerator and denominator // e.g. (5*3)/(2*3) -> 5/2 function cancelGCD(node, gcd, numeratorValue, denominatorValue) { let newNode; - const newNumeratorNode = Node.Creator.constant(numeratorValue/gcd); - const newDenominatorNode = Node.Creator.constant(denominatorValue/gcd); + const newNumeratorNode = NodeCreator.constant(numeratorValue / gcd); + const newDenominatorNode = NodeCreator.constant(denominatorValue / gcd); if (parseFloat(newDenominatorNode.value) === 1) { newNode = newNumeratorNode; - } - else { - newNode = Node.Creator.operator( - '/', [newNumeratorNode, newDenominatorNode]); + } else { + newNode = NodeCreator.operator("/", [newNumeratorNode, newDenominatorNode]); } - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_GCD, node, newNode, false); + return NodeStatus.nodeChanged(ChangeTypes.CANCEL_GCD, node, newNode, false); } - -module.exports = divideByGCD; diff --git a/lib/simplifyExpression/fractionsSearch/index.js b/lib/src/simplifyExpression/fractionsSearch/index.ts similarity index 53% rename from lib/simplifyExpression/fractionsSearch/index.js rename to lib/src/simplifyExpression/fractionsSearch/index.ts index 6650cd55..cd1a3b9f 100644 --- a/lib/simplifyExpression/fractionsSearch/index.js +++ b/lib/src/simplifyExpression/fractionsSearch/index.ts @@ -9,46 +9,51 @@ */ -const addConstantAndFraction = require('./addConstantAndFraction'); -const addConstantFractions = require('./addConstantFractions'); -const cancelLikeTerms = require('./cancelLikeTerms'); -const divideByGCD = require('./divideByGCD'); -const simplifyFractionSigns = require('./simplifyFractionSigns'); -const simplifyPolynomialFraction = require('./simplifyPolynomialFraction'); - -const Node = require('../../node'); -const TreeSearch = require('../../TreeSearch'); - -const SIMPLIFICATION_FUNCTIONS = [ +import { addConstantAndFraction } from "./addConstantAndFraction"; +import { addConstantFractions } from "./addConstantFractions"; + +import { TreeSearch } from "../../TreeSearch"; +import { divideByGCD } from "./divideByGCD"; +import { simplifyPolynomialFraction } from "./simplifyPolynomialFraction"; +import { cancelLikeTerms } from "./cancelLikeTerms"; +import { simplifyFractionSigns } from "./simplifyFractionSigns"; +import { NodeStatus } from "../../node/NodeStatus"; +import { factorString } from "../../factor/FactorString"; +import { MathNode } from "mathjs"; + +const SIMPLIFICATION_FUNCTIONS: ReadonlyArray< + (node: MathNode) => NodeStatus +> = [ // e.g. 2/3 + 5/6 addConstantFractions, + // e.g. 4 + 5/6 or 4.5 + 6/8 addConstantAndFraction, + // e.g. 2/-9 -> -2/9 e.g. -2/-9 -> 2/9 simplifyFractionSigns, + // e.g. 8/12 -> 2/3 (divide by GCD 4) divideByGCD, + // e.g. 2x/4 -> x/2 (divideByGCD but for coefficients of polynomial terms) simplifyPolynomialFraction, + // e.g. (2x * 5) / 2x -> 5 cancelLikeTerms, ]; -const search = TreeSearch.preOrder(simplifyFractions); +export const fractionsSearch = TreeSearch.preOrder(simplifyFractions); -// Look for step(s) to perform on a node. Returns a Node.Status object. +// Look for step(s) to perform on a node. Returns a Status object. function simplifyFractions(node) { for (let i = 0; i < SIMPLIFICATION_FUNCTIONS.length; i++) { const nodeStatus = SIMPLIFICATION_FUNCTIONS[i](node); - if (nodeStatus.hasChanged()) { + if (nodeStatus.hasChanged) { return nodeStatus; - } - else { + } else { node = nodeStatus.newNode; } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } - - -module.exports = search; diff --git a/lib/src/simplifyExpression/fractionsSearch/simplifyFractionSigns.ts b/lib/src/simplifyExpression/fractionsSearch/simplifyFractionSigns.ts new file mode 100644 index 00000000..4aa05f41 --- /dev/null +++ b/lib/src/simplifyExpression/fractionsSearch/simplifyFractionSigns.ts @@ -0,0 +1,33 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { Negative } from "../../Negative"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; +import { MathNode } from "mathjs"; + +// Simplifies negative signs if possible +// e.g. -1/-3 --> 1/3 4/-5 --> -4/5 +// Note that -4/5 doesn't need to be simplified. +// Note that our goal is for the denominator to always be positive. If it +// isn't, we can simplify signs. +// Returns a Status object +export function simplifyFractionSigns(node: MathNode): NodeStatus { + if (!NodeType.isOperator(node) || node.op !== "/") { + return NodeStatus.noChange(node); + } + const oldFraction = node.cloneDeep(); + let numerator = node.args[0]; + let denominator = node.args[1]; + // The denominator should never be negative. + if (Negative.isNegative(denominator)) { + denominator = Negative.negate(denominator); + const changeType = Negative.isNegative(numerator) + ? ChangeTypes.CANCEL_MINUSES + : ChangeTypes.SIMPLIFY_SIGNS; + numerator = Negative.negate(numerator); + const newFraction = NodeCreator.operator("/", [numerator, denominator]); + return NodeStatus.nodeChanged(changeType, oldFraction, newFraction); + } else { + return NodeStatus.noChange(node); + } +} diff --git a/lib/src/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.ts b/lib/src/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.ts new file mode 100644 index 00000000..558836a5 --- /dev/null +++ b/lib/src/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.ts @@ -0,0 +1,48 @@ +import { divideByGCD } from "./divideByGCD"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCreator } from "../../node/Creator"; +import { arithmeticSearch } from "../arithmeticSearch/ArithmeticSearch"; +import { MathNode } from "mathjs"; + +// Simplifies a polynomial term with a fraction as its coefficients. +// e.g. 2x/4 --> x/2 10x/5 --> 2x +// Also simplified negative signs +// e.g. -y/-3 --> y/3 4x/-5 --> -4x/5 +// returns the new simplified node in a Status object +export function simplifyPolynomialFraction(node: MathNode): NodeStatus { + if (!PolynomialTerm.isPolynomialTerm(node)) { + return NodeStatus.noChange(node); + } + + const polyNode = new PolynomialTerm(node.cloneDeep()); + if (!polyNode.hasFractionCoeff()) { + return NodeStatus.noChange(node); + } + + const coefficientSimplifications = [ + divideByGCD, // for integer fractions + arithmeticSearch, // for decimal fractions + ]; + + for (let i = 0; i < coefficientSimplifications.length; i++) { + const coefficientFraction = polyNode.getCoeffNode(); // a division node + const newCoeffStatus = coefficientSimplifications[i](coefficientFraction); + if (newCoeffStatus.hasChanged) { + // we need to reset change groups because we're creating a new node + let newCoeff = NodeStatus.resetChangeGroups(newCoeffStatus.newNode); + if (newCoeff.value === "1") { + newCoeff = null; + } + const exponentNode = polyNode.getExponentNode(); + const newNode = NodeCreator.polynomialTerm( + polyNode.getSymbolNode(), + exponentNode, + newCoeff + ); + return NodeStatus.nodeChanged(newCoeffStatus.changeType, node, newNode); + } + } + + return NodeStatus.noChange(node); +} diff --git a/lib/src/simplifyExpression/functionsSearch/absoluteValue.ts b/lib/src/simplifyExpression/functionsSearch/absoluteValue.ts new file mode 100644 index 00000000..5be2c12d --- /dev/null +++ b/lib/src/simplifyExpression/functionsSearch/absoluteValue.ts @@ -0,0 +1,35 @@ +import * as math from "mathjs"; + +import { ChangeTypes } from "../../ChangeTypes"; +import { evaluate } from "../../util/evaluate"; +import { NodeType } from "../../node/NodeType"; +import { NodeCreator } from "../../node/Creator"; +import { NodeStatus } from "../../node/NodeStatus"; + +// Evaluates abs() function if it's on a single constant value. +// Returns a Status object. +export function absoluteValue(node) { + if (!NodeType.isFunction(node, "abs")) { + return NodeStatus.noChange(node); + } + if (node.args.length > 1) { + return NodeStatus.noChange(node); + } + let newNode = node.cloneDeep(); + const argument = newNode.args[0]; + if (NodeType.isConstant(argument, true)) { + newNode = NodeCreator.constant(math.abs(evaluate(argument))); + return NodeStatus.nodeChanged(ChangeTypes.ABSOLUTE_VALUE, node, newNode); + } else if (NodeType.isConstantFraction(argument, true)) { + const newNumerator = NodeCreator.constant( + math.abs(evaluate(argument.args[0])) + ); + const newDenominator = NodeCreator.constant( + math.abs(evaluate(argument.args[1])) + ); + newNode = NodeCreator.operator("/", [newNumerator, newDenominator]); + return NodeStatus.nodeChanged(ChangeTypes.ABSOLUTE_VALUE, node, newNode); + } else { + return NodeStatus.noChange(node); + } +} diff --git a/lib/src/simplifyExpression/functionsSearch/index.ts b/lib/src/simplifyExpression/functionsSearch/index.ts new file mode 100644 index 00000000..356f713c --- /dev/null +++ b/lib/src/simplifyExpression/functionsSearch/index.ts @@ -0,0 +1,27 @@ +import { TreeSearch } from "../../TreeSearch"; +import { NodeType } from "../../node/NodeType"; +import { NodeStatus } from "../../node/NodeStatus"; +import { nthRoot } from "./nthRoot"; +import { absoluteValue } from "./absoluteValue"; + +const FUNCTIONS = [nthRoot, absoluteValue]; + +// Searches through the tree, prioritizing deeper nodes, and evaluates +// functions (e.g. abs(-4)) if possible. +// Returns a Status object. +export const functionsSearch = TreeSearch.postOrder(functions); + +// Evaluates a function call if possible. Returns a Status object. +function functions(node) { + if (!NodeType.isFunction(node)) { + return NodeStatus.noChange(node); + } + + for (let i = 0; i < FUNCTIONS.length; i++) { + const nodeStatus = FUNCTIONS[i](node); + if (nodeStatus.hasChanged) { + return nodeStatus; + } + } + return NodeStatus.noChange(node); +} diff --git a/lib/simplifyExpression/functionsSearch/nthRoot.js b/lib/src/simplifyExpression/functionsSearch/nthRoot.ts similarity index 55% rename from lib/simplifyExpression/functionsSearch/nthRoot.js rename to lib/src/simplifyExpression/functionsSearch/nthRoot.ts index ab27ef95..806963e1 100644 --- a/lib/simplifyExpression/functionsSearch/nthRoot.js +++ b/lib/src/simplifyExpression/functionsSearch/nthRoot.ts @@ -1,33 +1,35 @@ -const math = require('mathjs'); - -const ChangeTypes = require('../../ChangeTypes'); -const ConstantFactors = require('../../factor/ConstantFactors'); -const Negative = require('../../Negative'); -const Node = require('../../node'); -const print = require('../../util/print'); - -// Evaluate nthRoot() function. -// Returns a Node.Status object. -function nthRoot(node) { - if (!Node.Type.isFunction(node, 'nthRoot')) { - return Node.Status.noChange(node); +import * as math from "mathjs"; + +import { ChangeTypes } from "../../ChangeTypes"; +import { ConstantFactors } from "../../factor/ConstantFactors"; +import { Negative } from "../../Negative"; +import { NodeType } from "../../node/NodeType"; +import { NodeCreator } from "../../node/Creator"; +import { NodeStatus } from "../../node/NodeStatus"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { printAscii } from "../../util/print"; + +/** + * Evaluate nthRoot() function. + * */ +export function nthRoot(node): NodeStatus { + if (!NodeType.isFunction(node, "nthRoot")) { + return NodeStatus.noChange(node); } const radicandNode = getRadicandNode(node); - if (Node.Type.isOperator(radicandNode)) { - if (radicandNode.op === '^') { + if (NodeType.isOperator(radicandNode)) { + if (radicandNode.op === "^") { return nthRootExponent(node); - } - else if (radicandNode.op === '*') { + } else if (radicandNode.op === "*") { return nthRootMultiplication(node); } - } - else if (Node.Type.isConstant(radicandNode)) { + } else if (NodeType.isConstant(radicandNode)) { return nthRootConstant(node); } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Returns the nthRoot evaluated for an exponent node. Expects an exponent under @@ -41,36 +43,38 @@ function nthRootExponent(node) { const radicandNode = getRadicandNode(node); const rootNode = getRootNode(node); const baseNode = radicandNode.args[0]; - const exponentNode = Node.Type.isParenthesis(radicandNode.args[1]) - ? radicandNode.args[1].content - : radicandNode.args[1]; + const exponentNode = NodeType.isParenthesis(radicandNode.args[1]) + ? radicandNode.args[1].content + : radicandNode.args[1]; if (rootNode.equals(exponentNode)) { newNode = baseNode; - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_EXPONENT_AND_ROOT, node, newNode); - } - else if (Node.Type.isConstant(rootNode) && Node.Type.isConstant(exponentNode)) { + return NodeStatus.nodeChanged( + ChangeTypes.CANCEL_EXPONENT_AND_ROOT, + node, + newNode + ); + } else if ( + NodeType.isConstant(rootNode) && + NodeType.isConstant(exponentNode) + ) { const rootValue = parseFloat(rootNode.value); const exponentValue = parseFloat(exponentNode.value); if (rootValue % exponentValue === 0) { - const newRootValue = rootValue/exponentValue; - const newRootNode = Node.Creator.constant(newRootValue); + const newRootValue = rootValue / exponentValue; + const newRootNode = NodeCreator.constant(newRootValue); - newNode = Node.Creator.nthRoot(baseNode, newRootNode); - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_EXPONENT, node, newNode); - } - else if (exponentValue % rootValue === 0) { - const newExponentValue = exponentValue/rootValue; - const newExponentNode = Node.Creator.constant(newExponentValue); + newNode = NodeCreator.nthRoot(baseNode, newRootNode); + return NodeStatus.nodeChanged(ChangeTypes.CANCEL_EXPONENT, node, newNode); + } else if (exponentValue % rootValue === 0) { + const newExponentValue = exponentValue / rootValue; + const newExponentNode = NodeCreator.constant(newExponentValue); - newNode = Node.Creator.operator('^', [baseNode, newExponentNode]); - return Node.Status.nodeChanged( - ChangeTypes.CANCEL_ROOT, node, newNode); + newNode = NodeCreator.operator("^", [baseNode, newExponentNode]); + return NodeStatus.nodeChanged(ChangeTypes.CANCEL_ROOT, node, newNode); } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Returns the nthRoot evaluated for a multiplication node. @@ -89,31 +93,36 @@ function nthRootMultiplication(node) { const substeps = []; let status; - if (Node.Type.isConstant(rootNode) && !Negative.isNegative(rootNode)) { + if (NodeType.isConstant(rootNode) && !Negative.isNegative(rootNode)) { // Step 1A status = factorMultiplicands(newNode); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } // Step 1B status = groupTermsByRoot(newNode); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } // Step 1C status = convertMultiplicationToExponent(newNode); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); - if (newNode.args[0].op === '^') { + newNode = NodeStatus.resetChangeGroups(status.newNode); + if (newNode.args[0].op === "^") { status = nthRootExponent(newNode); substeps.push(status); - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, status.newNode, true, substeps); + return NodeStatus.nodeChanged( + ChangeTypes.NTH_ROOT_VALUE, + node, + status.newNode, + true, + substeps + ); } } } @@ -121,26 +130,31 @@ function nthRootMultiplication(node) { // Step 2A status = distributeNthRoot(newNode); substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); // Step 2B status = evaluateNthRootForChildren(newNode); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); // Step 2C status = combineRoots(newNode); - if (status.hasChanged()) { + if (status.hasChanged) { substeps.push(status); - newNode = Node.Status.resetChangeGroups(status.newNode); + newNode = NodeStatus.resetChangeGroups(status.newNode); } - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, newNode, true, substeps); + return NodeStatus.nodeChanged( + ChangeTypes.NTH_ROOT_VALUE, + node, + newNode, + true, + substeps + ); } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Given an nthRoot node with a constant positive root, will do the step of @@ -151,12 +165,15 @@ function factorMultiplicands(node) { const radicandNode = getRadicandNode(node); let children = []; let factored = false; - radicandNode.args.forEach(child => { - if (Node.PolynomialTerm.isPolynomialTerm(child)) { - const polyTerm = new Node.PolynomialTerm(child); + radicandNode.args.forEach((child) => { + if (PolynomialTerm.isPolynomialTerm(child)) { + const polyTerm = new PolynomialTerm(child); const coeffNode = polyTerm.getCoeffNode(); - const polyTermNoCoeff = Node.Creator.polynomialTerm( - polyTerm.getSymbolNode(), polyTerm.getExponentNode(), null); + const polyTermNoCoeff = NodeCreator.polynomialTerm( + polyTerm.getSymbolNode(), + polyTerm.getExponentNode(), + null + ); if (coeffNode) { const factorNodes = getFactorNodes(coeffNode); if (factorNodes.length > 1) { @@ -165,8 +182,7 @@ function factorMultiplicands(node) { children = children.concat(factorNodes); } children.push(polyTermNoCoeff); - } - else { + } else { const factorNodes = getFactorNodes(child); if (factorNodes.length > 1) { factored = true; @@ -176,19 +192,22 @@ function factorMultiplicands(node) { }); if (factored) { - newNode.args[0] = Node.Creator.operator('*', children); - return Node.Status.nodeChanged( - ChangeTypes.FACTOR_INTO_PRIMES, node, newNode); + newNode.args[0] = NodeCreator.operator("*", children); + return NodeStatus.nodeChanged( + ChangeTypes.FACTOR_INTO_PRIMES, + node, + newNode + ); } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } function getFactorNodes(node) { - if (Node.Type.isConstant(node) && !Negative.isNegative(node)) { + if (NodeType.isConstant(node) && !Negative.isNegative(node)) { const value = parseFloat(node.value); const factors = ConstantFactors.getPrimeFactors(value); - const factorNodes = factors.map(Node.Creator.constant); + const factorNodes = factors.map(NodeCreator.constant); return factorNodes; } return [node]; @@ -205,26 +224,25 @@ function groupTermsByRoot(node) { radicandNode.args.sort(sortNodes); - const termStrings = radicandNode.args.map(arg => print.ascii(arg)); - + const termStrings = radicandNode.args.map((arg) => printAscii(arg)); // there is nothing to simplify when there are no duplicates of factors in the radicand if ([...new Set(termStrings)].length === termStrings.length) { - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // dictionary storing the number of times a constant appears // e.g. 2 * 2 * 2 => {'2': 3} , 2 appears 3 times const count = {}; - radicandNode.args.forEach(arg => { - const argString = print.ascii(arg); - count[argString] ? count[argString]++ : count[argString] = 1; + radicandNode.args.forEach((arg) => { + const argString = printAscii(arg); + count[argString] ? count[argString]++ : (count[argString] = 1); }); const termString = Object.keys(count); - const newTermGroups = termString.map(key => { + const newTermGroups = termString.map((key) => { let leftover = count[key]; const term = math.parse(key); const times = rootValue; @@ -234,15 +252,20 @@ function groupTermsByRoot(node) { // Recursively create groups while (leftover - times > 0) { leftover -= times; - args.push(Node.Creator.parenthesis( - Node.Creator.operator('*', Array(times).fill(term)))); + args.push( + NodeCreator.parenthesis( + NodeCreator.operator("*", Array(times).fill(term)) + ) + ); } // Remaining terms after groups have been created - const remainder = leftover === 1 - ? term - : Node.Creator.parenthesis( - Node.Creator.operator('*', Array(leftover).fill(term))); + const remainder = + leftover === 1 + ? term + : NodeCreator.parenthesis( + NodeCreator.operator("*", Array(leftover).fill(term)) + ); args.push(remainder); return args; @@ -251,12 +274,11 @@ function groupTermsByRoot(node) { // Compress array of arrays const newTerms = newTermGroups.reduce((acc, val) => acc.concat(val), []); - const newBase = Node.Creator.operator('*', newTerms); + const newBase = NodeCreator.operator("*", newTerms); - newNode = Node.Creator.nthRoot(newBase, rootNode); + newNode = NodeCreator.nthRoot(newBase, rootNode); - return Node.Status.nodeChanged( - ChangeTypes.GROUP_TERMS_BY_ROOT, node, newNode); + return NodeStatus.nodeChanged(ChangeTypes.GROUP_TERMS_BY_ROOT, node, newNode); } // Given an nthRoot node with a constant positive root, @@ -267,37 +289,42 @@ function convertMultiplicationToExponent(node) { const radicandNode = getRadicandNode(node); - if (Node.Type.isParenthesis(radicandNode)) { + if (NodeType.isParenthesis(radicandNode)) { const child = radicandNode.content; if (isMultiplicationOfEqualNodes(child)) { const baseNode = child.args[0]; - const exponentNode = Node.Creator.constant(child.args.length); - newNode.args[0] = Node.Creator.operator('^', [baseNode, exponentNode]); - return Node.Status.nodeChanged( - ChangeTypes.CONVERT_MULTIPLICATION_TO_EXPONENT, node, newNode); + const exponentNode = NodeCreator.constant(child.args.length); + newNode.args[0] = NodeCreator.operator("^", [baseNode, exponentNode]); + return NodeStatus.nodeChanged( + ChangeTypes.CONVERT_MULTIPLICATION_TO_EXPONENT, + node, + newNode + ); } - } - else if (Node.Type.isOperator(radicandNode, '*')) { + } else if (NodeType.isOperator(radicandNode, "*")) { const children = []; - radicandNode.args.forEach(child => { - if (Node.Type.isParenthesis(child)) { + radicandNode.args.forEach((child) => { + if (NodeType.isParenthesis(child)) { const grandChild = child.content; if (isMultiplicationOfEqualNodes(grandChild)) { const baseNode = grandChild.args[0]; - const exponentNode = Node.Creator.constant(grandChild.args.length); - children.push(Node.Creator.operator('^', [baseNode, exponentNode])); + const exponentNode = NodeCreator.constant(grandChild.args.length); + children.push(NodeCreator.operator("^", [baseNode, exponentNode])); return; } } children.push(child); }); - newNode.args[0] = Node.Creator.operator('*', children); - return Node.Status.nodeChanged( - ChangeTypes.CONVERT_MULTIPLICATION_TO_EXPONENT, node, newNode); + newNode.args[0] = NodeCreator.operator("*", children); + return NodeStatus.nodeChanged( + ChangeTypes.CONVERT_MULTIPLICATION_TO_EXPONENT, + node, + newNode + ); } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Given an nthRoot node with a multiplication under the radicand, will @@ -311,12 +338,11 @@ function distributeNthRoot(node) { const children = []; for (let i = 0; i < radicandNode.args.length; i++) { const child = radicandNode.args[i]; - children.push(Node.Creator.nthRoot(child, rootNode)); + children.push(NodeCreator.nthRoot(child, rootNode)); } - newNode = Node.Creator.operator('*', children); - return Node.Status.nodeChanged( - ChangeTypes.DISTRIBUTE_NTH_ROOT, node, newNode); + newNode = NodeCreator.operator("*", children); + return NodeStatus.nodeChanged(ChangeTypes.DISTRIBUTE_NTH_ROOT, node, newNode); } // Given a multiplication node of nthRoots (with the same root) @@ -329,21 +355,24 @@ function evaluateNthRootForChildren(node) { for (let i = 0; i < newNode.args.length; i++) { const child = newNode.args[i]; const childNodeStatus = nthRoot(child); - if (childNodeStatus.hasChanged()) { + if (childNodeStatus.hasChanged) { newNode.args[i] = childNodeStatus.newNode; - substeps.push(Node.Status.childChanged(newNode, childNodeStatus, i)); + substeps.push(NodeStatus.childChanged(newNode, childNodeStatus, i)); } } if (substeps.length === 0) { - return Node.Status.noChange(node); - } - else if (substeps.length === 1) { + return NodeStatus.noChange(node); + } else if (substeps.length === 1) { return substeps[0]; - } - else { - return Node.Status.nodeChanged( - ChangeTypes.EVALUATE_DISTRIBUTED_NTH_ROOT, node, newNode, true, substeps); + } else { + return NodeStatus.nodeChanged( + ChangeTypes.EVALUATE_DISTRIBUTED_NTH_ROOT, + node, + newNode, + true, + substeps + ); } } @@ -360,31 +389,35 @@ function combineRoots(node) { const radicandArgs = []; for (let i = 0; i < newNode.args.length; i++) { const child = newNode.args[i]; - if (Node.Type.isFunction(child, 'nthRoot')) { + if (NodeType.isFunction(child, "nthRoot")) { radicandArgs.push(child.args[0]); rootNode = getRootNode(child); - } - else { + } else { children.push(child); } } if (children.length > 0) { if (radicandArgs.length > 0) { - const radicandNode = radicandArgs.length === 1 ? - radicandArgs[0] : Node.Creator.operator('*', radicandArgs); - children.push(Node.Creator.nthRoot(radicandNode, rootNode)); + const radicandNode = + radicandArgs.length === 1 + ? radicandArgs[0] + : NodeCreator.operator("*", radicandArgs); + children.push(NodeCreator.nthRoot(radicandNode, rootNode)); } - newNode = Node.Creator.operator('*', children); + newNode = NodeCreator.operator("*", children); if (!newNode.equals(node)) { - return Node.Status.nodeChanged( - ChangeTypes.COMBINE_UNDER_ROOT, node, newNode); + return NodeStatus.nodeChanged( + ChangeTypes.COMBINE_UNDER_ROOT, + node, + newNode + ); } } // if there are no items moved out of the root, then nothing has changed - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Returns the nthRoot evaluated on a constant node @@ -396,10 +429,9 @@ function nthRootConstant(node) { const rootNode = getRootNode(node); if (Negative.isNegative(radicandNode)) { - return Node.Status.noChange(node); - } - else if (!Node.Type.isConstant(rootNode) || Negative.isNegative(rootNode)) { - return Node.Status.noChange(node); + return NodeStatus.noChange(node); + } else if (!NodeType.isConstant(rootNode) || Negative.isNegative(rootNode)) { + return NodeStatus.noChange(node); } const radicandValue = parseFloat(radicandNode.value); @@ -410,9 +442,8 @@ function nthRootConstant(node) { // nthRoot may have round-off error, so we'll check for perfect roots by rounding to the nearest integer // and checking if that value that satisfies the root expression if (math.pow(roundedNthRootValue, rootValue) === radicandValue) { - newNode = Node.Creator.constant(roundedNthRootValue); - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, newNode); + newNode = NodeCreator.constant(roundedNthRootValue); + return NodeStatus.nodeChanged(ChangeTypes.NTH_ROOT_VALUE, node, newNode); } // Try to find if we can simplify by finding factors that can be // pulled out of the radical @@ -421,25 +452,31 @@ function nthRootConstant(node) { const factors = ConstantFactors.getPrimeFactors(radicandValue); if (factors.length > 1) { let substeps = []; - const factorNodes = factors.map(Node.Creator.constant); + const factorNodes = factors.map(NodeCreator.constant); - newNode.args[0] = Node.Creator.operator('*', factorNodes); - substeps.push(Node.Status.nodeChanged( - ChangeTypes.FACTOR_INTO_PRIMES, node, newNode)); + newNode.args[0] = NodeCreator.operator("*", factorNodes); + substeps.push( + NodeStatus.nodeChanged(ChangeTypes.FACTOR_INTO_PRIMES, node, newNode) + ); // run nthRoot on the new node const nodeStatus = nthRootMultiplication(newNode); - if (nodeStatus.hasChanged()) { + if (nodeStatus.hasChanged) { substeps = substeps.concat(nodeStatus.substeps); newNode = nodeStatus.newNode; - return Node.Status.nodeChanged( - ChangeTypes.NTH_ROOT_VALUE, node, newNode, true, substeps); + return NodeStatus.nodeChanged( + ChangeTypes.NTH_ROOT_VALUE, + node, + newNode, + true, + substeps + ); } } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Helpers @@ -447,18 +484,18 @@ function nthRootConstant(node) { // Given an nthRoot node, will return the root node. // The root node is the second child of the nthRoot node, but if one doesn't // exist, we assume it's a square root and return 2. -function getRootNode(node) { - if (!Node.Type.isFunction(node, 'nthRoot')) { - throw Error('Expected nthRoot'); +export function getRootNode(node) { + if (!NodeType.isFunction(node, "nthRoot")) { + throw Error("Expected nthRoot"); } - return node.args.length === 2 ? node.args[1] : Node.Creator.constant(2); + return node.args.length === 2 ? node.args[1] : NodeCreator.constant(2); } // Given an nthRoot node, will return the radicand node. -function getRadicandNode(node) { - if (!Node.Type.isFunction(node, 'nthRoot')) { - throw Error('Expected nthRoot'); +export function getRadicandNode(node) { + if (!NodeType.isFunction(node, "nthRoot")) { + throw Error("Expected nthRoot"); } return node.args[0]; @@ -467,13 +504,11 @@ function getRadicandNode(node) { // Sorts nodes, ordering constants nodes from smallest to largest and symbol // nodes after function sortNodes(a, b) { - if (Node.Type.isConstant(a) && Node.Type.isConstant(b)) { + if (NodeType.isConstant(a) && NodeType.isConstant(b)) { return parseFloat(a.value) - parseFloat(b.value); - } - else if (Node.Type.isConstant(a)) { + } else if (NodeType.isConstant(a)) { return -1; - } - else if (Node.Type.isConstant(b)) { + } else if (NodeType.isConstant(b)) { return 1; } return 0; @@ -482,18 +517,12 @@ function sortNodes(a, b) { // Simple helper function which determines a node is a multiplication node // of all equal nodes function isMultiplicationOfEqualNodes(node) { - if (!Node.Type.isOperator(node) || node.op !== '*') { + if (!NodeType.isOperator(node) || node.op !== "*") { return false; } - const termStrings = node.args.map(print.ascii); + const termStrings = node.args.map(printAscii); // return if they are all equal nodes return [...new Set(termStrings)].length === 1; } - -module.exports = { - getRadicandNode, - getRootNode, - nthRoot, -}; diff --git a/lib/src/simplifyExpression/index.ts b/lib/src/simplifyExpression/index.ts new file mode 100644 index 00000000..1c60ee75 --- /dev/null +++ b/lib/src/simplifyExpression/index.ts @@ -0,0 +1,15 @@ +import * as math from "mathjs"; +import { stepThrough } from "./stepThrough"; + +export function simplifyExpression(expressionString, debug = false) { + let exprNode; + try { + exprNode = math.parse(expressionString); + } catch (err) { + return []; + } + if (exprNode) { + return stepThrough(exprNode, debug); + } + return []; +} diff --git a/lib/src/simplifyExpression/multiplyFractionsSearch/index.ts b/lib/src/simplifyExpression/multiplyFractionsSearch/index.ts new file mode 100644 index 00000000..4beade4b --- /dev/null +++ b/lib/src/simplifyExpression/multiplyFractionsSearch/index.ts @@ -0,0 +1,83 @@ +import { ChangeTypes } from "../../ChangeTypes"; +import { TreeSearch } from "../../TreeSearch"; +import { NodeType } from "../../node/NodeType"; +import { PolynomialTerm } from "../../node/PolynomialTerm"; +import { NodeStatus } from "../../node/NodeStatus"; +import { NodeCustomType } from "../../node/CustomType"; +import { NodeCreator } from "../../node/Creator"; + +// If `node` is a product of terms where: +// 1) at least one is a fraction +// 2) either none are polynomial terms, OR +// at least one has a symbol in the denominator +// then multiply them together. +// e.g. 2 * 5/x -> (2*5)/x +// e.g. 3 * 1/5 * 5/9 = (3*1*5)/(5*9) +// e.g. 2x * 1/x -> (2x*1) / x +// NOTE: The reason we exclude the case of polynomial terms is because +// we do not want to combine 9/2 * x -> 9x / 2 (which is less readable). +// Cases like 5/2 * x * y/5 will be handled in collect and combine. +// TODO: add a step somewhere to remove common terms in numerator and +// denominator (so the 5s would cancel out on the next step after this) +// This step must happen after things have been distributed, or else the answer +// will be formatted badly, so it's a tree search of its own. +// Returns a Status object. +export const multiplyFractionsSearch = TreeSearch.postOrder(multiplyFractions); + +function multiplyFractions(node) { + if (!NodeType.isOperator(node) || node.op !== "*") { + return NodeStatus.noChange(node); + } + + // we need to use the verbose syntax for `some` here because isFraction + // can take more than one parameter + const atLeastOneFraction = node.args.some((arg) => + NodeCustomType.isFraction(arg) + ); + const hasPolynomialTerms = node.args.some(PolynomialTerm.isPolynomialTerm); + const hasPolynomialInDenominatorTerms = node.args.some( + hasPolynomialInDenominator + ); + + if ( + !atLeastOneFraction || + (hasPolynomialTerms && !hasPolynomialInDenominatorTerms) + ) { + return NodeStatus.noChange(node); + } + + const numeratorArgs = []; + const denominatorArgs = []; + node.args.forEach((operand) => { + if (NodeCustomType.isFraction(operand)) { + const fraction = NodeCustomType.getFraction(operand); + numeratorArgs.push(fraction.args[0]); + denominatorArgs.push(fraction.args[1]); + } else { + numeratorArgs.push(operand); + } + }); + + const newNumerator = NodeCreator.parenthesis( + NodeCreator.operator("*", numeratorArgs) + ); + const newDenominator = + denominatorArgs.length === 1 + ? denominatorArgs[0] + : NodeCreator.parenthesis(NodeCreator.operator("*", denominatorArgs)); + + const newNode = NodeCreator.operator("/", [newNumerator, newDenominator]); + return NodeStatus.nodeChanged(ChangeTypes.MULTIPLY_FRACTIONS, node, newNode); +} + +// Returns true if `node` has a polynomial in the denominator, +// e.g. 5/x or 1/2x^2 +function hasPolynomialInDenominator(node) { + if (!NodeCustomType.isFraction(node)) { + return false; + } + + const fraction = NodeCustomType.getFraction(node); + const denominator = fraction.args[1]; + return PolynomialTerm.isPolynomialTerm(denominator); +} diff --git a/lib/simplifyExpression/simplify.js b/lib/src/simplifyExpression/simplify.ts similarity index 53% rename from lib/simplifyExpression/simplify.js rename to lib/src/simplifyExpression/simplify.ts index 28a74383..87c4c43f 100644 --- a/lib/simplifyExpression/simplify.js +++ b/lib/src/simplifyExpression/simplify.ts @@ -1,16 +1,18 @@ -const math = require('mathjs'); - -const checks = require('../checks'); -const flattenOperands = require('../util/flattenOperands'); -const print = require('../util/print'); -const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); -const stepThrough = require('./stepThrough'); +import * as math from "mathjs"; +import { flattenOperands } from "../util/flattenOperands"; +import { removeUnnecessaryParens } from "../util/removeUnnecessaryParens"; +import { stepThrough } from "./stepThrough"; +import { hasUnsupportedNodes } from "../checks/hasUnsupportedNodes"; +import { printAscii } from "../util/print"; // Given a mathjs expression node, steps through simplifying the expression. // Returns the simplified expression node. -function simplify(node, debug=false) { - if (checks.hasUnsupportedNodes(node)) { +export function simplify(node, debug = false) { + if (hasUnsupportedNodes(node)) { + if (debug) { + throw new Error("UNSUPPORTED_NODES"); + } return node; } @@ -18,8 +20,7 @@ function simplify(node, debug=false) { let simplifiedNode; if (steps.length > 0) { simplifiedNode = steps.pop().newNode; - } - else { + } else { // removing parens isn't counted as a step, so try it here simplifiedNode = removeUnnecessaryParens(flattenOperands(node), true); } @@ -30,8 +31,5 @@ function simplify(node, debug=false) { // Unflattens a node so it is in the math.js style, by printing and parsing it // again function unflatten(node) { - return math.parse(print.ascii(node)); + return math.parse(printAscii(node)); } - - -module.exports = simplify; diff --git a/lib/simplifyExpression/stepThrough.js b/lib/src/simplifyExpression/stepThrough.ts similarity index 62% rename from lib/simplifyExpression/stepThrough.js rename to lib/src/simplifyExpression/stepThrough.ts index 26e9703e..e3af37cd 100644 --- a/lib/simplifyExpression/stepThrough.js +++ b/lib/src/simplifyExpression/stepThrough.ts @@ -1,55 +1,58 @@ -const checks = require('../checks'); -const Node = require('../node'); -const Status = require('../node/Status'); - -const arithmeticSearch = require('./arithmeticSearch'); -const basicsSearch = require('./basicsSearch'); -const breakUpNumeratorSearch = require('./breakUpNumeratorSearch'); -const collectAndCombineSearch = require('./collectAndCombineSearch'); -const distributeSearch = require('./distributeSearch'); -const divisionSearch = require('./divisionSearch'); -const fractionsSearch = require('./fractionsSearch'); -const functionsSearch = require('./functionsSearch'); -const multiplyFractionsSearch = require('./multiplyFractionsSearch'); - -const flattenOperands = require('../util/flattenOperands'); -const print = require('../util/print'); -const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); +import { NodeStatus } from "../node/NodeStatus"; + +import { collectAndCombineSearch } from "./collectAndCombineSearch"; +import { distributeSearch } from "./distributeSearch"; +import { divisionSearch } from "./divisionSearch"; +import { multiplyFractionsSearch } from "./multiplyFractionsSearch"; + +import { flattenOperands } from "../util/flattenOperands"; +import { printAscii } from "../util/print"; +import { removeUnnecessaryParens } from "../util/removeUnnecessaryParens"; +import { hasUnsupportedNodes } from "../checks/hasUnsupportedNodes"; +import { breakUpNumeratorSearch } from "./breakUpNumeratorSearch"; +import { arithmeticSearch } from "./arithmeticSearch/ArithmeticSearch"; +import { functionsSearch } from "./functionsSearch"; +import { fractionsSearch } from "./fractionsSearch"; +import { basicsSearch } from "./basicsSearch"; +import { emptyResponse } from "../util/empty-response"; // Given a mathjs expression node, steps through simplifying the expression. // Returns a list of details about each step. -function stepThrough(node, debug=false) { +export function stepThrough(node, debug = false) { if (debug) { // eslint-disable-next-line - console.log('\n\nSimplifying: ' + print.ascii(node, false, true)); + console.log("\n\nSimplifying: " + printAscii(node, false)); } - if (checks.hasUnsupportedNodes(node)) { - return []; + if (hasUnsupportedNodes(node)) { + return emptyResponse(); } let nodeStatus; const steps = []; - const originalExpressionStr = print.ascii(node); + const originalExpressionStr = printAscii(node); const MAX_STEP_COUNT = 20; - let iters = 0; + let iterations = 0; // Now, step through the math expression until nothing changes nodeStatus = step(node); - while (nodeStatus.hasChanged()) { + while (nodeStatus.hasChanged) { if (debug) { logSteps(nodeStatus); } steps.push(removeUnnecessaryParensInStep(nodeStatus)); - node = Status.resetChangeGroups(nodeStatus.newNode); + node = NodeStatus.resetChangeGroups(nodeStatus.newNode); nodeStatus = step(node); - if (iters++ === MAX_STEP_COUNT) { + if (iterations++ === MAX_STEP_COUNT) { // eslint-disable-next-line - console.error('Math error: Potential infinite loop for expression: ' + - originalExpressionStr + ', returning no steps'); + console.error( + "Math error: Potential infinite loop for expression: " + + originalExpressionStr + + ", returning no steps" + ); return []; } } @@ -58,7 +61,7 @@ function stepThrough(node, debug=false) { } // Given a mathjs expression node, performs a single step to simplify the -// expression. Returns a Node.Status object. +// expression. Returns a Status object. function step(node) { let nodeStatus; @@ -68,23 +71,31 @@ function step(node) { const simplificationTreeSearches = [ // Basic simplifications that we always try first e.g. (...)^0 => 1 basicsSearch, + // Simplify any division chains so there's at most one division operation. // e.g. 2/x/6 -> 2/(x*6) e.g. 2/(x/6) => 2 * 6/x divisionSearch, + // Adding fractions, cancelling out things in fractions fractionsSearch, + // e.g. addition of polynomial terms: 2x + 4x^2 + x => 4x^2 + 3x // e.g. multiplication of polynomial terms: 2x * x * x^2 => 2x^3 // e.g. multiplication of constants: 10^3 * 10^2 => 10^5 collectAndCombineSearch, + // e.g. 2 + 2 => 4 arithmeticSearch, + // e.g. (2 + x) / 4 => 2/4 + x/4 breakUpNumeratorSearch, + // e.g. 3/x * 2x/5 => (3 * 2x) / (x * 5) multiplyFractionsSearch, + // e.g. (2x + 3)(x + 4) => 2x^2 + 11x + 12 distributeSearch, + // e.g. abs(-4) => 4 functionsSearch, ]; @@ -95,16 +106,15 @@ function step(node) { // a step. Remove unnecessary parens, in case one a step results in more // parens than needed. node = removeUnnecessaryParens(nodeStatus.newNode, true); - if (nodeStatus.hasChanged()) { + if (nodeStatus.hasChanged) { node = flattenOperands(node); nodeStatus.newNode = node.cloneDeep(); return nodeStatus; - } - else { + } else { node = flattenOperands(node); } } - return Node.Status.noChange(node); + return NodeStatus.noChange(node); } // Removes unnecessary parens throughout the steps. @@ -123,13 +133,11 @@ function logSteps(nodeStatus) { // eslint-disable-next-line console.log(nodeStatus.changeType); // eslint-disable-next-line - console.log(print.ascii(nodeStatus.newNode) + '\n'); + console.log(printAscii(nodeStatus.newNode) + "\n"); if (nodeStatus.substeps.length > 0) { // eslint-disable-next-line - console.log('\nsubsteps: '); - nodeStatus.substeps.forEach(substep => substep); + console.log("\nsubsteps: "); + nodeStatus.substeps.forEach((substep) => substep); } } - -module.exports = stepThrough; diff --git a/lib/src/solveEquation/EquationOperations.ts b/lib/src/solveEquation/EquationOperations.ts new file mode 100644 index 00000000..0059187b --- /dev/null +++ b/lib/src/solveEquation/EquationOperations.ts @@ -0,0 +1,265 @@ +// Operations on equation nodes + +import { ChangeTypes } from "../ChangeTypes"; +import { Equation } from "../equation/Equation"; +import { EquationStatus } from "../equation/Status"; +import { Negative } from "../Negative"; +import { Symbols } from "../Symbols"; +import { NodeType } from "../node/NodeType"; +import { NodeCreator } from "../node/Creator"; +import { PolynomialTerm } from "../node/PolynomialTerm"; + +const COMPARATOR_TO_INVERSE = { + ">": "<", + ">=": "<=", + "<": ">", + "<=": ">=", + "=": "=", +}; + +export class EquationOperations { + // Ensures that the given equation has the given symbolName on the left side, + // by swapping the right and left sides if it is only in the right side. + // So 3 = x would become x = 3. + static ensureSymbolInLeftNode(equation, symbolName) { + const leftSideSymbolTerm = Symbols.getLastSymbolTerm( + equation.leftNode, + symbolName + ); + const rightSideSymbolTerm = Symbols.getLastSymbolTerm( + equation.rightNode, + symbolName + ); + + if (!leftSideSymbolTerm) { + if (rightSideSymbolTerm) { + const comparator = COMPARATOR_TO_INVERSE[equation.comparator]; + const oldEquation = equation; + const newEquation = new Equation( + equation.rightNode, + equation.leftNode, + comparator + ); + // no change groups are set for this step because everything changes, so + // they wouldn't be pedagogically helpful. + return new EquationStatus( + ChangeTypes.SWAP_SIDES, + oldEquation, + newEquation + ); + } else { + throw Error("No term with symbol: " + symbolName); + } + } + return EquationStatus.noChange(equation); + } + + // Ensures that a symbol is not in the denominator by multiplying + // both sides by the denominator if there is a symbol present. + static removeSymbolFromDenominator(equation, symbolName) { + // Can't multiply a symbol across non-equal comparators + // because you don't know if it's negative and need to flip the sign + if (equation.comparator !== "=") { + return EquationStatus.noChange(equation); + } + const leftNode = equation.leftNode; + const denominator = Symbols.getLastDenominatorWithSymbolTerm( + leftNode, + symbolName + ); + if (denominator) { + return performTermOperationOnEquation( + equation, + "*", + denominator, + ChangeTypes.MULTIPLY_TO_BOTH_SIDES + ); + } + return EquationStatus.noChange(equation); + } + + // Removes the given symbolName from the right side by adding or subtracting + // it from both sides as appropriate. + // e.g. 2x = 3x + 5 --> 2x - 3x = 5 + // There are actually no cases where we'd remove symbols from the right side + // by multiplying or dividing by a symbol term. + // TODO: support inverting functions e.g. sqrt, ^, log etc. + static removeSymbolFromRightSide(equation, symbolName) { + const rightNode = equation.rightNode; + let symbolTerm = Symbols.getLastSymbolTerm(rightNode, symbolName); + + let inverseOp, inverseTerm, changeType; + if (!symbolTerm) { + return EquationStatus.noChange(equation); + } + + // Clone it so that any operations on it don't affect the node already + // in the equation + symbolTerm = symbolTerm.cloneDeep(); + + if (PolynomialTerm.isPolynomialTerm(rightNode)) { + if (Negative.isNegative(symbolTerm)) { + inverseOp = "+"; + changeType = ChangeTypes.ADD_TO_BOTH_SIDES; + inverseTerm = Negative.negate(symbolTerm); + } else { + inverseOp = "-"; + changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; + inverseTerm = symbolTerm; + } + } else if (NodeType.isOperator(rightNode)) { + if (rightNode.op === "+") { + if (Negative.isNegative(symbolTerm)) { + inverseOp = "+"; + changeType = ChangeTypes.ADD_TO_BOTH_SIDES; + inverseTerm = Negative.negate(symbolTerm); + } else { + inverseOp = "-"; + changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; + inverseTerm = symbolTerm; + } + } else { + // Note that operator '-' won't show up here because subtraction is + // flattened into adding the negative. See 'TRICKY catch' in the README + // for more details. + throw Error("Unsupported operation: " + symbolTerm.op); + } + } else if (NodeType.isUnaryMinus(rightNode)) { + inverseOp = "+"; + changeType = ChangeTypes.ADD_TO_BOTH_SIDES; + inverseTerm = symbolTerm.args[0]; + } else { + throw Error("Unsupported node type: " + rightNode); + } + return performTermOperationOnEquation( + equation, + inverseOp, + inverseTerm, + changeType + ); + } + + // Isolates the given symbolName to the left side by adding, multiplying, subtracting + // or dividing all other symbols and constants from both sides appropriately + // TODO: support inverting functions e.g. sqrt, ^, log etc. + static isolateSymbolOnLeftSide(equation, symbolName) { + let leftNode = equation.leftNode; + + if (NodeType.isParenthesis(leftNode)) { + // if entire left node is a parenthesis, we can ignore the parenthesis + leftNode = leftNode.content; + } + + let nonSymbolTerm = Symbols.getLastNonSymbolTerm(leftNode, symbolName); + let inverseOp, inverseTerm, changeType; + + if (!nonSymbolTerm) { + return EquationStatus.noChange(equation); + } + + // Clone it so that any operations on it don't affect the node already + // in the equation + nonSymbolTerm = nonSymbolTerm.cloneDeep(); + + if (NodeType.isOperator(leftNode)) { + if (leftNode.op === "+") { + if (Negative.isNegative(nonSymbolTerm)) { + inverseOp = "+"; + changeType = ChangeTypes.ADD_TO_BOTH_SIDES; + inverseTerm = Negative.negate(nonSymbolTerm); + } else { + inverseOp = "-"; + changeType = ChangeTypes.SUBTRACT_FROM_BOTH_SIDES; + inverseTerm = nonSymbolTerm; + } + } else if (leftNode.op === "*") { + if (NodeType.isConstantFraction(nonSymbolTerm)) { + inverseOp = "*"; + changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION; + inverseTerm = NodeCreator.operator("/", [ + nonSymbolTerm.args[1], + nonSymbolTerm.args[0], + ]); + } else { + inverseOp = "/"; + changeType = ChangeTypes.DIVIDE_FROM_BOTH_SIDES; + inverseTerm = nonSymbolTerm; + } + } else if (leftNode.op === "/") { + // The non symbol term is always a fraction because it's the + // coefficient of our symbol term. + // If the numerator is 1, we multiply both sides by the denominator, + // otherwise we multiply by the inverse + if (["1", "-1"].indexOf(nonSymbolTerm.args[0].value) !== -1) { + inverseOp = "*"; + changeType = ChangeTypes.MULTIPLY_TO_BOTH_SIDES; + inverseTerm = nonSymbolTerm.args[1]; + } else { + inverseOp = "*"; + changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_INVERSE_FRACTION; + inverseTerm = NodeCreator.operator("/", [ + nonSymbolTerm.args[1], + nonSymbolTerm.args[0], + ]); + } + } else if (leftNode.op === "^") { + // TODO: support roots + return EquationStatus.noChange(equation); + } else { + throw Error("Unsupported operation: " + leftNode.op); + } + } else if (NodeType.isUnaryMinus(leftNode)) { + inverseOp = "*"; + changeType = ChangeTypes.MULTIPLY_BOTH_SIDES_BY_NEGATIVE_ONE; + inverseTerm = NodeCreator.constant(-1); + } else { + throw Error("Unsupported node type: " + leftNode); + } + + return performTermOperationOnEquation( + equation, + inverseOp, + inverseTerm, + changeType + ); + } +} + +// Modifies the left and right sides of an equation by `op`-ing `term` +// to both sides. Returns an Status object. +function performTermOperationOnEquation(equation, op, term, changeType) { + const oldEquation = equation.clone(); + + const leftTerm = term.cloneDeep(); + const rightTerm = term.cloneDeep(); + const leftNode = performTermOperationOnExpression( + equation.leftNode, + op, + leftTerm + ); + const rightNode = performTermOperationOnExpression( + equation.rightNode, + op, + rightTerm + ); + + let comparator = equation.comparator; + if (Negative.isNegative(term) && (op === "*" || op === "/")) { + comparator = COMPARATOR_TO_INVERSE[comparator]; + } + + const newEquation = new Equation(leftNode, rightNode, comparator); + return new EquationStatus(changeType, oldEquation, newEquation); +} + +// Performs an operation of a term on an entire given expression +function performTermOperationOnExpression(expression, op, term) { + const node = NodeType.isOperator(expression) + ? NodeCreator.parenthesis(expression) + : expression; + + term.changeGroup = 1; + const newNode = NodeCreator.operator(op, [node, term]); + + return newNode; +} diff --git a/lib/solveEquation/index.js b/lib/src/solveEquation/index.ts similarity index 70% rename from lib/solveEquation/index.js rename to lib/src/solveEquation/index.ts index c990bcd8..2dce4a87 100644 --- a/lib/solveEquation/index.js +++ b/lib/src/solveEquation/index.ts @@ -1,9 +1,9 @@ -const math = require('mathjs'); +import * as math from "mathjs"; -const stepThrough = require('./stepThrough'); +import { stepThrough } from "./stepThrough"; -function solveEquationString(equationString, debug=false) { - const comparators = ['<=', '>=', '=', '<', '>']; +export function solveEquation(equationString, debug = false) { + const comparators = ["<=", ">=", "=", "<", ">"]; for (let i = 0; i < comparators.length; i++) { const comparator = comparators[i]; @@ -22,8 +22,7 @@ function solveEquationString(equationString, debug=false) { try { leftNode = math.parse(leftSide); rightNode = math.parse(rightSide); - } - catch (err) { + } catch (err) { return []; } if (leftNode && rightNode) { @@ -33,5 +32,3 @@ function solveEquationString(equationString, debug=false) { return []; } - -module.exports = solveEquationString; diff --git a/lib/solveEquation/stepThrough.js b/lib/src/solveEquation/stepThrough.ts similarity index 69% rename from lib/solveEquation/stepThrough.js rename to lib/src/solveEquation/stepThrough.ts index 64380c72..9bab541d 100644 --- a/lib/solveEquation/stepThrough.js +++ b/lib/src/solveEquation/stepThrough.ts @@ -1,39 +1,55 @@ -const ChangeTypes = require('../ChangeTypes'); -const checks = require('../checks'); -const Equation = require('../equation/Equation'); -const EquationOperations = require('./EquationOperations'); -const EquationStatus = require('../equation/Status'); -const evaluate = require('../util/evaluate'); -const factor = require('../factor/stepThrough'); -const flattenOperands = require('../util/flattenOperands'); -const Node = require('../node'); -const removeUnnecessaryParens = require('../util/removeUnnecessaryParens'); -const simplifyExpressionNode = require('../simplifyExpression/stepThrough'); -const Symbols = require('../Symbols'); +import { ChangeTypes } from "../ChangeTypes"; +import { Equation } from "../equation/Equation"; +import { EquationStatus } from "../equation/Status"; +import { evaluate } from "../util/evaluate"; +import { flattenOperands } from "../util/flattenOperands"; +import { removeUnnecessaryParens } from "../util/removeUnnecessaryParens"; +import { Symbols } from "../Symbols"; +import { hasUnsupportedNodes } from "../checks/hasUnsupportedNodes"; +import { canFindRoots } from "../checks/canFindRoots"; +import { NodeCreator } from "../node/Creator"; +import { NodeType } from "../node/NodeType"; +import { resolvesToConstant } from "../checks/resolvesToConstant"; +import { EquationOperations } from "./EquationOperations"; + +import { stepThrough as simplifyExpressionNode } from "../simplifyExpression/stepThrough"; +import { stepThrough as factor } from "../factor/stepThrough"; const COMPARATOR_TO_FUNCTION = { - '=': function(left, right) { return left === right; }, - '>': function(left, right) { return left > right; }, - '>=': function(left, right) { return left >= right; }, - '<': function(left, right) { return left < right; }, - '<=': function(left, right) { return left <= right; }, + "=": function (left, right) { + return left === right; + }, + ">": function (left, right) { + return left > right; + }, + ">=": function (left, right) { + return left >= right; + }, + "<": function (left, right) { + return left < right; + }, + "<=": function (left, right) { + return left <= right; + }, }; // Given a leftNode, rightNode and a comparator, will return the steps to get // the solution. Possible solutions include: // - solving for a variable (e.g. 'x=3' for '3x=4+5') // - the result of comparing values (e.g. 'true' for 3 = 3, 'false' for 3 < 2) -function stepThrough(leftNode, rightNode, comparator, debug=false) { +export function stepThrough(leftNode, rightNode, comparator, debug = false) { let equation = new Equation(leftNode, rightNode, comparator); if (debug) { // eslint-disable-next-line - console.log('\n\nSolving: ' + equation.ascii(false, true)); + console.log("\n\nSolving: " + equation.ascii(false)); } // we can't solve/find steps if there are any unsupported nodes - if (checks.hasUnsupportedNodes(equation.leftNode) || - checks.hasUnsupportedNodes(equation.rightNode)) { + if ( + hasUnsupportedNodes(equation.leftNode) || + hasUnsupportedNodes(equation.rightNode) + ) { return []; } @@ -61,7 +77,7 @@ function stepThrough(leftNode, rightNode, comparator, debug=false) { // Checks if there are roots in the original equation before we // do any simplification. // E.g. (33 + 89) (x - 99) = 0 - if (checks.canFindRoots(equation)) { + if (canFindRoots(equation)) { steps.push(getRootsStatus(equation)); return steps; } @@ -73,7 +89,9 @@ function stepThrough(leftNode, rightNode, comparator, debug=false) { if (steps.length > 0) { const lastStep = steps[steps.length - 1]; equation = Equation.createEquationFromString( - lastStep.newEquation.ascii(), equation.comparator); + lastStep.newEquation.ascii(), + equation.comparator + ); } equation.leftNode = flattenOperands(equation.leftNode); @@ -87,32 +105,33 @@ function stepThrough(leftNode, rightNode, comparator, debug=false) { // The left side of the equation is either factored or simplified. // If it is factor and we can find roots, return them. // e.g. x^2 + 3x + 2 = 0 -> (x + 1) (x + 2) = 0 -> x = -1 - if (checks.canFindRoots(equation)) { + if (canFindRoots(equation)) { steps.push(getRootsStatus(equation)); return steps; } try { equationStatus = step(equation, symbolName); - } - catch (e) { + } catch (e) { // This error happens for some math that we don't support - if (e.message.startsWith('No term with symbol: ')) { + if (e.message.startsWith("No term with symbol: ")) { // eslint-disable-next-line - console.error('Math error: ' + e.message + ', returning no steps'); + console.error("Math error: " + e.message + ", returning no steps"); return []; - } - else { + } else { throw e; // bubble up } } - if (equationStatus.hasChanged()) { + if (equationStatus.hasChanged) { if (equationStatus.newEquation.ascii().length > 300) { // eslint-disable-next-line - throw Error('Math error: Potential infinite loop for equation ' + - originalEquationStr + '. It reached over 300 characters '+ - ' long, so returning no steps'); + throw Error( + "Math error: Potential infinite loop for equation " + + originalEquationStr + + ". It reached over 300 characters " + + " long, so returning no steps" + ); } if (debug) { logSteps(equationStatus); @@ -123,11 +142,14 @@ function stepThrough(leftNode, rightNode, comparator, debug=false) { equation = EquationStatus.resetChangeGroups(equationStatus.newEquation); if (iters++ === MAX_STEP_COUNT) { // eslint-disable-next-line - console.error('Math error: Potential infinite loop for equation: ' + - originalEquationStr + ', returning no steps'); + console.error( + "Math error: Potential infinite loop for equation: " + + originalEquationStr + + ", returning no steps" + ); return []; } - } while (equationStatus.hasChanged()); + } while (equationStatus.hasChanged); return steps; } @@ -145,19 +167,19 @@ function getRootsStatus(equation) { if (solutions.length > 1) { const flattenSolutionsList = []; - solutions.forEach(s => s.items - ? flattenSolutionsList.push(...s.items) - : flattenSolutionsList.push(s)); - allSolutions = Node.Creator.list(flattenSolutionsList); - } - else if (solutions.length === 1) { + solutions.forEach((s) => + s.items + ? flattenSolutionsList.push(...s.items) + : flattenSolutionsList.push(s) + ); + allSolutions = NodeCreator.list(flattenSolutionsList); + } else if (solutions.length === 1) { allSolutions = solutions[0]; - } - else { - allSolutions = Node.Creator.list([]); + } else { + allSolutions = NodeCreator.list([]); } - const roots = new Equation(symbol, allSolutions, '='); + const roots = new Equation(symbol, allSolutions, "="); return new EquationStatus(ChangeTypes.FIND_ROOTS, equation, roots); } @@ -170,7 +192,7 @@ function getRootsStatus(equation) { TODO: handle multiple variable solutions e.g (x + 2) (y + 2) = 0 */ -function getSolutionsAndSymbol (equation) { +function getSolutionsAndSymbol(equation) { const leftNode = equation.leftNode; const solutions = []; @@ -181,11 +203,12 @@ function getSolutionsAndSymbol (equation) { // then it is a factor. (e.g. 2^7 resolves to a constant so it is not a factor, but // x^2 would have factors x = 0) // If left hand side is a multiplication node, return a list of all the valid factors. - if (Node.Type.isOperator(leftNode, '^') && !checks.resolvesToConstant(leftNode)){ + if (NodeType.isOperator(leftNode, "^") && !resolvesToConstant(leftNode)) { factorsWithSymbols = [leftNode]; - } - else { - factorsWithSymbols = equation.leftNode.args.filter(arg => !checks.resolvesToConstant(arg)); + } else { + factorsWithSymbols = equation.leftNode.args.filter( + (arg) => !resolvesToConstant(arg) + ); } /* @@ -199,33 +222,30 @@ function getSolutionsAndSymbol (equation) { let factor = factorsWithSymbols[f]; let exponent = 1; - if (Node.Type.isOperator(factor, '^')) { + if (NodeType.isOperator(factor, "^")) { exponent = parseFloat(factor.args[1].value); factor = factor.args[0]; } - const leftNode = Node.Type.isParenthesis(factor) - ? factor.content - : factor; + const leftNode = NodeType.isParenthesis(factor) ? factor.content : factor; - steps = stepThrough(leftNode, equation.rightNode, '='); + steps = stepThrough(leftNode, equation.rightNode, "="); - if (steps.length === 0 && Node.Type.isSymbol(leftNode)) { + if (steps.length === 0 && NodeType.isSymbol(leftNode)) { // e.g. x = 0 symbol = leftNode; // Push the solution multiple times when we have duplicate roots // e.g. (x + 1)^2 -> x = [-1, -1] solutions.push(...Array(exponent).fill(equation.rightNode)); - } - else if (steps.length !== 0) { + } else if (steps.length !== 0) { // Solving for the variable on one side may sometimes // result in more than one step // e.g. x - 2 = 0 const lastStep = steps.slice(-1)[0]; // Append to a list of roots - if (Node.Type.isSymbol(lastStep.newEquation.leftNode)) { + if (NodeType.isSymbol(lastStep.newEquation.leftNode)) { symbol = lastStep.newEquation.leftNode; solutions.push(...Array(exponent).fill(lastStep.newEquation.rightNode)); } @@ -237,18 +257,20 @@ function getSolutionsAndSymbol (equation) { // Given an equation of constants, will simplify both sides, returning // the steps and the result of the equation e.g. 'True' or 'False' -function solveConstantEquation(equation, debug, steps=[]) { +function solveConstantEquation(equation, debug, steps = []) { const compareFunction = COMPARATOR_TO_FUNCTION[equation.comparator]; if (!compareFunction) { - throw Error('Unexpected comparator'); + throw Error("Unexpected comparator"); } - steps = addSimplificationSteps(steps, equation, true, debug); + steps = addSimplificationSteps(steps, equation, true); if (steps.length > 0) { const lastStep = steps[steps.length - 1]; equation = Equation.createEquationFromString( - lastStep.newEquation.ascii(), equation.comparator); + lastStep.newEquation.ascii(), + equation.comparator + ); } // If the left or right side didn't have any steps, unnecessary parens @@ -256,10 +278,13 @@ function solveConstantEquation(equation, debug, steps=[]) { equation.leftNode = removeUnnecessaryParens(equation.leftNode); equation.rightNode = removeUnnecessaryParens(equation.rightNode); - if (!Node.Type.isConstantOrConstantFraction(equation.leftNode, true) || - !Node.Type.isConstantOrConstantFraction(equation.rightNode, true)) { - throw Error('Expected both nodes to be constants, instead got: ' + - equation.ascii()); + if ( + !NodeType.isConstantOrConstantFraction(equation.leftNode, true) || + !NodeType.isConstantOrConstantFraction(equation.rightNode, true) + ) { + throw Error( + "Expected both nodes to be constants, instead got: " + equation.ascii() + ); } const leftValue = evaluate(equation.leftNode); @@ -267,8 +292,7 @@ function solveConstantEquation(equation, debug, steps=[]) { let changeType; if (compareFunction(leftValue, rightValue)) { changeType = ChangeTypes.STATEMENT_IS_TRUE; - } - else { + } else { changeType = ChangeTypes.STATEMENT_IS_FALSE; } @@ -288,10 +312,13 @@ function step(equation, symbolName) { const solveFunctions = [ // ensure the symbol is always on the left node EquationOperations.ensureSymbolInLeftNode, + // get rid of denominators that have the symbol EquationOperations.removeSymbolFromDenominator, + // remove the symbol from the right side EquationOperations.removeSymbolFromRightSide, + // isolate the symbol on the left side EquationOperations.isolateSymbolOnLeftSide, ]; @@ -299,7 +326,7 @@ function step(equation, symbolName) { for (let i = 0; i < solveFunctions.length; i++) { const equationStatus = solveFunctions[i](equation, symbolName); - if (equationStatus.hasChanged()) { + if (equationStatus.hasChanged) { return equationStatus; } } @@ -307,7 +334,7 @@ function step(equation, symbolName) { } // Simplifies the equation and returns the simplification steps -function addSimplificationSteps(steps, equation, debug=false) { +function addSimplificationSteps(steps, equation, debug = false) { let oldEquation = equation.clone(); /* @@ -322,9 +349,10 @@ function addSimplificationSteps(steps, equation, debug=false) { e.g. 0 = x^2 + 3x + 2 -> x^2 + 3x + 2 = 0 <- swap to the left side */ const leftSimplifySteps = simplifyExpressionNode(equation.leftNode, false); - const simplifiedLeftNode = leftSimplifySteps.length !== 0 - ? leftSimplifySteps.slice(-1)[0].newNode - : equation.leftNode; + const simplifiedLeftNode = + leftSimplifySteps.length !== 0 + ? leftSimplifySteps.slice(-1)[0].newNode + : equation.leftNode; const leftFactorSteps = factor(simplifiedLeftNode, false); const leftSubSteps = []; @@ -345,14 +373,18 @@ function addSimplificationSteps(steps, equation, debug=false) { logSteps(step); } steps.push(step); - } - else if (leftSubSteps.length > 1) { + } else if (leftSubSteps.length > 1) { const lastStep = leftSubSteps[leftSubSteps.length - 1]; - const finalEquation = EquationStatus.resetChangeGroups(lastStep.newEquation); + const finalEquation = EquationStatus.resetChangeGroups( + lastStep.newEquation + ); // no change groups are set here - too much is changing for it to be useful const simplifyStatus = new EquationStatus( ChangeTypes.SIMPLIFY_LEFT_SIDE, - oldEquation, finalEquation, leftSubSteps); + oldEquation, + finalEquation, + leftSubSteps + ); if (debug) { logSteps(simplifyStatus); } @@ -362,7 +394,8 @@ function addSimplificationSteps(steps, equation, debug=false) { // update `equation` to have the new simplified left node if (steps.length > 0) { equation = EquationStatus.resetChangeGroups( - steps[steps.length - 1].newEquation); + steps[steps.length - 1].newEquation + ); } // the updated equation from simplifing the left side is the old equation @@ -383,14 +416,18 @@ function addSimplificationSteps(steps, equation, debug=false) { logSteps(step); } steps.push(step); - } - else if (rightSubSteps.length > 1) { + } else if (rightSubSteps.length > 1) { const lastStep = rightSubSteps[rightSubSteps.length - 1]; - const finalEquation = EquationStatus.resetChangeGroups(lastStep.newEquation); + const finalEquation = EquationStatus.resetChangeGroups( + lastStep.newEquation + ); // no change groups are set here - too much is changing for it to be useful const simplifyStatus = new EquationStatus( ChangeTypes.SIMPLIFY_RIGHT_SIDE, - oldEquation, finalEquation, rightSubSteps); + oldEquation, + finalEquation, + rightSubSteps + ); if (debug) { logSteps(simplifyStatus); } @@ -402,15 +439,12 @@ function addSimplificationSteps(steps, equation, debug=false) { function logSteps(equationStatus) { // eslint-disable-next-line - console.log('\n' + equationStatus.changeType); + console.log("\n" + equationStatus.changeType); // eslint-disable-next-line console.log(equationStatus.newEquation.ascii()); if (equationStatus.substeps.length > 0) { // eslint-disable-next-line - console.log('\n substeps: '); + console.log("\n substeps: "); equationStatus.substeps.forEach(logSteps); } } - - -module.exports = stepThrough; diff --git a/lib/util/Util.js b/lib/src/util/Util.ts similarity index 71% rename from lib/util/Util.js rename to lib/src/util/Util.ts index 033395ab..ce46c995 100644 --- a/lib/util/Util.js +++ b/lib/src/util/Util.ts @@ -1,18 +1,14 @@ /* Various utility functions used in the math stepper */ -const Util = {}; // Adds `value` to a list in `dict`, creating a new list if the key isn't in // the dictionary yet. Returns the updated dictionary. -Util.appendToArrayInObject = function(dict, key, value) { +export function appendToArrayInObject(dict, key, value) { if (dict[key]) { dict[key].push(value); - } - else { + } else { dict[key] = [value]; } return dict; -}; - -module.exports = Util; +} diff --git a/lib/src/util/empty-response.ts b/lib/src/util/empty-response.ts new file mode 100644 index 00000000..d7005489 --- /dev/null +++ b/lib/src/util/empty-response.ts @@ -0,0 +1,7 @@ +/** + * For now an empty array is returned when no steps can be computed. + * Might be changed later on to error throwing? + * */ +export function emptyResponse() { + return []; +} diff --git a/lib/src/util/evaluate.ts b/lib/src/util/evaluate.ts new file mode 100644 index 00000000..a3db45fe --- /dev/null +++ b/lib/src/util/evaluate.ts @@ -0,0 +1,9 @@ +/** + * Evaluates a node to a numerical value + * e.g. the tree representing (2 + 2) * 5 would be evaluated to the number 20 + * it's important that `node` does not contain any symbol nodes + * */ +export function evaluate(node) { + // TODO: once we swap in math-parser, call its evaluate function instead + return node.eval(); +} diff --git a/lib/util/flattenOperands.js b/lib/src/util/flattenOperands.ts similarity index 54% rename from lib/util/flattenOperands.js rename to lib/src/util/flattenOperands.ts index 8e5c0343..a103c3fb 100644 --- a/lib/util/flattenOperands.js +++ b/lib/src/util/flattenOperands.ts @@ -1,74 +1,78 @@ -const evaluate = require('./evaluate'); +import { evaluate } from "./evaluate"; -const Negative = require('../Negative'); -const Node = require('../node'); +import { Negative } from "../Negative"; +import { NodeType } from "../node/NodeType"; +import { NodeCreator } from "../node/Creator"; +import { NodeMixedNumber } from "../node/MixedNumber"; +import { PolynomialTerm } from "../node/PolynomialTerm"; -/* -Background: +/** + * Background: + * + * Expression trees are commonly parsed as binary trees, and mathjs does this too. + * That means that a mathjs expression tree likely looks like: + * http://collegelabs.co/clabs/nld/images/524px-Expression_Tree.svg.png + * + * e.g. 2+2+2 is parsed by mathjs as 2 + 2+2 (a plus node with children 2 and 2+2) + * However... + * 1. This is more complicated than needed. 2+2+2 is the same as 2+(2+2) + * 2. To collect like terms, we actually *need* it to be flat. e.g. with 2x+(2+2x), + * there's no easy way to know that there are two 2x's to collect without + * running up and down the tree. If we flatten to 2x+2+2x, it becomes a lot + * easier to collect like terms to (2x+2x) + 2, which would then be combined to + * 4x + 2 + * The purpose of flatteOperands is to flatten the tree in this way. + * + * e.g. an expression that is grouped in the tree like + * (2 + ((4 * ((1 + 2) + (3 + 4))) * 8)) + * should be flattened to look like: + * (2 + (4 * (1 + 2 + 3 + 4) * 8)) + * + * Subtraction and division are also flattened, though that gets a bit more + * complicated and you may as well start reading through the code if you're + * interested in how that works + * */ -Expression trees are commonly parsed as binary trees, and mathjs does this too. -That means that a mathjs expression tree likely looks like: -http://collegelabs.co/clabs/nld/images/524px-Expression_Tree.svg.png - -e.g. 2+2+2 is parsed by mathjs as 2 + 2+2 (a plus node with children 2 and 2+2) -However... -1. This is more complicated than needed. 2+2+2 is the same as 2+(2+2) -2. To collect like terms, we actually *need* it to be flat. e.g. with 2x+(2+2x), - there's no easy way to know that there are two 2x's to collect without - running up and down the tree. If we flatten to 2x+2+2x, it becomes a lot - easier to collect like terms to (2x+2x) + 2, which would then be combined to - 4x + 2 -The purpose of flatteOperands is to flatten the tree in this way. - -e.g. an expression that is grouped in the tree like -(2 + ((4 * ((1 + 2) + (3 + 4))) * 8)) -should be flattened to look like: -(2 + (4 * (1 + 2 + 3 + 4) * 8)) - -Subtraction and division are also flattened, though that gets a bit more -complicated and you may as well start reading through the code if you're -interested in how that works -*/ - -// Flattens the tree accross the same operation (just + and * for now) -// e.g. 2+2+2 is parsed by mathjs as 2+(2+2), but this would change that to -// 2+2+2, ie one + node that has three children. -// Input: an expression tree -// Output: the expression tree updated with flattened operations -function flattenOperands(node) { - // If the node is a mixed number, do not perform any flattening - // -- Flattening will take out the implicit multiplication, and so - // it will be impossible to tell if the node is a mixed number or - // if it is legitimate multiplication - // -- Converting fractions happens before any other simplification step, - // so the tree *will* get flattened before any other changes happen - if (Node.MixedNumber.isMixedNumber(node)) { +/** + * Flattens the tree accross the same operation (just + and * for now) + * e.g. 2+2+2 is parsed by mathjs as 2+(2+2), but this would change that to + * 2+2+2, ie one + node that has three children. + * Input: an expression tree + * Output: the expression tree updated with flattened operations + * */ +export function flattenOperands(node) { + /** + * If the node is a mixed number, do not perform any flattening + * -- Flattening will take out the implicit multiplication, and so + * it will be impossible to tell if the node is a mixed number or + * if it is legitimate multiplication + * -- Converting fractions happens before any other simplification step, + * so the tree *will* get flattened before any other changes happen + * */ + if (NodeMixedNumber.isMixedNumber(node)) { return node; } - if (Node.Type.isConstant(node, true)) { + if (NodeType.isConstant(node, true)) { // the evaluate() changes unary minuses around constant nodes to constant nodes // with negative values. - const constNode = Node.Creator.constant(evaluate(node)); + const constNode = NodeCreator.constant(evaluate(node)); if (node.changeGroup) { constNode.changeGroup = node.changeGroup; } return constNode; - } - else if (Node.Type.isOperator(node)) { - if ('+-/*'.includes(node.op)) { + } else if (NodeType.isOperator(node)) { + if ("+-/*".includes(node.op)) { let parentOp; - if (node.op === '/') { + if (node.op === "/") { // Division is flattened in partner with multiplication. This means // that after collecting the operands, they'll be children args of * - parentOp = '*'; - } - else if (node.op === '-') { + parentOp = "*"; + } else if (node.op === "-") { // Subtraction is flattened in partner with addition, This means that // after collecting the operands, they'll be children args of + - parentOp = '+'; - } - else { + parentOp = "+"; + } else { parentOp = node.op; } return flattenSupportedOperation(node, parentOp); @@ -80,20 +84,17 @@ function flattenOperands(node) { }); } return node; - } - else if (Node.Type.isParenthesis(node)) { + } else if (NodeType.isParenthesis(node)) { node.content = flattenOperands(node.content); return node; - } - else if (Node.Type.isUnaryMinus(node)) { + } else if (NodeType.isUnaryMinus(node)) { const arg = flattenOperands(node.args[0]); const flattenedNode = Negative.negate(arg, true); if (node.changeGroup) { flattenedNode.changeGroup = node.changeGroup; } return flattenedNode; - } - else if (Node.Type.isFunction(node) && node.fn.args) { + } else if (NodeType.isFunction(node) && node.fn.args) { // node.fn.args will only be populated in the case where a function node is // followed by a node in parenthesis // mathjs parses this incorrectly; we want to convert the node to be @@ -102,32 +103,33 @@ function flattenOperands(node) { // abs(3)(1+2) -> abs(3) * (1+2) const flattenedFn = flattenOperands(node.fn); // e.g. nthRoot(11) const flattenedArg = flattenOperands(node.args[0]); // e.g. x+y - const newNode = Node.Creator.operator( - '*', [flattenedFn, Node.Creator.parenthesis(flattenedArg)]); + const newNode = NodeCreator.operator("*", [ + flattenedFn, + NodeCreator.parenthesis(flattenedArg), + ]); return newNode; - } - else if (Node.Type.isFunction(node, 'abs')) { + } else if (NodeType.isFunction(node, "abs")) { node.args[0] = flattenOperands(node.args[0]); return node; - } - else if (Node.Type.isFunction(node, 'nthRoot')) { + } else if (NodeType.isFunction(node, "nthRoot")) { node.args[0] = flattenOperands(node.args[0]); if (node.args[1]) { node.args[1] = flattenOperands(node.args[1]); } return node; - } - else { + } else { return node; } } -// Flattens operations (see flattenOperands docstring) for an operator node -// with an operation type that can be flattened. Currently * + / are supported. -// Returns the updated, flattened node. -// NOTE: the returned node will be of operation type `parentOp`, regardless of -// the operation type of `node`, unless `node` wasn't changed -// e.g. 2 * 3 / 4 would be * of 2 and 3/4, but 2/3 would stay 2/3 and division +/** + * Flattens operations (see flattenOperands docstring) for an operator node + * with an operation type that can be flattened. Currently * + / are supported. + * Returns the updated, flattened node. + * NOTE: the returned node will be of operation type `parentOp`, regardless of + * the operation type of `node`, unless `node` wasn't changed + * e.g. 2 * 3 / 4 would be * of 2 and 3/4, but 2/3 would stay 2/3 and division + * */ function flattenSupportedOperation(node, parentOp) { // First get the list of operands that this operator operates on. // e.g. 2 + 3 + 4 + 5 is stored as (((2 + 3) + 4) + 5) in the tree and we @@ -139,8 +141,7 @@ function flattenSupportedOperation(node, parentOp) { // with the one operand. if (operands.length === 1) { node = operands[0]; - } - else { + } else { // When we are dealing with flattening division, and there's also // multiplication involved, we might end up with a top level * instead. // e.g. 2*4/5 is parsed with / at the top, but in the end we want 2 * (4/5) @@ -148,13 +149,15 @@ function flattenSupportedOperation(node, parentOp) { // (which is impossible for division), then by recursing through the // original tree for any multiplication node - if there was one, it would // have ended up at the root. - if (node.op === '/' && (operands.length > 2 || - hasMultiplicationBesideDivision(node))) { - node = Node.Creator.operator('*', operands); + if ( + node.op === "/" && + (operands.length > 2 || hasMultiplicationBesideDivision(node)) + ) { + node = NodeCreator.operator("*", operands); } // similarily, - will become + always - else if (node.op === '-') { - node = Node.Creator.operator('+', operands); + else if (node.op === "-") { + node = NodeCreator.operator("+", operands); } // otherwise keep the operator, replace operands else { @@ -162,44 +165,46 @@ function flattenSupportedOperation(node, parentOp) { } // When we collect operands to flatten multiplication, the // multiplication of those operands should never be implicit - if (node.op === '*') { + if (node.op === "*") { node.implicit = false; } } return node; } -// Recursively finds the operands under `parentOp` in the input tree `node`. -// The input tree `node` will always have a parent that is an operation -// of type `op`. -// Op is a string e.g. '+' or '*' -// returns the list of all the node operated on by `parentOp` +/** + * Recursively finds the operands under `parentOp` in the input tree `node`. + * The input tree `node` will always have a parent that is an operation + * of type `op`. + * Op is a string e.g. '+' or '*' + * returns the list of all the node operated on by `parentOp` + * */ function getOperands(node, parentOp) { // We can only recurse on operations of type op. // If the node is not an operator node or of the right operation type, // we can't break up or flatten this tree any further, so we return just // the current node, and recurse on it to flatten its ops. - if (!Node.Type.isOperator(node)) { + if (!NodeType.isOperator(node)) { return [flattenOperands(node)]; } switch (node.op) { - // division is part of flattening multiplication - case '*': - case '/': - if (parentOp !== '*') { - return [flattenOperands(node)]; - } - break; - case '+': - case '-': - if (parentOp !== '+') { + // division is part of flattening multiplication + case "*": + case "/": + if (parentOp !== "*") { + return [flattenOperands(node)]; + } + break; + case "+": + case "-": + if (parentOp !== "+") { + return [flattenOperands(node)]; + } + break; + default: return [flattenOperands(node)]; - } - break; - default: - return [flattenOperands(node)]; } - if (Node.PolynomialTerm.isPolynomialTerm(node, true)) { + if (PolynomialTerm.isPolynomialTerm(node, true)) { node.args.forEach((arg, i) => { node.args[i] = flattenOperands(node.args[i]); }); @@ -210,23 +215,20 @@ function getOperands(node, parentOp) { // coefficient multiplied by a symbol such as 2x^2 or 3y) // This is true if there's an implicit multiplication and the right operand // is a symbol or a symbol to an exponent. - else if (parentOp === '*' && isPolynomialTermMultiplication(node)) { + else if (parentOp === "*" && isPolynomialTermMultiplication(node)) { return maybeFlattenPolynomialTerm(node); - } - else if (parentOp === '*' && node.op === '/') { + } else if (parentOp === "*" && node.op === "/") { return flattenDivision(node); - } - else if (node.op === '-') { + } else if (node.op === "-") { // this operation will become addition e.g. 2 - 3 -> 2 + -(-3) const secondOperand = node.args[1]; const negativeSecondOperand = Negative.negate(secondOperand, true); const operands = [ getOperands(node.args[0], parentOp), - getOperands(negativeSecondOperand, parentOp) + getOperands(negativeSecondOperand, parentOp), ]; return [].concat.apply([], operands); - } - else { + } else { const operands = []; node.args.forEach((child) => { // This will make an array of arrays @@ -236,18 +238,20 @@ function getOperands(node, parentOp) { } } -// Return true iff node is a candidate for simplifying to a polynomial -// term. This function is a helper function for getOperands. -// Context: Usually we'd flatten 2*2*x to a multiplication node with 3 children -// (2, 2, and x) but if we got 2*2x, we want to keep 2x together. -// 2*2*x (a tree stored in two levels because initially nodes only have two -// children) in the flattening process should be turned into 2*2x instead of -// 2*2*x (which has three children). -// So this function would return true for the input 2*2x, if it was stored as -// an expression tree with root node * and children 2*2 and x +/** + * Return true iff node is a candidate for simplifying to a polynomial + * term. This function is a helper function for getOperands. + * Context: Usually we'd flatten 2*2*x to a multiplication node with 3 children + * (2, 2, and x) but if we got 2*2x, we want to keep 2x together. + * 2*2*x (a tree stored in two levels because initially nodes only have two + * children) in the flattening process should be turned into 2*2x instead of + * 2*2*x (which has three children). + * So this function would return true for the input 2*2x, if it was stored as + * an expression tree with root node * and children 2*2 and x + * */ function isPolynomialTermMultiplication(node) { // This concept only applies when we're flattening multiplication operations - if (node.op !== '*') { + if (node.op !== "*") { return false; } // This only makes sense when we're flattening two arguments @@ -257,22 +261,23 @@ function isPolynomialTermMultiplication(node) { // The second node should be for the form x or x^2 (ie a polynomial term // with no coefficient) const secondOperand = node.args[1]; - if (Node.PolynomialTerm.isPolynomialTerm(secondOperand)) { - const polyNode = new Node.PolynomialTerm(secondOperand); + if (PolynomialTerm.isPolynomialTerm(secondOperand)) { + const polyNode = new PolynomialTerm(secondOperand); return !polyNode.hasCoeff(); - } - else { + } else { return false; } } -// Takes a node that might represent a multiplication with a polynomial term -// and flattens it appropriately so the coefficient and symbol are grouped -// together. Returns a new list of operands from this node that should be -// multiplied together. +/** + * Takes a node that might represent a multiplication with a polynomial term + * and flattens it appropriately so the coefficient and symbol are grouped + * together. Returns a new list of operands from this node that should be + * multiplied together. + * */ function maybeFlattenPolynomialTerm(node) { // We recurse on the left side of the tree to find operands so far - const operands = getOperands(node.args[0], '*'); + const operands = getOperands(node.args[0], "*"); // If the last operand (so far) under * was a constant, then it's a // polynomial term. @@ -286,10 +291,9 @@ function maybeFlattenPolynomialTerm(node) { const nextOperand = flattenOperands(node.args[1]); // a coefficient can be constant or a fraction of constants - if (Node.Type.isConstantOrConstantFraction(lastOperand)) { + if (NodeType.isConstantOrConstantFraction(lastOperand)) { // we replace the constant (which we popped) with constant*symbol - operands.push( - Node.Creator.operator('*', [lastOperand, nextOperand], true)); + operands.push(NodeCreator.operator("*", [lastOperand, nextOperand], true)); } // Now we know it isn't a polynomial term, it's just another seperate operand else { @@ -299,52 +303,53 @@ function maybeFlattenPolynomialTerm(node) { return operands; } -// Takes a division node and returns a list of operands -// If there is multiplication in the numerator, the operands returned -// are to be multiplied together. Otherwise, a list of length one with -// just the division node is returned. getOperands might change the -// operator accordingly. +/** + * Takes a division node and returns a list of operands + * If there is multiplication in the numerator, the operands returned + * are to be multiplied together. Otherwise, a list of length one with + * just the division node is returned. getOperands might change the + * operator accordingly. + * */ function flattenDivision(node) { // We recurse on the left side of the tree to find operands so far // Flattening division is always considered part of a bigger picture // of multiplication, so we get operands with '*' - let operands = getOperands(node.args[0], '*'); + let operands = getOperands(node.args[0], "*"); if (operands.length === 1) { node.args[0] = operands.pop(); node.args[1] = flattenOperands(node.args[1]); operands = [node]; - } - else { + } else { // This is the last operand, the term we'll want to add our division to const numerator = operands.pop(); // This is the denominator of the current division node we're recursing on const denominator = flattenOperands(node.args[1]); // Note that this means 2 * 3 * 4 / 5 / 6 * 7 will flatten but keep the 4/5/6 // as an operand - in simplifyDivision.js this is changed to 4/(5*6) - const divisionNode = Node.Creator.operator('/', [numerator, denominator]); + const divisionNode = NodeCreator.operator("/", [numerator, denominator]); operands.push(divisionNode); } return operands; } -// Returns true if there is a * node nested in some division, with no other -// operators or parentheses between them. -// e.g. returns true: 2*3/4, 2 / 5 / 6 * 7 / 8 -// e.g. returns false: 3/4/5, ((3*2) - 5) / 7, (2*5)/6 +/** + * Returns true if there is a * node nested in some division, with no other + * operators or parentheses between them. + * e.g. returns true: 2*3/4, 2 / 5 / 6 * 7 / 8 + * e.g. returns false: 3/4/5, ((3*2) - 5) / 7, (2*5)/6 + * */ function hasMultiplicationBesideDivision(node) { - if (!Node.Type.isOperator(node)) { + if (!NodeType.isOperator(node)) { return false; } - if (node.op === '*') { + if (node.op === "*") { return true; } // we ony recurse through division - if (node.op !== '/') { + if (node.op !== "/") { return false; } return node.args.some(hasMultiplicationBesideDivision); } - -module.exports = flattenOperands; diff --git a/lib/src/util/print.ts b/lib/src/util/print.ts new file mode 100644 index 00000000..5a2952aa --- /dev/null +++ b/lib/src/util/print.ts @@ -0,0 +1,134 @@ +import { PolynomialTerm } from "../node/PolynomialTerm"; +import { NodeCreator } from "../node/Creator"; +import { NodeType } from "../node/NodeType"; +import { flattenOperands } from "./flattenOperands"; + +/** + * Prints an expression node in asciimath + * If showPlusMinus is true, print + - (e.g. 2 + -3) + * If it's false (the default) 2 + -3 would print as 2 - 3 + * (The + - is needed to support the conversion of subtraction to addition of + * negative terms. See flattenOperands for more details if you're curious.) + * */ +export function printAscii(node, showPlusMinus = false) { + node = flattenOperands(node.cloneDeep()); + + let string = printTreeTraversal(node); + if (!showPlusMinus) { + string = string.replace(/\s*?\+\s*?\-\s*?/g, " - "); + } + return string; +} + +export function printTreeTraversal(node, parentNode = undefined) { + if (PolynomialTerm.isPolynomialTerm(node)) { + const polyTerm = new PolynomialTerm(node); + + // This is so we don't print 2/3 x^2 as 2 / 3x^2 + // Still print x/2 as x/2 and not 1/2 x though + if (polyTerm.hasFractionCoeff() && node.op !== "/") { + const coeffTerm = polyTerm.getCoeffNode(); + const coeffStr = printTreeTraversal(coeffTerm); + + const nonCoeffTerm = NodeCreator.polynomialTerm( + polyTerm.getSymbolNode(), + polyTerm.exponent, + null + ); + const nonCoeffStr = printTreeTraversal(nonCoeffTerm); + + return `${coeffStr} ${nonCoeffStr}`; + } + } + + if (NodeType.isIntegerFraction(node)) { + return `${node.args[0]}/${node.args[1]}`; + } + + if (NodeType.isOperator(node)) { + if (node.op === "/" && NodeType.isOperator(node.args[1])) { + return `${printTreeTraversal(node.args[0])} / (${printTreeTraversal( + node.args[1] + )})`; + } + + let opString = ""; + + switch (node.op) { + case "+": + case "-": + // add space between operator and operands + opString = ` ${node.op} `; + break; + case "*": + if (node.implicit) { + break; + } + opString = ` ${node.op} `; + break; + case "/": + // no space for constant fraction divisions (slightly easier to read) + if (NodeType.isConstantFraction(node, true)) { + opString = `${node.op}`; + } else { + opString = ` ${node.op} `; + } + break; + case "^": + // no space for exponents + opString = `${node.op}`; + break; + } + + let str = node.args + .map((arg) => printTreeTraversal(arg, node)) + .join(opString); + + // Need to add parens around any [+, -] operation + // nested in [/, *, ^] operation + // Check #120, #126 issues for more details. + // { "/" [{ "+" ["x", "2"] }, "2"] } -> (x + 2) / 2. + if ( + parentNode && + NodeType.isOperator(parentNode) && + node.op && + parentNode.op && + "*/^".indexOf(parentNode.op) >= 0 && + "+-".indexOf(node.op) >= 0 + ) { + str = `(${str})`; + } + + return str; + } else if (NodeType.isParenthesis(node)) { + return `(${printTreeTraversal(node.content)})`; + } else if (NodeType.isUnaryMinus(node)) { + if ( + NodeType.isOperator(node.args[0]) && + "*/^".indexOf(node.args[0].op) === -1 && + !PolynomialTerm.isPolynomialTerm(node) + ) { + return `-(${printTreeTraversal(node.args[0])})`; + } else { + return `-${printTreeTraversal(node.args[0])}`; + } + } else { + return node.toString(); + } +} + +/** + * Prints an expression node in LaTeX + * (The + - is needed to support the conversion of subtraction to addition of + * negative terms. See flattenOperands for more details if you're curious.) + * */ +export function printLatex(node, showPlusMinus = false) { + let nodeTex = node.toTex({ implicit: "hide" }); + + if (!showPlusMinus) { + // Replaces '+ -' with '-' + nodeTex = nodeTex.replace(/\s*?\+\s*?\-\s*?/g, " - "); + } + + return nodeTex; +} diff --git a/lib/util/removeUnnecessaryParens.js b/lib/src/util/removeUnnecessaryParens.ts similarity index 70% rename from lib/util/removeUnnecessaryParens.js rename to lib/src/util/removeUnnecessaryParens.ts index 3bbf796d..7fdeffb9 100644 --- a/lib/util/removeUnnecessaryParens.js +++ b/lib/src/util/removeUnnecessaryParens.ts @@ -1,18 +1,21 @@ -const checks = require('../checks'); -const LikeTermCollector = require('../simplifyExpression/collectAndCombineSearch/LikeTermCollector'); -const Node = require('../node'); +import { NodeType } from "../node/NodeType"; +import { PolynomialTerm } from "../node/PolynomialTerm"; +import { LikeTermCollector } from "../simplifyExpression/collectAndCombineSearch/LikeTermCollector"; +import { resolvesToConstant } from "../checks/resolvesToConstant"; +import { canSimplifyPolynomialTerms } from "../checks/canSimplifyPolynomialTerms"; +import { MathNode } from "mathjs"; // Removes any parenthesis around nodes that can't be resolved further. // Input must be a top level expression. // Returns a node. -function removeUnnecessaryParens(node, rootNode=false) { +export function removeUnnecessaryParens(node, rootNode = false) { // Parens that wrap everything are redundant. // NOTE: removeUnnecessaryParensSearch recursively removes parens that aren't // needed, while this step only applies to the very top level expression. // e.g. (2 + 3) * 4 can't become 2 + 3 * 4, but if (2 + 3) as a top level // expression can become 2 + 3 if (rootNode) { - while (Node.Type.isParenthesis(node)) { + while (NodeType.isParenthesis(node)) { node = node.content; } } @@ -23,26 +26,21 @@ function removeUnnecessaryParens(node, rootNode=false) { // it doesn't change the value of the expression. Returns a node. // NOTE: after this function is called, every parenthesis node in the // tree should always have an operator node or unary minus as its child. -function removeUnnecessaryParensSearch(node) { - if (Node.Type.isOperator(node)) { +function removeUnnecessaryParensSearch(node: MathNode): MathNode { + if (NodeType.isOperator(node)) { return removeUnnecessaryParensInOperatorNode(node); - } - else if (Node.Type.isFunction(node)) { + } else if (NodeType.isFunction(node)) { return removeUnnecessaryParensInFunctionNode(node); - } - else if (Node.Type.isParenthesis(node)) { + } else if (NodeType.isParenthesis(node)) { return removeUnnecessaryParensInParenthesisNode(node); - } - else if (Node.Type.isConstant(node, true) || Node.Type.isSymbol(node)) { + } else if (NodeType.isConstant(node, true) || NodeType.isSymbol(node)) { return node; - } - else if (Node.Type.isUnaryMinus(node)) { + } else if (NodeType.isUnaryMinus(node)) { const content = node.args[0]; node.args[0] = removeUnnecessaryParensSearch(content); return node; - } - else { - throw Error('Unsupported node type: ' + node.type); + } else { + throw Error("Unsupported node type: " + NodeType); } } @@ -53,9 +51,9 @@ function removeUnnecessaryParensInOperatorNode(node) { // Special case: if the node is an exponent node and the base // is an operator, we should keep the parentheses for the base. // e.g. (2x)^2 -> (2x)^2 instead of 2x^2 - if (node.op === '^' && Node.Type.isParenthesis(node.args[0])) { + if (node.op === "^" && NodeType.isParenthesis(node.args[0])) { const base = node.args[0]; - if (Node.Type.isOperator(base.content)) { + if (NodeType.isOperator(base.content)) { base.content = removeUnnecessaryParensSearch(base.content); node.args[1] = removeUnnecessaryParensSearch(node.args[1]); @@ -71,10 +69,12 @@ function removeUnnecessaryParensInOperatorNode(node) { // all they can be. If that expression is part of an addition or subtraction // operation, we can remove the parenthesis. // e.g. (x+4) + 12 -> x+4 + 12 - if (node.op === '+') { + if (node.op === "+") { node.args.forEach((child, i) => { - if (Node.Type.isParenthesis(child) && - !canCollectOrCombine(child.content)) { + if ( + NodeType.isParenthesis(child) && + !canCollectOrCombine(child.content) + ) { // remove the parens by replacing the child node (in its args list) // with its content node.args[i] = child.content; @@ -84,9 +84,11 @@ function removeUnnecessaryParensInOperatorNode(node) { // This is different from addition because when subtracting a group of terms //in parenthesis, we want to distribute the subtraction. // e.g. `(2 + x) - (1 + x)` => `2 + x - (1 + x)` not `2 + x - 1 + x` - else if (node.op === '-') { - if (Node.Type.isParenthesis(node.args[0]) && - !canCollectOrCombine(node.args[0].content)) { + else if (node.op === "-") { + if ( + NodeType.isParenthesis(node.args[0]) && + !canCollectOrCombine(node.args[0].content) + ) { node.args[0] = node.args[0].content; } } @@ -98,7 +100,7 @@ function removeUnnecessaryParensInOperatorNode(node) { // Returns a node. function removeUnnecessaryParensInFunctionNode(node) { node.args.forEach((child, i) => { - if (Node.Type.isParenthesis(child)) { + if (NodeType.isParenthesis(child)) { child = child.content; } node.args[i] = removeUnnecessaryParensSearch(child); @@ -107,7 +109,6 @@ function removeUnnecessaryParensInFunctionNode(node) { return node; } - // Parentheses are unnecessary when their content is a constant e.g. (2) // or also a parenthesis node, e.g. ((2+3)) - this removes those parentheses. // Note that this means that the type of the content of a ParenthesisNode after @@ -116,7 +117,7 @@ function removeUnnecessaryParensInFunctionNode(node) { function removeUnnecessaryParensInParenthesisNode(node) { // polynomials terms can be complex trees (e.g. 3x^2/5) but don't need parens // around them - if (Node.PolynomialTerm.isPolynomialTerm(node.content)) { + if (PolynomialTerm.isPolynomialTerm(node.content)) { // also recurse to remove any unnecessary parens within the term // (e.g. the exponent might have parens around it) if (node.content.args) { @@ -128,36 +129,36 @@ function removeUnnecessaryParensInParenthesisNode(node) { } // If the content is just one symbol or constant, the parens are not // needed. - else if (Node.Type.isConstant(node.content, true) || - Node.Type.isIntegerFraction(node.content) || - Node.Type.isSymbol(node.content)) { + else if ( + NodeType.isConstant(node.content, true) || + NodeType.isIntegerFraction(node.content) || + NodeType.isSymbol(node.content) + ) { node = node.content; } // If the content is just one function call, the parens are not needed. - else if (Node.Type.isFunction(node.content)) { + else if (NodeType.isFunction(node.content)) { node = node.content; node = removeUnnecessaryParensSearch(node); } // If there is an operation within the parens, then the parens are // likely needed. So, recurse. - else if (Node.Type.isOperator(node.content)) { + else if (NodeType.isOperator(node.content)) { node.content = removeUnnecessaryParensSearch(node.content); // exponent nodes don't need parens around them - if (node.content.op === '^') { + if (node.content.op === "^") { node = node.content; } } // If the content is also parens, we have doubly nested parens. First // recurse on the child node, then set the current node equal to its child // to get rid of the extra parens. - else if (Node.Type.isParenthesis(node.content)) { + else if (NodeType.isParenthesis(node.content)) { node = removeUnnecessaryParensSearch(node.content); - } - else if (Node.Type.isUnaryMinus(node.content)) { + } else if (NodeType.isUnaryMinus(node.content)) { node.content = removeUnnecessaryParensSearch(node.content); - } - else { - throw Error('Unsupported node type: ' + node.content.type); + } else { + throw Error("Unsupported node type: " + node.content.type); } return node; @@ -166,9 +167,9 @@ function removeUnnecessaryParensInParenthesisNode(node) { // Returns true if any of the collect or combine steps can be applied to the // expression tree `node`. function canCollectOrCombine(node) { - return LikeTermCollector.canCollectLikeTerms(node) || - checks.resolvesToConstant(node) || - checks.canSimplifyPolynomialTerms(node); + return ( + LikeTermCollector.canCollectLikeTerms(node) || + resolvesToConstant(node) || + canSimplifyPolynomialTerms(node) + ); } - -module.exports = removeUnnecessaryParens; diff --git a/lib/tsconfig.json b/lib/tsconfig.json new file mode 100644 index 00000000..9b85219c --- /dev/null +++ b/lib/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "declaration": true, + "outDir": "../dist", + "sourceMap": true + }, + "include": [ + "./**/*.ts" + ] +} diff --git a/lib/util/evaluate.js b/lib/util/evaluate.js deleted file mode 100644 index 53d79728..00000000 --- a/lib/util/evaluate.js +++ /dev/null @@ -1,10 +0,0 @@ -// Evaluates a node to a numerical value -// e.g. the tree representing (2 + 2) * 5 would be evaluated to the number 20 -// it's important that `node` does not contain any symbol nodes - -function evaluate(node) { - // TODO: once we swap in math-parser, call its evaluate function instead - return node.eval(); -} - -module.exports = evaluate; diff --git a/lib/util/print.js b/lib/util/print.js deleted file mode 100644 index af7d3a50..00000000 --- a/lib/util/print.js +++ /dev/null @@ -1,125 +0,0 @@ -const flatten = require('./flattenOperands'); -const Node = require('../node'); - -// Prints an expression node in asciimath -// If showPlusMinus is true, print + - (e.g. 2 + -3) -// If it's false (the default) 2 + -3 would print as 2 - 3 -// (The + - is needed to support the conversion of subtraction to addition of -// negative terms. See flattenOperands for more details if you're curious.) -function printAscii(node, showPlusMinus=false) { - node = flatten(node.cloneDeep()); - - let string = printTreeTraversal(node); - if (!showPlusMinus) { - string = string.replace(/\s*?\+\s*?\-\s*?/g, ' - '); - } - return string; -} - -function printTreeTraversal(node, parentNode) { - if (Node.PolynomialTerm.isPolynomialTerm(node)) { - const polyTerm = new Node.PolynomialTerm(node); - // This is so we don't print 2/3 x^2 as 2 / 3x^2 - // Still print x/2 as x/2 and not 1/2 x though - if (polyTerm.hasFractionCoeff() && node.op !== '/') { - const coeffTerm = polyTerm.getCoeffNode(); - const coeffStr = printTreeTraversal(coeffTerm); - - const nonCoeffTerm = Node.Creator.polynomialTerm( - polyTerm.getSymbolNode(), polyTerm.exponent, null); - const nonCoeffStr = printTreeTraversal(nonCoeffTerm); - - return `${coeffStr} ${nonCoeffStr}`; - } - } - - if (Node.Type.isIntegerFraction(node)) { - return `${node.args[0]}/${node.args[1]}`; - } - - if (Node.Type.isOperator(node)) { - if (node.op === '/' && Node.Type.isOperator(node.args[1])) { - return `${printTreeTraversal(node.args[0])} / (${printTreeTraversal(node.args[1])})`; - } - - let opString = ''; - - switch (node.op) { - case '+': - case '-': - // add space between operator and operands - opString = ` ${node.op} `; - break; - case '*': - if (node.implicit) { - break; - } - opString = ` ${node.op} `; - break; - case '/': - // no space for constant fraction divisions (slightly easier to read) - if (Node.Type.isConstantFraction(node, true)) { - opString = `${node.op}`; - } - else { - opString = ` ${node.op} `; - } - break; - case '^': - // no space for exponents - opString = `${node.op}`; - break; - } - - let str = node.args.map(arg => printTreeTraversal(arg, node)).join(opString); - - // Need to add parens around any [+, -] operation - // nested in [/, *, ^] operation - // Check #120, #126 issues for more details. - // { "/" [{ "+" ["x", "2"] }, "2"] } -> (x + 2) / 2. - if (parentNode && - Node.Type.isOperator(parentNode) && - node.op && parentNode.op && - '*/^'.indexOf(parentNode.op) >= 0 && - '+-'.indexOf(node.op) >= 0) { - str = `(${str})`; - } - - return str; - } - else if (Node.Type.isParenthesis(node)) { - return `(${printTreeTraversal(node.content)})`; - } - else if (Node.Type.isUnaryMinus(node)) { - if (Node.Type.isOperator(node.args[0]) && - '*/^'.indexOf(node.args[0].op) === -1 && - !Node.PolynomialTerm.isPolynomialTerm(node)) { - return `-(${printTreeTraversal(node.args[0])})`; - } - else { - return `-${printTreeTraversal(node.args[0])}`; - } - } - else { - return node.toString(); - } -} - -// Prints an expression node in LaTeX -// (The + - is needed to support the conversion of subtraction to addition of -// negative terms. See flattenOperands for more details if you're curious.) -function printLatex(node, showPlusMinus=false) { - let nodeTex = node.toTex({implicit: 'hide'}); - - if (!showPlusMinus) { - // Replaces '+ -' with '-' - nodeTex = nodeTex.replace(/\s*?\+\s*?\-\s*?/g, ' - '); - } - - return nodeTex; -} - -module.exports = { - ascii: printAscii, - latex: printLatex, -}; diff --git a/package-lock.json b/package-lock.json index 05cb853a..be470285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,232 @@ { - "name": "mathsteps", - "version": "0.2.0", + "name": "@taskbase/mathsteps-repo", + "version": "0.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@types/assert": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/assert/-/assert-1.5.4.tgz", + "integrity": "sha512-CaFVW21Ulu0J9sUaEWJjwmhkDkeoxa4fniVSERzZC13sU9v8NNM2lMlkfZZv60j47D+qDt0Lyo8skVP3CTXUdA==", + "dev": true + }, + "@types/expect": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-24.3.0.tgz", + "integrity": "sha512-aq5Z+YFBz5o2b6Sp1jigx5nsmoZMK5Ceurjwy6PZmRv7dEi1jLtkARfvB1ME+OXJUG+7TZUDcv3WoCr/aor6dQ==", + "dev": true, + "requires": { + "expect": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true, + "optional": true + }, + "@types/mathjs": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@types/mathjs/-/mathjs-6.0.11.tgz", + "integrity": "sha512-q9B8ZreO41L38iTY76bCZEtAqzeRs4mNIOZpZ1sLSlcYgvgfFrnf8y8qfmas0tVWrsODjmQbQJFD6RJJJCqJbQ==", + "dev": true, + "requires": { + "decimal.js": "^10.0.0" + }, + "dependencies": { + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", + "dev": true + } + } + }, + "@types/mocha": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.1.tgz", + "integrity": "sha512-NysN+bNqj6E0Hv4CTGWSlPzMW6vTKjDpOteycDkV4IWBsO+PU48JonrPzV9ODjiI2XrjmA05KInLgF5ivZ/YGQ==", + "dev": true + }, + "@types/node": { + "version": "14.14.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.33.tgz", + "integrity": "sha512-oJqcTrgPUF29oUP8AsUqbXGJNuPutsetaa9kTQAQce5Lx5dTYWV02ScBiT/k1BX/Z7pKeqedmvp39Wu4zR7N7g==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", + "dev": true + }, + "@types/yargs": { + "version": "15.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", + "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", + "dev": true + }, "acorn": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", @@ -16,7 +239,7 @@ "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", "dev": true, "requires": { - "acorn": "3.3.0" + "acorn": "^3.0.4" }, "dependencies": { "acorn": { @@ -33,8 +256,8 @@ "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", "dev": true, "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" } }, "ajv-keywords": { @@ -61,13 +284,19 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", "dev": true, "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "array-union": { @@ -76,7 +305,7 @@ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", "dev": true, "requires": { - "array-uniq": "1.0.3" + "array-uniq": "^1.0.1" } }, "array-uniq": { @@ -97,9 +326,9 @@ "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" } }, "balanced-match": { @@ -114,17 +343,32 @@ "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", "dev": true, "requires": { - "callsites": "0.2.0" + "callsites": "^0.2.0" } }, "callsites": { @@ -139,11 +383,11 @@ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" } }, "circular-json": { @@ -158,7 +402,7 @@ "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", "dev": true, "requires": { - "restore-cursor": "1.0.1" + "restore-cursor": "^1.0.1" } }, "cli-width": { @@ -179,6 +423,21 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "commander": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", @@ -188,7 +447,8 @@ "complex.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.1.tgz", - "integrity": "sha1-6pDHoFrs6vOjdtLA9qeEIXJ9aHk=" + "integrity": "sha1-6pDHoFrs6vOjdtLA9qeEIXJ9aHk=", + "dev": true }, "concat-map": { "version": "0.0.1", @@ -202,9 +462,9 @@ "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", "dev": true, "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "typedarray": "0.0.6" + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, "core-util-is": { @@ -213,13 +473,19 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { - "es5-ext": "0.10.30" + "es5-ext": "^0.10.9" } }, "debug": { @@ -234,7 +500,8 @@ "decimal.js": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-7.1.1.tgz", - "integrity": "sha1-GtytfXDXqRxCbXVvHrZWbDvmy88=" + "integrity": "sha1-GtytfXDXqRxCbXVvHrZWbDvmy88=", + "dev": true }, "deep-is": { "version": "0.1.3", @@ -248,13 +515,13 @@ "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", "dev": true, "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.0", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.2" + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" } }, "diff": { @@ -263,14 +530,20 @@ "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", "dev": true }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true + }, "doctrine": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", "dev": true, "requires": { - "esutils": "2.0.2", - "isarray": "1.0.0" + "esutils": "^2.0.2", + "isarray": "^1.0.0" } }, "es5-ext": { @@ -279,8 +552,8 @@ "integrity": "sha1-cUGhaDZpfbq/qq7uQUlc4p9SyTk=", "dev": true, "requires": { - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" + "es6-iterator": "2", + "es6-symbol": "~3.1" } }, "es6-iterator": { @@ -289,9 +562,9 @@ "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-symbol": "3.1.1" + "d": "1", + "es5-ext": "^0.10.14", + "es6-symbol": "^3.1" } }, "es6-map": { @@ -300,12 +573,12 @@ "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", - "es6-set": "0.1.5", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" } }, "es6-set": { @@ -314,11 +587,11 @@ "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" + "event-emitter": "~0.3.5" } }, "es6-symbol": { @@ -327,8 +600,8 @@ "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30" + "d": "1", + "es5-ext": "~0.10.14" } }, "es6-weak-map": { @@ -337,10 +610,10 @@ "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" } }, "escape-string-regexp": { @@ -355,10 +628,10 @@ "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", "dev": true, "requires": { - "es6-map": "0.1.5", - "es6-weak-map": "2.0.2", - "esrecurse": "4.2.0", - "estraverse": "4.2.0" + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" } }, "eslint": { @@ -367,41 +640,41 @@ "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", "dev": true, "requires": { - "babel-code-frame": "6.26.0", - "chalk": "1.1.3", - "concat-stream": "1.6.0", - "debug": "2.6.8", - "doctrine": "2.0.0", - "escope": "3.6.0", - "espree": "3.5.1", - "esquery": "1.0.0", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "file-entry-cache": "2.0.0", - "glob": "7.1.2", - "globals": "9.18.0", - "ignore": "3.3.5", - "imurmurhash": "0.1.4", - "inquirer": "0.12.0", - "is-my-json-valid": "2.16.1", - "is-resolvable": "1.0.0", - "js-yaml": "3.10.0", - "json-stable-stringify": "1.0.1", - "levn": "0.3.0", - "lodash": "4.17.4", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "optionator": "0.8.2", - "path-is-inside": "1.0.2", - "pluralize": "1.2.1", - "progress": "1.1.8", - "require-uncached": "1.0.3", - "shelljs": "0.7.8", - "strip-bom": "3.0.0", - "strip-json-comments": "2.0.1", - "table": "3.8.3", - "text-table": "0.2.0", - "user-home": "2.0.0" + "babel-code-frame": "^6.16.0", + "chalk": "^1.1.3", + "concat-stream": "^1.5.2", + "debug": "^2.1.1", + "doctrine": "^2.0.0", + "escope": "^3.6.0", + "espree": "^3.4.0", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "glob": "^7.0.3", + "globals": "^9.14.0", + "ignore": "^3.2.0", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.7.5", + "strip-bom": "^3.0.0", + "strip-json-comments": "~2.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" } }, "eslint-config-google": { @@ -422,8 +695,8 @@ "integrity": "sha1-DJiLirRttTEAoZVK5LqZXd0n2H4=", "dev": true, "requires": { - "acorn": "5.1.2", - "acorn-jsx": "3.0.1" + "acorn": "^5.1.1", + "acorn-jsx": "^3.0.0" } }, "esprima": { @@ -438,7 +711,7 @@ "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", "dev": true, "requires": { - "estraverse": "4.2.0" + "estraverse": "^4.0.0" } }, "esrecurse": { @@ -447,8 +720,8 @@ "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", "dev": true, "requires": { - "estraverse": "4.2.0", - "object-assign": "4.1.1" + "estraverse": "^4.1.0", + "object-assign": "^4.0.1" } }, "estraverse": { @@ -469,8 +742,8 @@ "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30" + "d": "1", + "es5-ext": "~0.10.14" } }, "exit-hook": { @@ -479,6 +752,31 @@ "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", "dev": true }, + "expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + } + } + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -491,8 +789,8 @@ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", "dev": true, "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" } }, "file-entry-cache": { @@ -501,8 +799,17 @@ "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", "dev": true, "requires": { - "flat-cache": "1.2.2", - "object-assign": "4.1.1" + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" } }, "flat-cache": { @@ -511,16 +818,17 @@ "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", "dev": true, "requires": { - "circular-json": "0.3.3", - "del": "2.2.2", - "graceful-fs": "4.1.11", - "write": "0.2.1" + "circular-json": "^0.3.1", + "del": "^2.0.2", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" } }, "fraction.js": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.0.tgz", - "integrity": "sha1-c5dOL4tR73CVNtYkzJB4Liu2EnQ=" + "integrity": "sha1-c5dOL4tR73CVNtYkzJB4Liu2EnQ=", + "dev": true }, "fs.realpath": { "version": "1.0.0", @@ -540,7 +848,7 @@ "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", "dev": true, "requires": { - "is-property": "1.0.2" + "is-property": "^1.0.0" } }, "glob": { @@ -549,12 +857,12 @@ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "globals": { @@ -569,12 +877,12 @@ "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "dev": true, "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "graceful-fs": { @@ -595,9 +903,15 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "ignore": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.5.tgz", @@ -616,8 +930,8 @@ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -632,19 +946,19 @@ "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", "dev": true, "requires": { - "ansi-escapes": "1.4.0", - "ansi-regex": "2.1.1", - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "cli-width": "2.2.0", - "figures": "1.7.0", - "lodash": "4.17.4", - "readline2": "1.0.1", - "run-async": "0.1.0", - "rx-lite": "3.1.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "through": "2.3.8" + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" } }, "interpret": { @@ -659,7 +973,7 @@ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "is-my-json-valid": { @@ -668,12 +982,18 @@ "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", "dev": true, "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" } }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -686,7 +1006,7 @@ "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", "dev": true, "requires": { - "is-path-inside": "1.0.0" + "is-path-inside": "^1.0.0" } }, "is-path-inside": { @@ -695,7 +1015,7 @@ "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", "dev": true, "requires": { - "path-is-inside": "1.0.2" + "path-is-inside": "^1.0.1" } }, "is-property": { @@ -710,7 +1030,7 @@ "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", "dev": true, "requires": { - "tryit": "1.0.3" + "tryit": "^1.0.1" } }, "isarray": { @@ -743,6 +1063,155 @@ } } }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -755,8 +1224,8 @@ "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", "dev": true, "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "json-stable-stringify": { @@ -765,7 +1234,26 @@ "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", "dev": true, "requires": { - "jsonify": "0.0.0" + "jsonify": "~0.0.0" + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true, + "optional": true + } } }, "jsonify": { @@ -786,8 +1274,8 @@ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "lodash": { @@ -802,10 +1290,17 @@ "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", "dev": true }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "mathjs": { "version": "3.11.2", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-3.11.2.tgz", "integrity": "sha1-0spyPX6SVMxY0UIaWmHYz6yP9kI=", + "dev": true, "requires": { "complex.js": "2.0.1", "decimal.js": "7.1.1", @@ -815,13 +1310,23 @@ "typed-function": "0.10.5" } }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.8" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -877,9 +1382,9 @@ "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", "dev": true, "requires": { - "graceful-fs": "2.0.3", - "inherits": "2.0.3", - "minimatch": "0.2.14" + "graceful-fs": "~2.0.0", + "inherits": "2", + "minimatch": "~0.2.11" } }, "graceful-fs": { @@ -894,8 +1399,8 @@ "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", "dev": true, "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" + "lru-cache": "2", + "sigmund": "~1.0.0" } }, "ms": { @@ -948,7 +1453,7 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "onetime": { @@ -963,12 +1468,12 @@ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", "dev": true, "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" } }, "os-homedir": { @@ -995,6 +1500,12 @@ "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", "dev": true }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -1013,7 +1524,7 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "pluralize": { @@ -1028,6 +1539,41 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + } + } + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", @@ -1040,19 +1586,25 @@ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", "dev": true }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" } }, "readline2": { @@ -1061,8 +1613,8 @@ "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", "dev": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", "mute-stream": "0.0.5" } }, @@ -1072,7 +1624,7 @@ "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "dev": true, "requires": { - "resolve": "1.4.0" + "resolve": "^1.1.6" } }, "require-uncached": { @@ -1081,8 +1633,8 @@ "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", "dev": true, "requires": { - "caller-path": "0.1.0", - "resolve-from": "1.0.1" + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" } }, "resolve": { @@ -1091,7 +1643,7 @@ "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", "dev": true, "requires": { - "path-parse": "1.0.5" + "path-parse": "^1.0.5" } }, "resolve-from": { @@ -1106,8 +1658,8 @@ "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", "dev": true, "requires": { - "exit-hook": "1.1.1", - "onetime": "1.1.0" + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" } }, "rimraf": { @@ -1116,7 +1668,7 @@ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "requires": { - "glob": "7.1.2" + "glob": "^7.0.5" } }, "run-async": { @@ -1125,7 +1677,7 @@ "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", "dev": true, "requires": { - "once": "1.4.0" + "once": "^1.3.0" } }, "rx-lite": { @@ -1143,7 +1695,8 @@ "seed-random": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", - "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=" + "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=", + "dev": true }, "shelljs": { "version": "0.7.8", @@ -1151,9 +1704,9 @@ "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", "dev": true, "requires": { - "glob": "7.1.2", - "interpret": "1.0.4", - "rechoir": "0.6.2" + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" } }, "sigmund": { @@ -1162,25 +1715,55 @@ "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "slice-ansi": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", "dev": true, "requires": { - "safe-buffer": "5.1.1" + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } } }, "string-width": { @@ -1189,9 +1772,18 @@ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" } }, "strip-ansi": { @@ -1200,7 +1792,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "strip-bom": { @@ -1227,12 +1819,12 @@ "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", "dev": true, "requires": { - "ajv": "4.11.8", - "ajv-keywords": "1.5.1", - "chalk": "1.1.3", - "lodash": "4.17.4", + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", "slice-ansi": "0.0.4", - "string-width": "2.1.1" + "string-width": "^2.0.0" }, "dependencies": { "ansi-regex": { @@ -1253,8 +1845,8 @@ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" } }, "strip-ansi": { @@ -1263,7 +1855,7 @@ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "3.0.0" + "ansi-regex": "^3.0.0" } } } @@ -1283,7 +1875,17 @@ "tiny-emitter": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.0.2.tgz", - "integrity": "sha1-jklHDT9V+J4kchA2imu5+1GqFgE=" + "integrity": "sha1-jklHDT9V+J4kchA2imu5+1GqFgE=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } }, "tryit": { "version": "1.0.3", @@ -1291,19 +1893,110 @@ "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", "dev": true }, + "ts-mocha": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-8.0.0.tgz", + "integrity": "sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==", + "dev": true, + "requires": { + "ts-node": "7.0.1", + "tsconfig-paths": "^3.5.0" + }, + "dependencies": { + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "ts-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", + "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } + }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "optional": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true, + "optional": true + } + } + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { - "prelude-ls": "1.1.2" + "prelude-ls": "~1.1.2" } }, "typed-function": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-0.10.5.tgz", - "integrity": "sha1-Lg8Yq9BlIZ+raUpEamXG0ZgYMsA=" + "integrity": "sha1-Lg8Yq9BlIZ+raUpEamXG0ZgYMsA=", + "dev": true }, "typedarray": { "version": "0.0.6", @@ -1311,13 +2004,19 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz", + "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==", + "dev": true + }, "user-home": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", "dev": true, "requires": { - "os-homedir": "1.0.2" + "os-homedir": "^1.0.0" } }, "util-deprecate": { @@ -1344,7 +2043,7 @@ "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", "dev": true, "requires": { - "mkdirp": "0.5.1" + "mkdirp": "^0.5.1" } }, "xtend": { @@ -1352,6 +2051,12 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index 60103bd6..15995d58 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,40 @@ { - "name": "mathsteps", - "version": "0.2.0", - "description": "Step by step math solutions", - "main": "index.js", - "dependencies": { - "mathjs": "3.11.2" + "name": "@taskbase/mathsteps-repo", + "version": "0.0.0", + "private": true, + "description": "Repository of mathsteps lib", + "scripts": { + "build": "npm run test && cd lib && tsc && cp ./package.json ../dist", + "test": "npx ts-mocha -p test/tsconfig.json './test/**/*.test.ts'", + "test-single": "npx ts-mocha -p test/tsconfig.json './test/**/syntax.test.ts'", + "setup-hooks": "ln -s ../../scripts/git-hooks/pre-commit.sh .git/hooks/pre-commit", + "prettify": "prettier --write \"lib/**/*.ts\"", + "prettify-tests": "prettier --write \"test/**/*.ts\"", + "publish-lib": "npm run build && cd dist && npm publish --access=public", + "publish-beta": "npm run build && cd dist && npm publish --tag beta --access=public", + "publish-dryrun": "npm run build && cd dist && npm publish --dry-run --access=public" }, "engines": { "node": ">=6.0.0" }, "devDependencies": { + "@types/assert": "^1.5.4", + "@types/expect": "^24.3.0", + "@types/mathjs": "^6.0.11", + "@types/mocha": "^8.2.1", "eslint": "^3.10.2", "eslint-config-google": "^0.7.0", "eslint-plugin-sort-requires": "^2.1.0", - "mocha": "2.4.5" - }, - "scripts": { - "lint": "node_modules/.bin/eslint .", - "test": "node_modules/.bin/mocha --recursive", - "setup-hooks": "ln -s ../../scripts/git-hooks/pre-commit.sh .git/hooks/pre-commit" + "mocha": "2.4.5", + "prettier": "2.2.1", + "ts-mocha": "^8.0.0", + "ts-node": "^9.1.1", + "typescript": "^4.2.2", + "mathjs": "3.11.2" }, "repository": { "type": "git", - "url": "git+https://github.com/socraticorg/mathsteps.git" + "url": "git+https://github.com/taskbase/mathsteps.git" }, "keywords": [ "math", @@ -33,10 +45,11 @@ "algebra", "system" ], - "author": "Evy Kassirer", + "author": "Taskbase", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/socraticorg/mathsteps/issues" + "url": "https://github.com/taskbase/mathsteps/issues" }, - "homepage": "https://github.com/socraticorg/mathsteps#readme" + "homepage": "https://github.com/taskbase/mathsteps#readme", + "dependencies": {} } diff --git a/scripts/git-hooks/pre-commit.sh b/scripts/git-hooks/pre-commit.sh index 0c566715..2be4abb6 100755 --- a/scripts/git-hooks/pre-commit.sh +++ b/scripts/git-hooks/pre-commit.sh @@ -1,4 +1,5 @@ #!/bin/sh -npm test && -npm run lint +npm run prettify +npm run prettify-tests +npm test diff --git a/test/Negative.test.js b/test/Negative.test.js deleted file mode 100644 index 7bdb63d5..00000000 --- a/test/Negative.test.js +++ /dev/null @@ -1,25 +0,0 @@ -const print = require('../lib/util/print'); - -const Negative = require('../lib/Negative'); - -const TestUtil = require('./TestUtil'); - -function testNegate(exprString, outputStr) { - const inputStr = Negative.negate(TestUtil.parseAndFlatten(exprString)); - TestUtil.testFunctionOutput(print.ascii, inputStr, outputStr); -} - -describe('negate', function() { - const tests = [ - ['1', '-1'], - ['-1', '1'], - ['1/2', '-1/2'], - ['(x+2)', '-(x + 2)'], - ['x', '-x'], - ['x^2', '-x^2'], - ['-y^3', 'y^3'], - ['2/3 x', '-2/3 x'], - ['-5/6 z', '5/6 z'], - ]; - tests.forEach(t => testNegate(t[0], t[1])); -}); diff --git a/test/Negative.test.ts b/test/Negative.test.ts new file mode 100644 index 00000000..49669cd2 --- /dev/null +++ b/test/Negative.test.ts @@ -0,0 +1,25 @@ +import { printAscii } from "../lib/src/util/print"; + +import { Negative } from "../lib/src/Negative"; + +import { TestUtil } from "./TestUtil"; + +function testNegate(exprString, outputStr) { + const inputStr = Negative.negate(TestUtil.parseAndFlatten(exprString)); + TestUtil.testFunctionOutput(printAscii, inputStr, outputStr); +} + +describe("negate", function () { + const tests = [ + ["1", "-1"], + ["-1", "1"], + ["1/2", "-1/2"], + ["(x+2)", "-(x + 2)"], + ["x", "-x"], + ["x^2", "-x^2"], + ["-y^3", "y^3"], + ["2/3 x", "-2/3 x"], + ["-5/6 z", "5/6 z"], + ]; + tests.forEach((t) => testNegate(t[0], t[1])); +}); diff --git a/test/Node/MixedNumber.test.js b/test/Node/MixedNumber.test.js deleted file mode 100644 index 25f79f0c..00000000 --- a/test/Node/MixedNumber.test.js +++ /dev/null @@ -1,73 +0,0 @@ -const MixedNumber = require('../../lib/node/MixedNumber'); -const TestUtil = require('../TestUtil'); - -function testIsMixedNumber(input, output) { - TestUtil.testBooleanFunction(MixedNumber.isMixedNumber, input, output); -} - -function testIsNegativeMixedNumber(input, output) { - TestUtil.testBooleanFunction(MixedNumber.isNegativeMixedNumber, input, output); -} - -function testGetWholeNumberValue(input, output) { - input = TestUtil.parseAndFlatten(input); - TestUtil.testFunctionOutput(MixedNumber.getWholeNumberValue, input, output); -} - -function testGetNumeratorValue(input, output) { - input = TestUtil.parseAndFlatten(input); - TestUtil.testFunctionOutput(MixedNumber.getNumeratorValue, input, output); -} - -function testGetDenominatorValue(input, output) { - input = TestUtil.parseAndFlatten(input); - TestUtil.testFunctionOutput(MixedNumber.getDenominatorValue, input, output); -} - -describe('isMixedNumber', function () { - const tests = [ - ['5(1)/(6)', true], - ['19(2)/(3)', true], - ['-1(7)/(8)', true], - ['4*(1/2)', false], - ['(1/2)3', false], - ['3*10/15', false], - ]; - tests.forEach(t => testIsMixedNumber(t[0], t[1])); -}); - -describe('isNegativeMixedNumber', function () { - const tests = [ - ['-1(7)/(8)', true], - ['5(1)/(6)', false], - ['19(2)/(3)', false], - ]; - tests.forEach(t => testIsNegativeMixedNumber(t[0], t[1])); -}); - -describe('getWholeNumber', function () { - const tests = [ - ['5(1)/(6)', 5], - ['19(2)/(3)', 19], - ['-1(7)/(8)', 1], - ]; - tests.forEach(t => testGetWholeNumberValue(t[0], t[1])); -}); - -describe('getNumerator', function () { - const tests = [ - ['5(1)/(6)', 1], - ['19(2)/(3)', 2], - ['-1(7)/(8)', 7], - ]; - tests.forEach(t => testGetNumeratorValue(t[0], t[1])); -}); - -describe('getDenominator', function () { - const tests = [ - ['5(1)/(6)', 6], - ['19(2)/(3)', 3], - ['-1(7)/(8)', 8], - ]; - tests.forEach(t => testGetDenominatorValue(t[0], t[1])); -}); diff --git a/test/Node/MixedNumber.test.ts b/test/Node/MixedNumber.test.ts new file mode 100644 index 00000000..5759d124 --- /dev/null +++ b/test/Node/MixedNumber.test.ts @@ -0,0 +1,85 @@ +import { TestUtil } from "../TestUtil"; +import { NodeMixedNumber } from "../../lib/src/node/MixedNumber"; + +function testIsMixedNumber(input, output) { + TestUtil.testBooleanFunction(NodeMixedNumber.isMixedNumber, input, output); +} + +function testIsNegativeMixedNumber(input, output) { + TestUtil.testBooleanFunction( + NodeMixedNumber.isNegativeMixedNumber, + input, + output + ); +} + +function testGetWholeNumberValue(input, output) { + input = TestUtil.parseAndFlatten(input); + TestUtil.testFunctionOutput( + NodeMixedNumber.getWholeNumberValue, + input, + output + ); +} + +function testGetNumeratorValue(input, output) { + input = TestUtil.parseAndFlatten(input); + TestUtil.testFunctionOutput(NodeMixedNumber.getNumeratorValue, input, output); +} + +function testGetDenominatorValue(input, output) { + input = TestUtil.parseAndFlatten(input); + TestUtil.testFunctionOutput( + NodeMixedNumber.getDenominatorValue, + input, + output + ); +} + +describe("isMixedNumber", function () { + const tests = [ + ["5(1)/(6)", true], + ["19(2)/(3)", true], + ["-1(7)/(8)", true], + ["4*(1/2)", false], + ["(1/2)3", false], + ["3*10/15", false], + ]; + tests.forEach((t) => testIsMixedNumber(t[0], t[1])); +}); + +describe("isNegativeMixedNumber", function () { + const tests = [ + ["-1(7)/(8)", true], + ["5(1)/(6)", false], + ["19(2)/(3)", false], + ]; + tests.forEach((t) => testIsNegativeMixedNumber(t[0], t[1])); +}); + +describe("getWholeNumber", function () { + const tests = [ + ["5(1)/(6)", 5], + ["19(2)/(3)", 19], + ["-1(7)/(8)", 1], + ]; + tests.forEach((t) => testGetWholeNumberValue(t[0], t[1])); +}); + +describe("getNumerator", function () { + const tests = [ + ["5(1)/(6)", 1], + ["19(2)/(3)", 2], + ["-1(7)/(8)", 7], + ]; + tests.forEach((t) => testGetNumeratorValue(t[0], t[1])); +}); + +describe("getDenominator", function () { + const tests = [ + ["5(1)/(6)", 6], + ["19(2)/(3)", 3], + ["-1(7)/(8)", 8], + ]; + tests.forEach((t) => testGetDenominatorValue(t[0], t[1])); +}); diff --git a/test/Node/NthRootTerm.test.js b/test/Node/NthRootTerm.test.js deleted file mode 100644 index fa4976f5..00000000 --- a/test/Node/NthRootTerm.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const NthRootTerm = require('../../lib/node/NthRootTerm'); - -const TestUtil = require('../TestUtil'); - -function testIsNthRootTerm(exprStr, isTerm) { - TestUtil.testBooleanFunction(NthRootTerm.isNthRootTerm, exprStr, isTerm); -} - -describe('classifies nth root terms correctly', function() { - const tests = [ - ['nthRoot(3)', true], - ['nthRoot(4, 3)', true], - ['nthRoot(x)', true], - ['nthRoot(x)^2', true], - ['nthRoot(4*x^2, 2)', true], - ['4nthRoot(x)', true], - ['2*nthRoot(3,2)', true], - ['-nthRoot(y^2)', true], - ['nthRoot(x) * nthRoot(x)', false], - ['nthRoot(2) + nthRoot(5)', false], - ['3', false], - ['x', false], - ['y^5', false], - ]; - tests.forEach(t => testIsNthRootTerm(t[0], t[1])); -}); diff --git a/test/Node/NthRootTerm.test.ts b/test/Node/NthRootTerm.test.ts new file mode 100644 index 00000000..79d6c120 --- /dev/null +++ b/test/Node/NthRootTerm.test.ts @@ -0,0 +1,26 @@ +import { NthRootTerm } from "../../lib/src/node/NthRootTerm"; + +import { TestUtil } from "../TestUtil"; + +function testIsNthRootTerm(exprStr, isTerm) { + TestUtil.testBooleanFunction(NthRootTerm.isNthRootTerm, exprStr, isTerm); +} + +describe("classifies nth root terms correctly", function () { + const tests = [ + ["nthRoot(3)", true], + ["nthRoot(4, 3)", true], + ["nthRoot(x)", true], + ["nthRoot(x)^2", true], + ["nthRoot(4*x^2, 2)", true], + ["4nthRoot(x)", true], + ["2*nthRoot(3,2)", true], + ["-nthRoot(y^2)", true], + ["nthRoot(x) * nthRoot(x)", false], + ["nthRoot(2) + nthRoot(5)", false], + ["3", false], + ["x", false], + ["y^5", false], + ]; + tests.forEach((t) => testIsNthRootTerm(t[0], t[1])); +}); diff --git a/test/Node/PolynomialTerm.test.js b/test/Node/PolynomialTerm.test.js deleted file mode 100644 index 5c5c97b9..00000000 --- a/test/Node/PolynomialTerm.test.js +++ /dev/null @@ -1,23 +0,0 @@ -const PolynomialTerm = require('../../lib/node/PolynomialTerm'); - -const TestUtil = require('../TestUtil'); - -function testIsPolynomialTerm(exprStr, isTerm) { - TestUtil.testBooleanFunction(PolynomialTerm.isPolynomialTerm, exprStr, isTerm); -} - -describe('classifies symbol terms correctly', function() { - const tests = [ - ['x', true], - ['x^2', true], - ['y^55', true], - ['y^4/4', true], - ['5y/3', true], - ['x^y', true], - ['3', false], - ['2^5', false], - ['x*y^5', false], - ['-12y^5/-3', true], - ]; - tests.forEach(t => testIsPolynomialTerm(t[0], t[1])); -}); diff --git a/test/Node/PolynomialTerm.test.ts b/test/Node/PolynomialTerm.test.ts new file mode 100644 index 00000000..abede425 --- /dev/null +++ b/test/Node/PolynomialTerm.test.ts @@ -0,0 +1,27 @@ +import { PolynomialTerm } from "../../lib/src/node/PolynomialTerm"; + +import { TestUtil } from "../TestUtil"; + +function testIsPolynomialTerm(exprStr, isTerm) { + TestUtil.testBooleanFunction( + PolynomialTerm.isPolynomialTerm, + exprStr, + isTerm + ); +} + +describe("classifies symbol terms correctly", function () { + const tests = [ + ["x", true], + ["x^2", true], + ["y^55", true], + ["y^4/4", true], + ["5y/3", true], + ["x^y", true], + ["3", false], + ["2^5", false], + ["x*y^5", false], + ["-12y^5/-3", true], + ]; + tests.forEach((t) => testIsPolynomialTerm(t[0], t[1])); +}); diff --git a/test/Node/Type.test.js b/test/Node/Type.test.js deleted file mode 100644 index eb31a08b..00000000 --- a/test/Node/Type.test.js +++ /dev/null @@ -1,165 +0,0 @@ -const assert = require('assert'); -const math = require('mathjs'); - -const Negative = require('../../lib/Negative'); -const Node = require('../../lib/node'); -const TestUtil = require('../TestUtil'); - -const constNode = Node.Creator.constant; - -describe('Node.Type works', function () { - it('(2+2) parenthesis', function () { - assert.deepEqual( - Node.Type.isParenthesis(math.parse('(2+2)')), - true); - }); - it('10 constant', function () { - assert.deepEqual( - Node.Type.isConstant(math.parse(10)), - true); - }); - it('-2 constant', function () { - assert.deepEqual( - Node.Type.isConstant(constNode(-2)), - true); - }); - it('2+2 operator without operator param', function () { - assert.deepEqual( - Node.Type.isOperator(math.parse('2+2')), - true); - }); - it('2+2 operator with correct operator param', function () { - assert.deepEqual( - Node.Type.isOperator(math.parse('2+2'), '+'), - true); - }); - it('2+2 operator with incorrect operator param', function () { - assert.deepEqual( - Node.Type.isOperator(math.parse('2+2'), '-'), - false); - }); - it('-x not operator', function () { - assert.deepEqual( - Node.Type.isOperator(math.parse('-x')), - false); - }); - it('-x not symbol', function () { - assert.deepEqual( - Node.Type.isSymbol(math.parse('-x')), - false); - }); - it('y symbol', function () { - assert.deepEqual( - Node.Type.isSymbol(math.parse('y')), - true); - }); - it('abs(5) is abs function', function () { - assert.deepEqual( - Node.Type.isFunction(math.parse('abs(5)'), 'abs'), - true); - }); - it('sqrt(5) is not abs function', function () { - assert.deepEqual( - Node.Type.isFunction(math.parse('sqrt(5)'), 'abs'), - false); - }); - // it('nthRoot(4) is an nthRoot function', function () { - // assert.deepEqual( - // Node.Type.isFunction(math.parse('nthRoot(5)'), 'nthRoot'), - // true); - // }); -}); - -describe('isConstantOrConstantFraction', function () { - it('2 true', function () { - assert.deepEqual( - Node.Type.isConstantOrConstantFraction(math.parse('2')), - true); - }); - it('2/9 true', function () { - assert.deepEqual( - Node.Type.isConstantOrConstantFraction(math.parse('4/9')), - true); - }); - it('x/2 false', function () { - assert.deepEqual( - Node.Type.isConstantOrConstantFraction(math.parse('x/2')), - false); - }); -}); - -describe('isIntegerFraction', function () { - it('4/5 true', function () { - assert.deepEqual( - Node.Type.isIntegerFraction(math.parse('4/5')), - true); - }); - it('4.3/5 false', function () { - assert.deepEqual( - Node.Type.isIntegerFraction(math.parse('4.3/5')), - false); - }); - it('4x/5 false', function () { - assert.deepEqual( - Node.Type.isIntegerFraction(math.parse('4x/5')), - false); - }); - it('5 false', function () { - assert.deepEqual( - Node.Type.isIntegerFraction(math.parse('5')), - false); - }); -}); - -describe('isFraction', function () { - it('2/3 true', function () { - assert.deepEqual( - Node.CustomType.isFraction(math.parse('2/3')), - true); - }); - it('-2/3 true', function () { - assert.deepEqual( - Node.CustomType.isFraction(math.parse('-2/3')), - true); - }); - it('-(2/3) true', function () { - assert.deepEqual( - Node.CustomType.isFraction(math.parse('-(2/3)')), - true); - }); - it('(2/3) true', function () { - assert.deepEqual( - Node.CustomType.isFraction(math.parse('(2/3)')), - true); - }); -}); - -describe('getFraction', function () { - it('2/3 2/3', function () { - assert.deepEqual( - Node.CustomType.getFraction(math.parse('2/3')), - math.parse('2/3')); - }); - - const expectedFraction = math.parse('2/3'); - TestUtil.removeComments(expectedFraction); - - it('(2/3) 2/3', function () { - assert.deepEqual( - Node.CustomType.getFraction(math.parse('(2/3)')), - expectedFraction); - }); - - // we can't just parse -2/3 to get the expected fraction, - // because that will put a unary minus on the 2, - // instead of using a constant node of value -2 as our code does - const negativeExpectedFraction = math.parse('2/3'); - TestUtil.removeComments(negativeExpectedFraction); - Negative.negate(negativeExpectedFraction); - - it('-(2/3) -2/3', function () { - assert.deepEqual( - Node.CustomType.getFraction(math.parse('-(2/3)')), - negativeExpectedFraction); - }); -}); diff --git a/test/Node/Type.test.ts b/test/Node/Type.test.ts new file mode 100644 index 00000000..169ec9be --- /dev/null +++ b/test/Node/Type.test.ts @@ -0,0 +1,134 @@ +import * as math from "mathjs"; +import { Negative } from "../../lib/src/Negative"; +import { TestUtil } from "../TestUtil"; +import { NodeType } from "../../lib/src/node/NodeType"; +import { NodeCreator } from "../../lib/src/node/Creator"; +import { NodeCustomType } from "../../lib/src/node/CustomType"; +import assert = require("assert"); + +const constNode = NodeCreator.constant; + +describe("NodeType works", function () { + it("(2+2) parenthesis", function () { + assert.deepEqual(NodeType.isParenthesis(math.parse("(2+2)")), true); + }); + it("10 constant", function () { + assert.deepEqual(NodeType.isConstant((math.parse as any)(10)), true); + }); + it("-2 constant", function () { + assert.deepEqual(NodeType.isConstant(constNode(-2)), true); + }); + it("2+2 operator without operator param", function () { + assert.deepEqual(NodeType.isOperator(math.parse("2+2")), true); + }); + it("2+2 operator with correct operator param", function () { + assert.deepEqual(NodeType.isOperator(math.parse("2+2"), "+"), true); + }); + it("2+2 operator with incorrect operator param", function () { + assert.deepEqual(NodeType.isOperator(math.parse("2+2"), "-"), false); + }); + it("-x not operator", function () { + assert.deepEqual(NodeType.isOperator(math.parse("-x")), false); + }); + it("-x not symbol", function () { + assert.deepEqual(NodeType.isSymbol(math.parse("-x")), false); + }); + it("y symbol", function () { + assert.deepEqual(NodeType.isSymbol(math.parse("y")), true); + }); + it("abs(5) is abs function", function () { + assert.deepEqual(NodeType.isFunction(math.parse("abs(5)"), "abs"), true); + }); + it("sqrt(5) is not abs function", function () { + assert.deepEqual(NodeType.isFunction(math.parse("sqrt(5)"), "abs"), false); + }); + // it('nthRoot(4) is an nthRoot function', function () { + // assert.deepEqual( + // NodeType.isFunction(math.parse('nthRoot(5)'), 'nthRoot'), + // true); + // }); +}); + +describe("isConstantOrConstantFraction", function () { + it("2 true", function () { + assert.deepEqual( + NodeType.isConstantOrConstantFraction(math.parse("2")), + true + ); + }); + it("2/9 true", function () { + assert.deepEqual( + NodeType.isConstantOrConstantFraction(math.parse("4/9")), + true + ); + }); + it("x/2 false", function () { + assert.deepEqual( + NodeType.isConstantOrConstantFraction(math.parse("x/2")), + false + ); + }); +}); + +describe("isIntegerFraction", function () { + it("4/5 true", function () { + assert.deepEqual(NodeType.isIntegerFraction(math.parse("4/5")), true); + }); + it("4.3/5 false", function () { + assert.deepEqual(NodeType.isIntegerFraction(math.parse("4.3/5")), false); + }); + it("4x/5 false", function () { + assert.deepEqual(NodeType.isIntegerFraction(math.parse("4x/5")), false); + }); + it("5 false", function () { + assert.deepEqual(NodeType.isIntegerFraction(math.parse("5")), false); + }); +}); + +describe("isFraction", function () { + it("2/3 true", function () { + assert.deepEqual(NodeCustomType.isFraction(math.parse("2/3")), true); + }); + it("-2/3 true", function () { + assert.deepEqual(NodeCustomType.isFraction(math.parse("-2/3")), true); + }); + it("-(2/3) true", function () { + assert.deepEqual(NodeCustomType.isFraction(math.parse("-(2/3)")), true); + }); + it("(2/3) true", function () { + assert.deepEqual(NodeCustomType.isFraction(math.parse("(2/3)")), true); + }); +}); + +describe("getFraction", function () { + it("2/3 2/3", function () { + assert.deepEqual( + NodeCustomType.getFraction(math.parse("2/3")), + math.parse("2/3") + ); + }); + + const expectedFraction = math.parse("2/3"); + TestUtil.removeComments(expectedFraction); + + it("(2/3) 2/3", function () { + assert.deepEqual( + NodeCustomType.getFraction(math.parse("(2/3)")), + expectedFraction + ); + }); + + // we can't just parse -2/3 to get the expected fraction, + // because that will put a unary minus on the 2, + // instead of using a constant node of value -2 as our code does + const negativeExpectedFraction = math.parse("2/3"); + TestUtil.removeComments(negativeExpectedFraction); + Negative.negate(negativeExpectedFraction); + + it("-(2/3) -2/3", function () { + assert.deepEqual( + NodeCustomType.getFraction(math.parse("-(2/3)")), + negativeExpectedFraction + ); + }); +}); diff --git a/test/Symbols.test.js b/test/Symbols.test.js deleted file mode 100644 index 9a9bc104..00000000 --- a/test/Symbols.test.js +++ /dev/null @@ -1,58 +0,0 @@ -const assert = require('assert'); -const math = require('mathjs'); - -const print = require('../lib/util/print'); -const Symbols = require('../lib/Symbols'); - -function runTest(functionToTest, exprString, expectedOutput, symbolName) { - it(exprString + ' -> ' + expectedOutput, function () { - const expression = math.parse(exprString); - const foundSymbol = functionToTest(expression, symbolName); - assert.deepEqual( - print.ascii(foundSymbol), - expectedOutput - ); - }); -} - -describe('getLastSymbolTerm', function() { - const tests = [ - ['1/x', '1 / x', 'x'], - ['1/(3x)', '1 / (3x)', 'x'], - ['3x', '3x', 'x'], - ['x + 3x + 2', '3x', 'x'], - ['x/(x+3)', 'x / (x + 3)', 'x'], - ['x/(x+3) + y', 'x / (x + 3)', 'x'], - ['x/(x+3) + y + 3x', 'y', 'y'], - ['x/(x+3) + y + 3x + 1/2y', '1/2 y', 'y'], - ]; - - tests.forEach(t => runTest(Symbols.getLastSymbolTerm, t[0], t[1], t[2])); -}); - -describe('getLastNonSymbolTerm', function() { - const tests = [ - ['4x^2 + 2x + 2/4', '2/4', 'x'], - ['4x^2 + 2/4 + x', '2/4', 'x'], - ['4x^2 + 2x + y', 'y', 'x'], - ['4x^2', '4', 'x'], - ]; - - tests.forEach(t => runTest(Symbols.getLastNonSymbolTerm, t[0], t[1], t[2])); -}); - -describe('getLastDenominatorWithSymbolTerm', function() { - const tests = [ - ['1/x', 'x', 'x'], - ['1/(x+2)', '(x + 2)', 'x'], - ['1/(x+2) + 3x', '(x + 2)', 'x'], - ['1/(x+2) + 3x/(1+x)', '(1 + x)', 'x'], - ['1/(x+2) + (x+1)/(2x+3)', '(2x + 3)', 'x'], - ['1/x + x/5', 'x', 'x'], - ['2 + 2/x + x/2', 'x', 'x'], - ['2 + 2/y + x/2', 'y', 'y'], - ['2y + 2/x + 3/(2y) + x/2', '(2y)', 'y'], - ]; - - tests.forEach(t => runTest(Symbols.getLastDenominatorWithSymbolTerm, t[0], t[1], t[2])); -}); diff --git a/test/Symbols.test.ts b/test/Symbols.test.ts new file mode 100644 index 00000000..9d6a97cb --- /dev/null +++ b/test/Symbols.test.ts @@ -0,0 +1,57 @@ +import * as math from "mathjs"; + +import { Symbols } from "../lib/src/Symbols"; +import assert = require("assert"); +import { printAscii } from "../lib/src/util/print"; + +function runTest(functionToTest, exprString, expectedOutput, symbolName) { + it(exprString + " -> " + expectedOutput, function () { + const expression = math.parse(exprString); + const foundSymbol = functionToTest(expression, symbolName); + assert.deepEqual(printAscii(foundSymbol), expectedOutput); + }); +} + +describe("getLastSymbolTerm", function () { + const tests = [ + ["1/x", "1 / x", "x"], + ["1/(3x)", "1 / (3x)", "x"], + ["3x", "3x", "x"], + ["x + 3x + 2", "3x", "x"], + ["x/(x+3)", "x / (x + 3)", "x"], + ["x/(x+3) + y", "x / (x + 3)", "x"], + ["x/(x+3) + y + 3x", "y", "y"], + ["x/(x+3) + y + 3x + 1/2y", "1/2 y", "y"], + ]; + + tests.forEach((t) => runTest(Symbols.getLastSymbolTerm, t[0], t[1], t[2])); +}); + +describe("getLastNonSymbolTerm", function () { + const tests = [ + ["4x^2 + 2x + 2/4", "2/4", "x"], + ["4x^2 + 2/4 + x", "2/4", "x"], + ["4x^2 + 2x + y", "y", "x"], + ["4x^2", "4", "x"], + ]; + + tests.forEach((t) => runTest(Symbols.getLastNonSymbolTerm, t[0], t[1], t[2])); +}); + +describe("getLastDenominatorWithSymbolTerm", function () { + const tests = [ + ["1/x", "x", "x"], + ["1/(x+2)", "(x + 2)", "x"], + ["1/(x+2) + 3x", "(x + 2)", "x"], + ["1/(x+2) + 3x/(1+x)", "(1 + x)", "x"], + ["1/(x+2) + (x+1)/(2x+3)", "(2x + 3)", "x"], + ["1/x + x/5", "x", "x"], + ["2 + 2/x + x/2", "x", "x"], + ["2 + 2/y + x/2", "y", "y"], + ["2y + 2/x + 3/(2y) + x/2", "(2y)", "y"], + ]; + + tests.forEach((t) => + runTest(Symbols.getLastDenominatorWithSymbolTerm, t[0], t[1], t[2]) + ); +}); diff --git a/test/TestUtil.js b/test/TestUtil.js deleted file mode 100644 index c4b67b24..00000000 --- a/test/TestUtil.js +++ /dev/null @@ -1,68 +0,0 @@ -const assert = require('assert'); -const math = require('mathjs'); - -const flatten = require('../lib/util/flattenOperands'); -const print = require('../lib/util/print'); - -// TestUtil contains helper methods to share code across tests -const TestUtil = {}; - -// Takes in an input string and returns a flattened and parsed node -TestUtil.parseAndFlatten = function (exprString) { - return flatten(math.parse(exprString)); -}; - -// Tests a function that takes an input string and check its output -TestUtil.testFunctionOutput = function (fn, input, output) { - it(input + ' -> ' + output, () => { - assert.deepEqual(fn(input),output); - }); -}; - -// tests a function that takes in a node and returns a boolean value -TestUtil.testBooleanFunction = function (simplifier, exprString, expectedBooleanValue) { - it(exprString + ' ' + expectedBooleanValue, () => { - const inputNode = flatten(math.parse(exprString)); - assert.equal(simplifier(inputNode),expectedBooleanValue); - }); -}; - -// Tests a simplification function -TestUtil.testSimplification = function (simplifyingFunction, exprString, - expectedOutputString) { - it (exprString + ' -> ' + expectedOutputString, () => { - assert.deepEqual( - print.ascii(simplifyingFunction(flatten(math.parse(exprString))).newNode), - expectedOutputString); - }); -}; - -// Test the substeps in the expression -TestUtil.testSubsteps = function (fn, exprString, outputList, - outputStr) { - it(exprString + ' -> ' + outputStr, () => { - const status = fn(flatten(math.parse(exprString))); - const substeps = status.substeps; - - assert.deepEqual(substeps.length, outputList.length); - substeps.forEach((step, i) => { - assert.deepEqual( - print.ascii(step.newNode), - outputList[i]); - }); - if (outputStr) { - assert.deepEqual( - print.ascii(status.newNode), - outputStr); - } - }); -}; - -// Remove some property used in mathjs that we don't need and prevents node -// equality checks from passing -TestUtil.removeComments = function(node) { - node.filter(node => node.comment !== undefined).forEach( - node => delete node.comment); -}; - -module.exports = TestUtil; diff --git a/test/TestUtil.ts b/test/TestUtil.ts new file mode 100644 index 00000000..277a031b --- /dev/null +++ b/test/TestUtil.ts @@ -0,0 +1,68 @@ +import * as math from "mathjs"; + +import { flattenOperands } from "../lib/src/util/flattenOperands"; +import { printAscii } from "../lib/src/util/print"; +import assert = require("assert"); + +// TestUtil contains helper methods to share code across tests +export class TestUtil { + // Takes in an input string and returns a flattened and parsed node + static parseAndFlatten(exprString) { + return flattenOperands(math.parse(exprString)); + } + + // Tests a function that takes an input string and check its output + static testFunctionOutput(fn, input, output) { + it(input + " -> " + output, () => { + assert.deepEqual(fn(input), output); + }); + } + + // tests a function that takes in a node and returns a boolean value + static testBooleanFunction(simplifier, exprString, expectedBooleanValue) { + it(exprString + " " + expectedBooleanValue, () => { + const inputNode = flattenOperands(math.parse(exprString)); + assert.equal(simplifier(inputNode), expectedBooleanValue); + }); + } + + // Tests a simplification function + static testSimplification( + simplifyingFunction, + exprString, + expectedOutputString + ) { + it(exprString + " -> " + expectedOutputString, () => { + assert.deepEqual( + printAscii( + simplifyingFunction(flattenOperands(math.parse(exprString))).newNode + ), + expectedOutputString + ); + }); + } + + // Test the substeps in the expression + static testSubsteps(fn, exprString, outputList, outputStr) { + it(exprString + " -> " + outputStr, () => { + const status = fn(flattenOperands(math.parse(exprString))); + const substeps = status.substeps; + + assert.deepEqual(substeps.length, outputList.length); + substeps.forEach((step, i) => { + assert.deepEqual(printAscii(step.newNode), outputList[i]); + }); + if (outputStr) { + assert.deepEqual(printAscii(status.newNode), outputStr); + } + }); + } + + // Remove some property used in mathjs that we don't need and prevents node + // equality from passing + static removeComments(node) { + node + .filter((node) => node.comment !== undefined) + .forEach((node) => delete node.comment); + } +} diff --git a/test/canAddLikeTermPolynomialNodes.test.js b/test/canAddLikeTermPolynomialNodes.test.js deleted file mode 100644 index 410efa48..00000000 --- a/test/canAddLikeTermPolynomialNodes.test.js +++ /dev/null @@ -1,17 +0,0 @@ -const canAddLikeTerms = require('../lib/checks/canAddLikeTerms'); - -const TestUtil = require('./TestUtil'); - -function testCanBeAdded(expr, addable) { - TestUtil.testBooleanFunction( - canAddLikeTerms.canAddLikeTermPolynomialNodes, expr, addable); -} - -describe('can add like term polynomials', () => { - const tests = [ - ['x^2 + x^2', true], - ['x + x', true], - ['x^3 + x', false], - ]; - tests.forEach(t => testCanBeAdded(t[0], t[1])); -}); diff --git a/test/canAddLikeTermPolynomialNodes.test.ts b/test/canAddLikeTermPolynomialNodes.test.ts new file mode 100644 index 00000000..b53c51a8 --- /dev/null +++ b/test/canAddLikeTermPolynomialNodes.test.ts @@ -0,0 +1,15 @@ +import { TestUtil } from "./TestUtil"; +import { canAddLikeTermPolynomialNodes } from "../lib/src/checks/canAddLikeTerms"; + +function testCanBeAdded(expr, addable) { + TestUtil.testBooleanFunction(canAddLikeTermPolynomialNodes, expr, addable); +} + +describe("can add like term polynomials", () => { + const tests = [ + ["x^2 + x^2", true], + ["x + x", true], + ["x^3 + x", false], + ]; + tests.forEach((t) => testCanBeAdded(t[0], t[1])); +}); diff --git a/test/canMultiplyLikeTermConstantNodes.test.js b/test/canMultiplyLikeTermConstantNodes.test.js deleted file mode 100644 index 4b9963cc..00000000 --- a/test/canMultiplyLikeTermConstantNodes.test.js +++ /dev/null @@ -1,17 +0,0 @@ -const canMultiplyLikeTermConstantNodes = require('../lib/checks/canMultiplyLikeTermConstantNodes'); - -const TestUtil = require('./TestUtil'); - -function testCanBeMultipliedConstants(expr, multipliable) { - TestUtil.testBooleanFunction(canMultiplyLikeTermConstantNodes, expr, multipliable); -} - -describe('can multiply like term constants', () => { - const tests = [ - ['3^2 * 3^5', true], - ['2^3 * 3^2', false], - ['10^3 * 10^2', true], - ['10^2 * 10 * 10^4', true] - ]; - tests.forEach(t => testCanBeMultipliedConstants(t[0], t[1])); -}); diff --git a/test/canMultiplyLikeTermConstantNodes.test.ts b/test/canMultiplyLikeTermConstantNodes.test.ts new file mode 100644 index 00000000..181b6b99 --- /dev/null +++ b/test/canMultiplyLikeTermConstantNodes.test.ts @@ -0,0 +1,20 @@ +import { TestUtil } from "./TestUtil"; +import { canMultiplyLikeTermConstantNodes } from "../lib/src/checks/canMultiplyLikeTermConstantNodes"; + +function testCanBeMultipliedConstants(expr, multipliable) { + TestUtil.testBooleanFunction( + canMultiplyLikeTermConstantNodes, + expr, + multipliable + ); +} + +describe("can multiply like term constants", () => { + const tests = [ + ["3^2 * 3^5", true], + ["2^3 * 3^2", false], + ["10^3 * 10^2", true], + ["10^2 * 10 * 10^4", true], + ]; + tests.forEach((t) => testCanBeMultipliedConstants(t[0], t[1])); +}); diff --git a/test/canMultiplyLikeTermPolynomialNodes.test.js b/test/canMultiplyLikeTermPolynomialNodes.test.js deleted file mode 100644 index 0b2037c2..00000000 --- a/test/canMultiplyLikeTermPolynomialNodes.test.js +++ /dev/null @@ -1,17 +0,0 @@ -const canMultiplyLikeTermPolynomialNodes = require('../lib/checks/canMultiplyLikeTermPolynomialNodes'); - -const TestUtil = require('./TestUtil'); - -function testCanBeMultiplied(expr, multipliable) { - TestUtil.testBooleanFunction(canMultiplyLikeTermPolynomialNodes, expr, multipliable); -} - -describe('can multiply like term polynomials', () => { - const tests = [ - ['x^2 * x * x', true], - ['x^2 * 3x * x', false], - ['y * y^3', true], - ['x^3 * x^2', true] - ]; - tests.forEach(t => testCanBeMultiplied(t[0], t[1])); -}); diff --git a/test/canMultiplyLikeTermPolynomialNodes.test.ts b/test/canMultiplyLikeTermPolynomialNodes.test.ts new file mode 100644 index 00000000..8bb985f0 --- /dev/null +++ b/test/canMultiplyLikeTermPolynomialNodes.test.ts @@ -0,0 +1,20 @@ +import { TestUtil } from "./TestUtil"; +import { canMultiplyLikeTermPolynomialNodes } from "../lib/src/checks/canMultiplyLikeTermPolynomialNodes"; + +function testCanBeMultiplied(expr, multipliable) { + TestUtil.testBooleanFunction( + canMultiplyLikeTermPolynomialNodes, + expr, + multipliable + ); +} + +describe("can multiply like term polynomials", () => { + const tests = [ + ["x^2 * x * x", true], + ["x^2 * 3x * x", false], + ["y * y^3", true], + ["x^3 * x^2", true], + ]; + tests.forEach((t) => testCanBeMultiplied(t[0], t[1])); +}); diff --git a/test/canRearrangeCoefficient.test.js b/test/canRearrangeCoefficient.test.js deleted file mode 100644 index 09d7f1e1..00000000 --- a/test/canRearrangeCoefficient.test.js +++ /dev/null @@ -1,15 +0,0 @@ -const canRearrangeCoefficient = require('../lib/checks/canRearrangeCoefficient'); - -const TestUtil = require('./TestUtil'); - -function testCanBeRearranged(expr, arrangeable) { - TestUtil.testBooleanFunction(canRearrangeCoefficient, expr, arrangeable); -} - -describe('can rearrange coefficient', () => { - const tests = [ - ['x*2', true], - ['y^3 * 7', true] - ]; - tests.forEach(t => testCanBeRearranged(t[0], t[1])); -}); diff --git a/test/canRearrangeCoefficient.test.ts b/test/canRearrangeCoefficient.test.ts new file mode 100644 index 00000000..ab8b17f4 --- /dev/null +++ b/test/canRearrangeCoefficient.test.ts @@ -0,0 +1,14 @@ +import { TestUtil } from "./TestUtil"; +import { canRearrangeCoefficient } from "../lib/src/checks/canRearrangeCoefficient"; + +function testCanBeRearranged(expr, arrangeable) { + TestUtil.testBooleanFunction(canRearrangeCoefficient, expr, arrangeable); +} + +describe("can rearrange coefficient", () => { + const tests = [ + ["x*2", true], + ["y^3 * 7", true], + ]; + tests.forEach((t) => testCanBeRearranged(t[0], t[1])); +}); diff --git a/test/checks/checks.test.js b/test/checks/checks.test.js deleted file mode 100644 index 4df3005f..00000000 --- a/test/checks/checks.test.js +++ /dev/null @@ -1,31 +0,0 @@ -const checks = require('../../lib/checks'); - -const TestUtil = require('../TestUtil'); - -function testCanCombine(exprStr, canCombine) { - TestUtil.testBooleanFunction(checks.canSimplifyPolynomialTerms, exprStr, canCombine); -} - -describe('canSimplifyPolynomialTerms multiplication', function() { - const tests = [ - ['x^2 * x * x', true], - // false b/c coefficient - ['x^2 * 3x * x', false], - ['y * y^3', true], - ['5 * y^3', false], // just needs flattening - ['5/7 * x', false], // just needs flattening - ['5/7 * 9 * x', false], - ]; - tests.forEach(t => testCanCombine(t[0], t[1])); -}); - - -describe('canSimplifyPolynomialTerms addition', function() { - const tests = [ - ['x + x', true], - ['4y^2 + 7y^2 + y^2', true], - ['4y^2 + 7y^2 + y^2 + y', false], - ['y', false], - ]; - tests.forEach(t => testCanCombine(t[0], t[1])); -}); diff --git a/test/checks/checks.test.ts b/test/checks/checks.test.ts new file mode 100644 index 00000000..27e57a10 --- /dev/null +++ b/test/checks/checks.test.ts @@ -0,0 +1,29 @@ +import { TestUtil } from "../TestUtil"; +import { canSimplifyPolynomialTerms } from "../../lib/src/checks/canSimplifyPolynomialTerms"; + +function testCanCombine(exprStr, canCombine) { + TestUtil.testBooleanFunction(canSimplifyPolynomialTerms, exprStr, canCombine); +} + +describe("canSimplifyPolynomialTerms multiplication", function () { + const tests = [ + ["x^2 * x * x", true], + // false b/c coefficient + ["x^2 * 3x * x", false], + ["y * y^3", true], + ["5 * y^3", false], // just needs flattening + ["5/7 * x", false], // just needs flattening + ["5/7 * 9 * x", false], + ]; + tests.forEach((t) => testCanCombine(t[0], t[1])); +}); + +describe("canSimplifyPolynomialTerms addition", function () { + const tests = [ + ["x + x", true], + ["4y^2 + 7y^2 + y^2", true], + ["4y^2 + 7y^2 + y^2 + y", false], + ["y", false], + ]; + tests.forEach((t) => testCanCombine(t[0], t[1])); +}); diff --git a/test/checks/hasUnsupportedNodes.test.js b/test/checks/hasUnsupportedNodes.test.js deleted file mode 100644 index 7b71a5f5..00000000 --- a/test/checks/hasUnsupportedNodes.test.js +++ /dev/null @@ -1,30 +0,0 @@ -const assert = require('assert'); -const math = require('mathjs'); - -const checks = require('../../lib/checks'); - -describe('arithmetic stepping', function () { - it('4 + sqrt(16) no support for sqrt', function () { - assert.deepEqual( - checks.hasUnsupportedNodes(math.parse('4 + sqrt(4)')), - true); - }); - - it('x = 5 no support for assignment', function () { - assert.deepEqual( - checks.hasUnsupportedNodes(math.parse('x = 5')), - true); - }); - - it('x + (-5)^2 - 8*y/2 is fine', function () { - assert.deepEqual( - checks.hasUnsupportedNodes(math.parse('x + (-5)^2 - 8*y/2')), - false); - }); - - it('nthRoot() with no args has no support', function () { - assert.deepEqual( - checks.hasUnsupportedNodes(math.parse('nthRoot()')), - true); - }); -}); diff --git a/test/checks/hasUnsupportedNodes.test.ts b/test/checks/hasUnsupportedNodes.test.ts new file mode 100644 index 00000000..b9eedf11 --- /dev/null +++ b/test/checks/hasUnsupportedNodes.test.ts @@ -0,0 +1,24 @@ +import * as math from "mathjs"; +import { hasUnsupportedNodes } from "../../lib/src/checks/hasUnsupportedNodes"; +import assert = require("assert"); + +describe("arithmetic stepping", function () { + it("4 + sqrt(16) no support for sqrt", function () { + assert.deepEqual(hasUnsupportedNodes(math.parse("4 + sqrt(4)")), true); + }); + + it("x = 5 no support for assignment", function () { + assert.deepEqual(hasUnsupportedNodes(math.parse("x = 5")), true); + }); + + it("x + (-5)^2 - 8*y/2 is fine", function () { + assert.deepEqual( + hasUnsupportedNodes(math.parse("x + (-5)^2 - 8*y/2")), + false + ); + }); + + it("nthRoot() with no args has no support", function () { + assert.deepEqual(hasUnsupportedNodes(math.parse("nthRoot()")), true); + }); +}); diff --git a/test/checks/isQuadratic.test.js b/test/checks/isQuadratic.test.js deleted file mode 100644 index d5c98d01..00000000 --- a/test/checks/isQuadratic.test.js +++ /dev/null @@ -1,28 +0,0 @@ -const checks = require('../../lib/checks'); -const TestUtil = require('../TestUtil'); - -function testIsQuadratic(input, output) { - TestUtil.testBooleanFunction(checks.isQuadratic, input, output); -} - -describe('isQuadratic', function () { - const tests = [ - ['2 + 2', false], - ['x', false], - ['x^2 - 4', true], - ['x^2 + 2x + 1', true], - ['x^2 - 2x + 1', true], - ['x^2 + 3x + 2', true], - ['x^2 - 3x + 2', true], - ['x^2 + x - 2', true], - ['x^2 + x', true], - ['x^2 + 4', true], - ['x^2 + 4x + 1', true], - ['x^2', false], - ['x^3 + x^2 + x + 1', false], - ['x^2 + 4 + 2^x', false], - ['x^2 + 4 + 2y', false], - ['y^2 + 4 + 2x', false], - ]; - tests.forEach(t => testIsQuadratic(t[0], t[1])); -}); diff --git a/test/checks/isQuadratic.test.ts b/test/checks/isQuadratic.test.ts new file mode 100644 index 00000000..855a2ada --- /dev/null +++ b/test/checks/isQuadratic.test.ts @@ -0,0 +1,28 @@ +import { TestUtil } from "../TestUtil"; +import { isQuadratic } from "../../lib/src/checks/isQuadratic"; + +function testIsQuadratic(input, output) { + TestUtil.testBooleanFunction(isQuadratic, input, output); +} + +describe("isQuadratic", function () { + const tests = [ + ["2 + 2", false], + ["x", false], + ["x^2 - 4", true], + ["x^2 + 2x + 1", true], + ["x^2 - 2x + 1", true], + ["x^2 + 3x + 2", true], + ["x^2 - 3x + 2", true], + ["x^2 + x - 2", true], + ["x^2 + x", true], + ["x^2 + 4", true], + ["x^2 + 4x + 1", true], + ["x^2", false], + ["x^3 + x^2 + x + 1", false], + ["x^2 + 4 + 2^x", false], + ["x^2 + 4 + 2y", false], + ["y^2 + 4 + 2x", false], + ]; + tests.forEach((t) => testIsQuadratic(t[0], t[1])); +}); diff --git a/test/checks/resolvesToConstant.test.js b/test/checks/resolvesToConstant.test.js deleted file mode 100644 index 8d08d296..00000000 --- a/test/checks/resolvesToConstant.test.js +++ /dev/null @@ -1,18 +0,0 @@ -const checks = require('../../lib/checks'); - -const TestUtil = require('../TestUtil'); - -function testResolvesToConstant(exprString, resolves) { - TestUtil.testBooleanFunction(checks.resolvesToConstant, exprString, resolves); -} - -describe('resolvesToConstant', function () { - const tests = [ - ['(2+2)', true], - ['10', true], - ['((2^2 + 4)) * 7 / 8', true], - ['2 * 3^x', false], - ['-(2) * -3', true], - ]; - tests.forEach(t => testResolvesToConstant(t[0], t[1])); -}); diff --git a/test/checks/resolvesToConstant.test.ts b/test/checks/resolvesToConstant.test.ts new file mode 100644 index 00000000..94d40eed --- /dev/null +++ b/test/checks/resolvesToConstant.test.ts @@ -0,0 +1,17 @@ +import { TestUtil } from "../TestUtil"; +import { resolvesToConstant } from "../../lib/src/checks/resolvesToConstant"; + +function testResolvesToConstant(exprString, resolves) { + TestUtil.testBooleanFunction(resolvesToConstant, exprString, resolves); +} + +describe("resolvesToConstant", function () { + const tests = [ + ["(2+2)", true], + ["10", true], + ["((2^2 + 4)) * 7 / 8", true], + ["2 * 3^x", false], + ["-(2) * -3", true], + ]; + tests.forEach((t) => testResolvesToConstant(t[0], t[1])); +}); diff --git a/test/equation.test.js b/test/equation.test.js deleted file mode 100644 index 6dc2b54a..00000000 --- a/test/equation.test.js +++ /dev/null @@ -1,54 +0,0 @@ -const assert = require('assert'); -const math = require('mathjs'); - -const TestUtil = require('./TestUtil'); - -const Equation = require('../lib/equation/Equation'); - -function constructAndPrintEquation(left, right, comp) { - const leftNode = math.parse(left); - const rightNode = math.parse(right); - const equation = new Equation(leftNode, rightNode, comp); - return equation.ascii(); -} - -function constructAndPrintLatexEquation(left, right, comp) { - const rightNode = TestUtil.parseAndFlatten(right); - const leftNode = TestUtil.parseAndFlatten(left); - const equation = new Equation(leftNode, rightNode, comp); - return equation.latex(); -} - -function testLatexprint(left, right, comp, output) { - it (output, () => { - assert.equal( - constructAndPrintLatexEquation(left, right, comp), output - ); - }); -} - -function testEquationConstructor(left, right, comp, output) { - it (output, () => { - assert.equal( - constructAndPrintEquation(left, right, comp), output - ); - }); -} - -describe('Equation constructor', () => { - const tests = [ - ['2*x^2 + x', '4', '=', '2x^2 + x = 4'], - ['x^2 + 2*x + 2', '0', '>=', 'x^2 + 2x + 2 >= 0'], - ['2*x - 1', '0', '<=', '2x - 1 <= 0'] - ]; - tests.forEach(t => testEquationConstructor(t[0], t[1], t[2], t[3])); -}); - -describe('Latex printer', () => { - const tests = [ - ['2*x^2 + x', '4', '=', '2~{ x}^{2}+ x = 4'], - ['x^2 + 2*y + 2', '0', '>=', '{ x}^{2}+2~ y+2 >= 0'], - ['2x - 1', '0', '<=', '2~ x - 1 <= 0'] - ]; - tests.forEach(t => testLatexprint(t[0], t[1], t[2], t[3])); -}); diff --git a/test/equation.test.ts b/test/equation.test.ts new file mode 100644 index 00000000..c1f95e94 --- /dev/null +++ b/test/equation.test.ts @@ -0,0 +1,50 @@ +import * as math from "mathjs"; + +import { TestUtil } from "./TestUtil"; + +import { Equation } from "../lib/src/equation/Equation"; +import assert = require("assert"); + +function constructAndPrintEquation(left, right, comp) { + const leftNode = math.parse(left); + const rightNode = math.parse(right); + const equation = new Equation(leftNode, rightNode, comp); + return equation.ascii(); +} + +function constructAndPrintLatexEquation(left, right, comp) { + const rightNode = TestUtil.parseAndFlatten(right); + const leftNode = TestUtil.parseAndFlatten(left); + const equation = new Equation(leftNode, rightNode, comp); + return equation.latex(); +} + +function testLatexprint(left, right, comp, output) { + it(output, () => { + assert.equal(constructAndPrintLatexEquation(left, right, comp), output); + }); +} + +function testEquationConstructor(left, right, comp, output) { + it(output, () => { + assert.equal(constructAndPrintEquation(left, right, comp), output); + }); +} + +describe("Equation constructor", () => { + const tests = [ + ["2*x^2 + x", "4", "=", "2x^2 + x = 4"], + ["x^2 + 2*x + 2", "0", ">=", "x^2 + 2x + 2 >= 0"], + ["2*x - 1", "0", "<=", "2x - 1 <= 0"], + ]; + tests.forEach((t) => testEquationConstructor(t[0], t[1], t[2], t[3])); +}); + +describe("Latex printer", () => { + const tests = [ + ["2*x^2 + x", "4", "=", "2~{ x}^{2}+ x = 4"], + ["x^2 + 2*y + 2", "0", ">=", "{ x}^{2}+2~ y+2 >= 0"], + ["2x - 1", "0", "<=", "2~ x - 1 <= 0"], + ]; + tests.forEach((t) => testLatexprint(t[0], t[1], t[2], t[3])); +}); diff --git a/test/factor/ConstantFactors.test.js b/test/factor/ConstantFactors.test.js deleted file mode 100644 index 9fb102fa..00000000 --- a/test/factor/ConstantFactors.test.js +++ /dev/null @@ -1,44 +0,0 @@ -const ConstantFactors = require('../../lib/factor/ConstantFactors'); - -const TestUtil = require('../TestUtil'); - -function testPrimeFactors(input, output) { - TestUtil.testFunctionOutput(ConstantFactors.getPrimeFactors, input, output); -} - -describe('prime factors', function() { - const tests = [ - [1, [1]], - [-1, [-1, 1]], - [-2, [-1, 2]], - [5, [5]], - [12, [2, 2, 3]], - [15, [3, 5]], - [36, [2, 2, 3, 3]], - [49, [7, 7]], - [1260, [2, 2, 3, 3, 5, 7]], - [13195, [5, 7, 13, 29]], - [1234567891, [1234567891]] - ]; - tests.forEach(t => testPrimeFactors(t[0], t[1])); -}); - -function testFactorPairs(input, output) { - TestUtil.testFunctionOutput(ConstantFactors.getFactorPairs, input, output); -} - -describe('factor pairs', function() { - const tests = [ - [1, [[-1, -1], [1, 1]]], - [5, [[-1, -5], [1, 5]]], - [12, [[-3, -4], [-2, -6], [-1, -12], [1, 12], [2, 6], [3, 4]]], - [-12, [[-3, 4], [-2, 6], [-1, 12], [1, -12], [2, -6], [3, -4]]], - [15, [[-3, -5], [-1, -15], [1, 15], [3, 5]]], - [36, [[-6, -6], [-4, -9], [-3, -12], [-2, -18], [-1, -36], [1, 36], [2, 18], [3, 12], [4, 9], [6, 6,]]], - [49, [[-7, -7], [-1, -49], [1, 49], [7, 7]]], - [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]]], - [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]]], - [1234567891, [[-1, -1234567891], [1, 1234567891]]] - ]; - tests.forEach(t => testFactorPairs(t[0], t[1])); -}); diff --git a/test/factor/ConstantFactors.test.ts b/test/factor/ConstantFactors.test.ts new file mode 100644 index 00000000..c4243696 --- /dev/null +++ b/test/factor/ConstantFactors.test.ts @@ -0,0 +1,172 @@ +import { ConstantFactors } from "../../lib/src/factor/ConstantFactors"; + +import { TestUtil } from "../TestUtil"; + +function testPrimeFactors(input, output) { + TestUtil.testFunctionOutput(ConstantFactors.getPrimeFactors, input, output); +} + +describe("prime factors", function () { + const tests = [ + [1, [1]], + [-1, [-1, 1]], + [-2, [-1, 2]], + [5, [5]], + [12, [2, 2, 3]], + [15, [3, 5]], + [36, [2, 2, 3, 3]], + [49, [7, 7]], + [1260, [2, 2, 3, 3, 5, 7]], + [13195, [5, 7, 13, 29]], + [1234567891, [1234567891]], + ]; + tests.forEach((t) => testPrimeFactors(t[0], t[1])); +}); + +function testFactorPairs(input, output) { + TestUtil.testFunctionOutput(ConstantFactors.getFactorPairs, input, output); +} + +describe("factor pairs", function () { + const tests = [ + [ + 1, + [ + [-1, -1], + [1, 1], + ], + ], + [ + 5, + [ + [-1, -5], + [1, 5], + ], + ], + [ + 12, + [ + [-3, -4], + [-2, -6], + [-1, -12], + [1, 12], + [2, 6], + [3, 4], + ], + ], + [ + -12, + [ + [-3, 4], + [-2, 6], + [-1, 12], + [1, -12], + [2, -6], + [3, -4], + ], + ], + [ + 15, + [ + [-3, -5], + [-1, -15], + [1, 15], + [3, 5], + ], + ], + [ + 36, + [ + [-6, -6], + [-4, -9], + [-3, -12], + [-2, -18], + [-1, -36], + [1, 36], + [2, 18], + [3, 12], + [4, 9], + [6, 6], + ], + ], + [ + 49, + [ + [-7, -7], + [-1, -49], + [1, 49], + [7, 7], + ], + ], + [ + 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], + ], + ], + [ + 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], + ], + ], + [ + 1234567891, + [ + [-1, -1234567891], + [1, 1234567891], + ], + ], + ]; + tests.forEach((t) => testFactorPairs(t[0], t[1])); +}); diff --git a/test/factor/factor.test.js b/test/factor/factor.test.js deleted file mode 100644 index 9be72d95..00000000 --- a/test/factor/factor.test.js +++ /dev/null @@ -1,36 +0,0 @@ -const assert = require('assert'); -const factor = require('../../lib/factor'); -const print = require('../../lib/util/print'); - -const NO_STEPS = 'no-steps'; - -function testFactor(expressionString, outputStr, debug=false) { - const steps = factor(expressionString, debug); - let lastStep; - if (steps.length === 0) { - lastStep = NO_STEPS; - } - else { - lastStep = print.ascii(steps[steps.length -1].newNode); - } - it(expressionString + ' -> ' + outputStr, (done) => { - assert.equal(lastStep, outputStr); - done(); - }); -} - -describe('factor expressions', function () { - const tests = [ - ['x^2', NO_STEPS], - ['x^2 + 2x', 'x * (x + 2)'], - ['x^2 - 4', '(x + 2) * (x - 2)'], - ['x^2 + 4', NO_STEPS], - ['x^2 + 2x + 1', '(x + 1)^2'], - ['x^2 + 3x + 2', '(x + 1) * (x + 2)'], - ['x^3 + x^2 + x + 1', NO_STEPS], - ['1 + 2', NO_STEPS], - ['x + 2', NO_STEPS], - ]; - tests.forEach(t => testFactor(t[0], t[1], t[2])); -}); - diff --git a/test/factor/factor.test.ts b/test/factor/factor.test.ts new file mode 100644 index 00000000..d7c1704a --- /dev/null +++ b/test/factor/factor.test.ts @@ -0,0 +1,34 @@ +import { printAscii } from "../../lib/src/util/print"; +import assert = require("assert"); +import { factorString } from "../../lib/src/factor/FactorString"; + +const NO_STEPS = "no-steps"; + +function testFactor(expressionString, outputStr, debug = false) { + const steps = factorString(expressionString, debug); + let lastStep; + if (steps.length === 0) { + lastStep = NO_STEPS; + } else { + lastStep = printAscii(steps[steps.length - 1].newNode); + } + it(expressionString + " -> " + outputStr, (done) => { + assert.equal(lastStep, outputStr); + done(); + }); +} + +describe("factor expressions", function () { + const tests = [ + ["x^2", NO_STEPS], + ["x^2 + 2x", "x * (x + 2)"], + ["x^2 - 4", "(x + 2) * (x - 2)"], + ["x^2 + 4", NO_STEPS], + ["x^2 + 2x + 1", "(x + 1)^2"], + ["x^2 + 3x + 2", "(x + 1) * (x + 2)"], + ["x^3 + x^2 + x + 1", NO_STEPS], + ["1 + 2", NO_STEPS], + ["x + 2", NO_STEPS], + ]; + tests.forEach((t) => testFactor(t[0], t[1])); +}); diff --git a/test/factor/factorQuadratic.test.js b/test/factor/factorQuadratic.test.js deleted file mode 100644 index 0247d824..00000000 --- a/test/factor/factorQuadratic.test.js +++ /dev/null @@ -1,110 +0,0 @@ -const factorQuadratic = require('../../lib/factor/factorQuadratic'); -const TestUtil = require('../TestUtil'); - -function testFactorQuadratic(input, output) { - TestUtil.testSimplification(factorQuadratic, input, output); -} - -describe('factorQuadratic', function () { - const tests = [ - // no change - ['x^2', 'x^2'], - ['x^2 + x^2', 'x^2 + x^2'], - ['x^2 + 2 - 3', 'x^2 + 2 - 3'], - ['x^2 + 2y + 2x + 3', 'x^2 + 2y + 2x + 3'], - ['x^2 + 4', 'x^2 + 4'], - ['x^2 + 4 + 2^x', 'x^2 + 4 + 2^x'], - ['-x^2 - 1', '-x^2 - 1'], - // factor symbol - ['x^2 + 2x', 'x * (x + 2)'], - ['-x^2 - 2x', '-x * (x + 2)'], - ['x^2 - 3x', 'x * (x - 3)'], - ['2x^2 + 4x', '2x * (x + 2)'], - // difference of squares - ['x^2 - 4', '(x + 2) * (x - 2)'], - ['-x^2 + 1', '-(x + 1) * (x - 1)'], - ['4x^2 - 9', '(2x + 3) * (2x - 3)'], - ['4x^2 - 16', '4 * (x + 2) * (x - 2)'], - ['-4x^2 + 16', '-4 * (x + 2) * (x - 2)'], - // perfect square - ['x^2 + 2x + 1', '(x + 1)^2'], - ['x^2 - 2x + 1', '(x - 1)^2'], - ['-x^2 - 2x - 1', '-(x + 1)^2'], - ['4x^2 + 4x + 1', '(2x + 1)^2'], - ['12x^2 + 12x + 3', '3 * (2x + 1)^2'], - // sum product rule - ['x^2 + 3x + 2', '(x + 1) * (x + 2)'], - ['x^2 - 3x + 2', '(x - 1) * (x - 2)'], - ['x^2 + x - 2', '(x - 1) * (x + 2)'], - ['-x^2 - 3x - 2', '-(x + 1) * (x + 2)'], - ['2x^2 + 5x + 3','(x + 1) * (2x + 3)'], - ['2x^2 - 5x - 3','(2x + 1) * (x - 3)'], - ['2x^2 - 5x + 3','(x - 1) * (2x - 3)'], - // TODO: quadratic equation - ['x^2 + 4x + 1', 'x^2 + 4x + 1'], - ['x^2 - 3x + 1', 'x^2 - 3x + 1'], - ]; - tests.forEach(t => testFactorQuadratic(t[0], t[1])); -}); - -function testFactorSumProductRuleSubsteps(exprString, outputList) { - const lastString = outputList[outputList.length - 1]; - TestUtil.testSubsteps(factorQuadratic, exprString, outputList, lastString); -} - -describe('factorSumProductRule', function() { - const tests = [ - // sum product rule - ['x^2 + 3x + 2', - ['x^2 + x + 2x + 2', - '(x^2 + x) + (2x + 2)', - 'x * (x + 1) + (2x + 2)', - 'x * (x + 1) + 2 * (x + 1)', - '(x + 1) * (x + 2)'] - ], - ['x^2 - 3x + 2', - ['x^2 - x - 2x + 2', - '(x^2 - x) + (-2x + 2)', - 'x * (x - 1) + (-2x + 2)', - 'x * (x - 1) - 2 * (x - 1)', - '(x - 1) * (x - 2)'] - ], - ['x^2 + x - 2', - ['x^2 - x + 2x - 2', - '(x^2 - x) + (2x - 2)', - 'x * (x - 1) + (2x - 2)', - 'x * (x - 1) + 2 * (x - 1)', - '(x - 1) * (x + 2)'] - ], - ['-x^2 - 3x - 2', - ['-(x^2 + 3x + 2)', - '-(x^2 + x + 2x + 2)', - '-((x^2 + x) + (2x + 2))', - '-(x * (x + 1) + (2x + 2))', - '-(x * (x + 1) + 2 * (x + 1))', - '-(x + 1) * (x + 2)'] - ], - ['2x^2 + 5x + 3', - ['2x^2 + 2x + 3x + 3', - '(2x^2 + 2x) + (3x + 3)', - '2x * (x + 1) + (3x + 3)', - '2x * (x + 1) + 3 * (x + 1)', - '(x + 1) * (2x + 3)'] - ], - ['2x^2 - 5x - 3', - ['2x^2 + x - 6x - 3', - '(2x^2 + x) + (-6x - 3)', - 'x * (2x + 1) + (-6x - 3)', - 'x * (2x + 1) - 3 * (2x + 1)', - '(2x + 1) * (x - 3)'] - ], - ['2x^2 - 5x + 3', - ['2x^2 - 2x - 3x + 3', - '(2x^2 - 2x) + (-3x + 3)', - '2x * (x - 1) + (-3x + 3)', - '2x * (x - 1) - 3 * (x - 1)', - '(x - 1) * (2x - 3)'] - ], - ]; - tests.forEach(t => testFactorSumProductRuleSubsteps(t[0], t[1])); -}); diff --git a/test/factor/factorQuadratic.test.ts b/test/factor/factorQuadratic.test.ts new file mode 100644 index 00000000..3a336f04 --- /dev/null +++ b/test/factor/factorQuadratic.test.ts @@ -0,0 +1,131 @@ +import { factorQuadratic } from "../../lib/src/factor/factorQuadratic"; +import { TestUtil } from "../TestUtil"; + +function testFactorQuadratic(input, output) { + TestUtil.testSimplification(factorQuadratic, input, output); +} + +describe("factorQuadratic", function () { + const tests = [ + // no change + ["x^2", "x^2"], + ["x^2 + x^2", "x^2 + x^2"], + ["x^2 + 2 - 3", "x^2 + 2 - 3"], + ["x^2 + 2y + 2x + 3", "x^2 + 2y + 2x + 3"], + ["x^2 + 4", "x^2 + 4"], + ["x^2 + 4 + 2^x", "x^2 + 4 + 2^x"], + ["-x^2 - 1", "-x^2 - 1"], + // factor symbol + ["x^2 + 2x", "x * (x + 2)"], + ["-x^2 - 2x", "-x * (x + 2)"], + ["x^2 - 3x", "x * (x - 3)"], + ["2x^2 + 4x", "2x * (x + 2)"], + // difference of squares + ["x^2 - 4", "(x + 2) * (x - 2)"], + ["-x^2 + 1", "-(x + 1) * (x - 1)"], + ["4x^2 - 9", "(2x + 3) * (2x - 3)"], + ["4x^2 - 16", "4 * (x + 2) * (x - 2)"], + ["-4x^2 + 16", "-4 * (x + 2) * (x - 2)"], + // perfect square + ["x^2 + 2x + 1", "(x + 1)^2"], + ["x^2 - 2x + 1", "(x - 1)^2"], + ["-x^2 - 2x - 1", "-(x + 1)^2"], + ["4x^2 + 4x + 1", "(2x + 1)^2"], + ["12x^2 + 12x + 3", "3 * (2x + 1)^2"], + // sum product rule + ["x^2 + 3x + 2", "(x + 1) * (x + 2)"], + ["x^2 - 3x + 2", "(x - 1) * (x - 2)"], + ["x^2 + x - 2", "(x - 1) * (x + 2)"], + ["-x^2 - 3x - 2", "-(x + 1) * (x + 2)"], + ["2x^2 + 5x + 3", "(x + 1) * (2x + 3)"], + ["2x^2 - 5x - 3", "(2x + 1) * (x - 3)"], + ["2x^2 - 5x + 3", "(x - 1) * (2x - 3)"], + // TODO: quadratic equation + ["x^2 + 4x + 1", "x^2 + 4x + 1"], + ["x^2 - 3x + 1", "x^2 - 3x + 1"], + ]; + tests.forEach((t) => testFactorQuadratic(t[0], t[1])); +}); + +function testFactorSumProductRuleSubsteps(exprString, outputList) { + const lastString = outputList[outputList.length - 1]; + TestUtil.testSubsteps(factorQuadratic, exprString, outputList, lastString); +} + +describe("factorSumProductRule", function () { + const tests = [ + // sum product rule + [ + "x^2 + 3x + 2", + [ + "x^2 + x + 2x + 2", + "(x^2 + x) + (2x + 2)", + "x * (x + 1) + (2x + 2)", + "x * (x + 1) + 2 * (x + 1)", + "(x + 1) * (x + 2)", + ], + ], + [ + "x^2 - 3x + 2", + [ + "x^2 - x - 2x + 2", + "(x^2 - x) + (-2x + 2)", + "x * (x - 1) + (-2x + 2)", + "x * (x - 1) - 2 * (x - 1)", + "(x - 1) * (x - 2)", + ], + ], + [ + "x^2 + x - 2", + [ + "x^2 - x + 2x - 2", + "(x^2 - x) + (2x - 2)", + "x * (x - 1) + (2x - 2)", + "x * (x - 1) + 2 * (x - 1)", + "(x - 1) * (x + 2)", + ], + ], + [ + "-x^2 - 3x - 2", + [ + "-(x^2 + 3x + 2)", + "-(x^2 + x + 2x + 2)", + "-((x^2 + x) + (2x + 2))", + "-(x * (x + 1) + (2x + 2))", + "-(x * (x + 1) + 2 * (x + 1))", + "-(x + 1) * (x + 2)", + ], + ], + [ + "2x^2 + 5x + 3", + [ + "2x^2 + 2x + 3x + 3", + "(2x^2 + 2x) + (3x + 3)", + "2x * (x + 1) + (3x + 3)", + "2x * (x + 1) + 3 * (x + 1)", + "(x + 1) * (2x + 3)", + ], + ], + [ + "2x^2 - 5x - 3", + [ + "2x^2 + x - 6x - 3", + "(2x^2 + x) + (-6x - 3)", + "x * (2x + 1) + (-6x - 3)", + "x * (2x + 1) - 3 * (2x + 1)", + "(2x + 1) * (x - 3)", + ], + ], + [ + "2x^2 - 5x + 3", + [ + "2x^2 - 2x - 3x + 3", + "(2x^2 - 2x) + (-3x + 3)", + "2x * (x - 1) + (-3x + 3)", + "2x * (x - 1) - 3 * (x - 1)", + "(x - 1) * (2x - 3)", + ], + ], + ]; + tests.forEach((t) => testFactorSumProductRuleSubsteps(t[0], t[1])); +}); diff --git a/test/migrate-to-ts.sh b/test/migrate-to-ts.sh new file mode 100644 index 00000000..2d6887ef --- /dev/null +++ b/test/migrate-to-ts.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +OLD_EXTENSION="js" +NEW_EXTENSION="ts" + +for i in $(find `pwd` -name "*.${OLD_EXTENSION}"); +do + mv "$i" "${i%.$OLD_EXTENSION}.${NEW_EXTENSION}" +done diff --git a/test/simplifyExpression/arithmeticSearch/arithmeticSearch.test.js b/test/simplifyExpression/arithmeticSearch/arithmeticSearch.test.js deleted file mode 100644 index 5590e611..00000000 --- a/test/simplifyExpression/arithmeticSearch/arithmeticSearch.test.js +++ /dev/null @@ -1,18 +0,0 @@ -const arithmeticSearch = require('../../../lib/simplifyExpression/arithmeticSearch'); - -const TestUtil = require('../../TestUtil'); - -function testArithmeticSearch(exprStr, outputStr) { - TestUtil.testSimplification(arithmeticSearch, exprStr, outputStr); -} - -describe('evaluate arithmeticSearch', function () { - const tests = [ - ['2+2', '4'], - ['2*3*5', '30'], - ['6*6', '36'], - ['9/4', '9/4'], // does not divide - ['16 - 1953125', '-1953109'], // verify large negative number round correctly - ]; - tests.forEach(t => testArithmeticSearch(t[0], t[1])); -}); diff --git a/test/simplifyExpression/arithmeticSearch/arithmeticSearch.test.ts b/test/simplifyExpression/arithmeticSearch/arithmeticSearch.test.ts new file mode 100644 index 00000000..90c4d313 --- /dev/null +++ b/test/simplifyExpression/arithmeticSearch/arithmeticSearch.test.ts @@ -0,0 +1,17 @@ +import { TestUtil } from "../../TestUtil"; +import { arithmeticSearch } from "../../../lib/src/simplifyExpression/arithmeticSearch/ArithmeticSearch"; + +function testArithmeticSearch(exprStr, outputStr) { + TestUtil.testSimplification(arithmeticSearch, exprStr, outputStr); +} + +describe("evaluate arithmeticSearch", function () { + const tests = [ + ["2+2", "4"], + ["2*3*5", "30"], + ["6*6", "36"], + ["9/4", "9/4"], // does not divide + ["16 - 1953125", "-1953109"], // verify large negative number round correctly + ]; + tests.forEach((t) => testArithmeticSearch(t[0], t[1])); +}); diff --git a/test/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.test.js b/test/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.test.js deleted file mode 100644 index 4cdca7e9..00000000 --- a/test/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.test.js +++ /dev/null @@ -1,32 +0,0 @@ -const convertMixedNumberToImproperFraction = require( - '../../../lib/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction'); - -const TestUtil = require('../../TestUtil'); - -function testConvertMixedNumberToImproperFraction(exprString, outputList, outputStr) { - TestUtil.testSubsteps(convertMixedNumberToImproperFraction, exprString, outputList, outputStr); -} - -describe('convertMixedNumberToImproperFraction', function() { - const tests = [ - ['1(2)/(3)', - ['((1 * 3) + 2) / 3', - '(3 + 2) / 3', - '5/3'], - '5/3' - ], - ['19(4)/(8)', - ['((19 * 8) + 4) / 8', - '(152 + 4) / 8', - '156/8'], - '156/8' - ], - ['-5(10)/(11)', - ['-((5 * 11) + 10) / 11', - '-(55 + 10) / 11', - '-65/11'], - '-65/11' - ], - ]; - tests.forEach(t => testConvertMixedNumberToImproperFraction(t[0], t[1], t[2])); -}); diff --git a/test/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.test.ts b/test/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.test.ts new file mode 100644 index 00000000..20fec020 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction.test.ts @@ -0,0 +1,30 @@ +import { TestUtil } from "../../TestUtil"; +import { convertMixedNumberToImproperFraction } from "../../../lib/src/simplifyExpression/basicsSearch/convertMixedNumberToImproperFraction"; + +function testConvertMixedNumberToImproperFraction( + exprString, + outputList, + outputStr +) { + TestUtil.testSubsteps( + convertMixedNumberToImproperFraction, + exprString, + outputList, + outputStr + ); +} + +describe("convertMixedNumberToImproperFraction", function () { + const tests = [ + ["1(2)/(3)", ["((1 * 3) + 2) / 3", "(3 + 2) / 3", "5/3"], "5/3"], + ["19(4)/(8)", ["((19 * 8) + 4) / 8", "(152 + 4) / 8", "156/8"], "156/8"], + [ + "-5(10)/(11)", + ["-((5 * 11) + 10) / 11", "-(55 + 10) / 11", "-65/11"], + "-65/11", + ], + ]; + tests.forEach((t) => + testConvertMixedNumberToImproperFraction(t[0], t[1], t[2]) + ); +}); diff --git a/test/simplifyExpression/basicsSearch/rearrangeCoefficient.test.js b/test/simplifyExpression/basicsSearch/rearrangeCoefficient.test.js deleted file mode 100644 index d163dd7e..00000000 --- a/test/simplifyExpression/basicsSearch/rearrangeCoefficient.test.js +++ /dev/null @@ -1,11 +0,0 @@ -const rearrangeCoefficient = require('../../../lib/simplifyExpression/basicsSearch/rearrangeCoefficient'); - -const testSimplify = require('./testSimplify'); - -describe('rearrangeCoefficient', function() { - const tests = [ - ['2 * x^2', '2x^2'], - ['y^3 * 5', '5y^3'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], rearrangeCoefficient)); -}); diff --git a/test/simplifyExpression/basicsSearch/rearrangeCoefficient.test.ts b/test/simplifyExpression/basicsSearch/rearrangeCoefficient.test.ts new file mode 100644 index 00000000..8b126111 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/rearrangeCoefficient.test.ts @@ -0,0 +1,10 @@ +import { rearrangeCoefficient } from "../../../lib/src/simplifyExpression/basicsSearch/rearrangeCoefficient"; +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("rearrangeCoefficient", function () { + const tests = [ + ["2 * x^2", "2x^2"], + ["y^3 * 5", "5y^3"], + ]; + tests.forEach((t) => testSimplifyOperation(t[0], t[1], rearrangeCoefficient)); +}); diff --git a/test/simplifyExpression/basicsSearch/reduceExponentByZero.test.js b/test/simplifyExpression/basicsSearch/reduceExponentByZero.test.js deleted file mode 100644 index 506728f8..00000000 --- a/test/simplifyExpression/basicsSearch/reduceExponentByZero.test.js +++ /dev/null @@ -1,7 +0,0 @@ -const reduceExponentByZero = require('../../../lib/simplifyExpression/basicsSearch/reduceExponentByZero'); - -const testSimplify = require('./testSimplify'); - -describe('reduceExponentByZero', function() { - testSimplify('(x+3)^0', '1', reduceExponentByZero); -}); diff --git a/test/simplifyExpression/basicsSearch/reduceExponentByZero.test.ts b/test/simplifyExpression/basicsSearch/reduceExponentByZero.test.ts new file mode 100644 index 00000000..ab9c6b56 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/reduceExponentByZero.test.ts @@ -0,0 +1,6 @@ +import { reduceExponentByZero } from "../../../lib/src/simplifyExpression/basicsSearch/reduceExponentByZero"; +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("reduceExponentByZero", function () { + testSimplifyOperation("(x+3)^0", "1", reduceExponentByZero); +}); diff --git a/test/simplifyExpression/basicsSearch/reduceMutliplicationByZero.test.js b/test/simplifyExpression/basicsSearch/reduceMutliplicationByZero.test.js deleted file mode 100644 index 490a6f91..00000000 --- a/test/simplifyExpression/basicsSearch/reduceMutliplicationByZero.test.js +++ /dev/null @@ -1,11 +0,0 @@ -const reduceMultiplicationByZero = require('../../../lib/simplifyExpression/basicsSearch/reduceMultiplicationByZero'); - -const testSimplify = require('./testSimplify'); - -describe('reduce multiplication by 0', function () { - const tests = [ - ['0x', '0'], - ['2*0*z^2','0'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], reduceMultiplicationByZero)); -}); diff --git a/test/simplifyExpression/basicsSearch/reduceMutliplicationByZero.test.ts b/test/simplifyExpression/basicsSearch/reduceMutliplicationByZero.test.ts new file mode 100644 index 00000000..30c30fb6 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/reduceMutliplicationByZero.test.ts @@ -0,0 +1,13 @@ +import { reduceMultiplicationByZero } from "../../../lib/src/simplifyExpression/basicsSearch/reduceMultiplicationByZero"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("reduce multiplication by 0", function () { + const tests = [ + ["0x", "0"], + ["2*0*z^2", "0"], + ]; + tests.forEach((t) => + testSimplifyOperation(t[0], t[1], reduceMultiplicationByZero) + ); +}); diff --git a/test/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.test.js b/test/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.test.js deleted file mode 100644 index 2c24f182..00000000 --- a/test/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.test.js +++ /dev/null @@ -1,11 +0,0 @@ -const reduceZeroDividedByAnything = require('../../../lib/simplifyExpression/basicsSearch/reduceZeroDividedByAnything'); - -const testSimplify = require('./testSimplify'); - -describe('simplify basics', function () { - const tests = [ - ['0/5', '0'], - ['0/(x+6+7+x^2+2^y)', '0'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], reduceZeroDividedByAnything)); -}); diff --git a/test/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.test.ts b/test/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.test.ts new file mode 100644 index 00000000..5d243d6c --- /dev/null +++ b/test/simplifyExpression/basicsSearch/reduceZeroDividedByAnything.test.ts @@ -0,0 +1,13 @@ +import { reduceZeroDividedByAnything } from "../../../lib/src/simplifyExpression/basicsSearch/reduceZeroDividedByAnything"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("simplify basics", function () { + const tests = [ + ["0/5", "0"], + ["0/(x+6+7+x^2+2^y)", "0"], + ]; + tests.forEach((t) => + testSimplifyOperation(t[0], t[1], reduceZeroDividedByAnything) + ); +}); diff --git a/test/simplifyExpression/basicsSearch/removeAdditionOfZero.test.js b/test/simplifyExpression/basicsSearch/removeAdditionOfZero.test.js deleted file mode 100644 index 1d288236..00000000 --- a/test/simplifyExpression/basicsSearch/removeAdditionOfZero.test.js +++ /dev/null @@ -1,12 +0,0 @@ -const removeAdditionOfZero = require('../../../lib/simplifyExpression/basicsSearch/removeAdditionOfZero'); - -const testSimplify = require('./testSimplify'); - -describe('removeAdditionOfZero', function() { - var tests = [ - ['2+0+x', '2 + x'], - ['2+x+0', '2 + x'], - ['0+2+x', '2 + x'] - ]; - tests.forEach(t => testSimplify(t[0], t[1], removeAdditionOfZero)); -}); diff --git a/test/simplifyExpression/basicsSearch/removeAdditionOfZero.test.ts b/test/simplifyExpression/basicsSearch/removeAdditionOfZero.test.ts new file mode 100644 index 00000000..369ddc20 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/removeAdditionOfZero.test.ts @@ -0,0 +1,12 @@ +import { removeAdditionOfZero } from "../../../lib/src/simplifyExpression/basicsSearch/removeAdditionOfZero"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("removeAdditionOfZero", function () { + var tests = [ + ["2+0+x", "2 + x"], + ["2+x+0", "2 + x"], + ["0+2+x", "2 + x"], + ]; + tests.forEach((t) => testSimplifyOperation(t[0], t[1], removeAdditionOfZero)); +}); diff --git a/test/simplifyExpression/basicsSearch/removeDivisionByOne.test.js b/test/simplifyExpression/basicsSearch/removeDivisionByOne.test.js deleted file mode 100644 index acee1f9f..00000000 --- a/test/simplifyExpression/basicsSearch/removeDivisionByOne.test.js +++ /dev/null @@ -1,7 +0,0 @@ -const removeDivisionByOne = require('../../../lib/simplifyExpression/basicsSearch/removeDivisionByOne'); - -const testSimplify = require('./testSimplify'); - -describe('removeDivisionByOne', function() { - testSimplify('x/1', 'x', removeDivisionByOne); -}); diff --git a/test/simplifyExpression/basicsSearch/removeDivisionByOne.test.ts b/test/simplifyExpression/basicsSearch/removeDivisionByOne.test.ts new file mode 100644 index 00000000..fa542664 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/removeDivisionByOne.test.ts @@ -0,0 +1,7 @@ +import { removeDivisionByOne } from "../../../lib/src/simplifyExpression/basicsSearch/removeDivisionByOne"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("removeDivisionByOne", function () { + testSimplifyOperation("x/1", "x", removeDivisionByOne); +}); diff --git a/test/simplifyExpression/basicsSearch/removeExponentBaseOne.test.js b/test/simplifyExpression/basicsSearch/removeExponentBaseOne.test.js deleted file mode 100644 index 5aac993f..00000000 --- a/test/simplifyExpression/basicsSearch/removeExponentBaseOne.test.js +++ /dev/null @@ -1,12 +0,0 @@ -const removeExponentBaseOne = require('../../../lib/simplifyExpression/basicsSearch/removeExponentBaseOne'); - -const testSimplify = require('./testSimplify'); - -describe('removeExponentBaseOne', function() { - const tests = [ - ['1^3', '1'], - ['1^x', '1^x'], - ['1^(2 + 3 + 5/4 + 7 - 6/7)', '1'] - ]; - tests.forEach(t => testSimplify(t[0], t[1], removeExponentBaseOne)); -}); diff --git a/test/simplifyExpression/basicsSearch/removeExponentBaseOne.test.ts b/test/simplifyExpression/basicsSearch/removeExponentBaseOne.test.ts new file mode 100644 index 00000000..b37f78b4 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/removeExponentBaseOne.test.ts @@ -0,0 +1,14 @@ +import { removeExponentBaseOne } from "../../../lib/src/simplifyExpression/basicsSearch/removeExponentBaseOne"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("removeExponentBaseOne", function () { + const tests = [ + ["1^3", "1"], + ["1^x", "1^x"], + ["1^(2 + 3 + 5/4 + 7 - 6/7)", "1"], + ]; + tests.forEach((t) => + testSimplifyOperation(t[0], t[1], removeExponentBaseOne) + ); +}); diff --git a/test/simplifyExpression/basicsSearch/removeExponentByOne.test.js b/test/simplifyExpression/basicsSearch/removeExponentByOne.test.js deleted file mode 100644 index 047d1de4..00000000 --- a/test/simplifyExpression/basicsSearch/removeExponentByOne.test.js +++ /dev/null @@ -1,7 +0,0 @@ -const removeExponentByOne = require('../../../lib/simplifyExpression/basicsSearch/removeExponentByOne'); - -const testSimplify = require('./testSimplify'); - -describe('removeExponentByOne', function() { - testSimplify('x^1', 'x', removeExponentByOne); -}); diff --git a/test/simplifyExpression/basicsSearch/removeExponentByOne.test.ts b/test/simplifyExpression/basicsSearch/removeExponentByOne.test.ts new file mode 100644 index 00000000..48c74f57 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/removeExponentByOne.test.ts @@ -0,0 +1,7 @@ +import { removeExponentByOne } from "../../../lib/src/simplifyExpression/basicsSearch/removeExponentByOne"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("removeExponentByOne", function () { + testSimplifyOperation("x^1", "x", removeExponentByOne); +}); diff --git a/test/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.test.js b/test/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.test.js deleted file mode 100644 index e701c1f1..00000000 --- a/test/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.test.js +++ /dev/null @@ -1,12 +0,0 @@ -const removeMultiplicationByNegativeOne = require('../../../lib/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne'); - -const testSimplify = require('./testSimplify'); - -describe('removeMultiplicationByNegativeOne', function() { - const tests = [ - ['-1*x', '-x'], - ['x^2*-1', '-x^2'], - ['2x*2*-1', '2x * 2 * -1'], // does not remove multiplication by -1 - ]; - tests.forEach(t => testSimplify(t[0], t[1], removeMultiplicationByNegativeOne)); -}); diff --git a/test/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.test.ts b/test/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.test.ts new file mode 100644 index 00000000..f8639015 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne.test.ts @@ -0,0 +1,14 @@ +import { removeMultiplicationByNegativeOne } from "../../../lib/src/simplifyExpression/basicsSearch/removeMultiplicationByNegativeOne"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("removeMultiplicationByNegativeOne", function () { + const tests = [ + ["-1*x", "-x"], + ["x^2*-1", "-x^2"], + ["2x*2*-1", "2x * 2 * -1"], // does not remove multiplication by -1 + ]; + tests.forEach((t) => + testSimplifyOperation(t[0], t[1], removeMultiplicationByNegativeOne) + ); +}); diff --git a/test/simplifyExpression/basicsSearch/removeMultiplicationByOne.test.js b/test/simplifyExpression/basicsSearch/removeMultiplicationByOne.test.js deleted file mode 100644 index 0e554444..00000000 --- a/test/simplifyExpression/basicsSearch/removeMultiplicationByOne.test.js +++ /dev/null @@ -1,13 +0,0 @@ -const removeMultiplicationByOne = require('../../../lib/simplifyExpression/basicsSearch/removeMultiplicationByOne'); - -const testSimplify = require('./testSimplify'); - -describe('removeMultiplicationByOne', function() { - const tests = [ - ['x*1', 'x'], - ['1x', 'x'], - ['1*z^2', 'z^2'], - ['2*1*z^2', '2 * 1z^2'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], removeMultiplicationByOne)); -}); diff --git a/test/simplifyExpression/basicsSearch/removeMultiplicationByOne.test.ts b/test/simplifyExpression/basicsSearch/removeMultiplicationByOne.test.ts new file mode 100644 index 00000000..83f77b77 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/removeMultiplicationByOne.test.ts @@ -0,0 +1,15 @@ +import { removeMultiplicationByOne } from "../../../lib/src/simplifyExpression/basicsSearch/removeMultiplicationByOne"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("removeMultiplicationByOne", function () { + const tests = [ + ["x*1", "x"], + ["1x", "x"], + ["1*z^2", "z^2"], + ["2*1*z^2", "2 * 1z^2"], + ]; + tests.forEach((t) => + testSimplifyOperation(t[0], t[1], removeMultiplicationByOne) + ); +}); diff --git a/test/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.test.js b/test/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.test.js deleted file mode 100644 index 33c13a44..00000000 --- a/test/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.test.js +++ /dev/null @@ -1,12 +0,0 @@ -const simplifyDoubleUnaryMinus = require('../../../lib/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus'); - -const testSimplify = require('./testSimplify'); - - -describe('simplifyDoubleUnaryMinus', function() { - var tests = [ - ['--5', '5'], - ['--x', 'x'] - ]; - tests.forEach(t => testSimplify(t[0], t[1], simplifyDoubleUnaryMinus)); -}); diff --git a/test/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.test.ts b/test/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.test.ts new file mode 100644 index 00000000..b0767301 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus.test.ts @@ -0,0 +1,13 @@ +import { simplifyDoubleUnaryMinus } from "../../../lib/src/simplifyExpression/basicsSearch/simplifyDoubleUnaryMinus"; + +import { testSimplifyOperation } from "./testSimplify.test"; + +describe("simplifyDoubleUnaryMinus", function () { + var tests = [ + ["--5", "5"], + ["--x", "x"], + ]; + tests.forEach((t) => + testSimplifyOperation(t[0], t[1], simplifyDoubleUnaryMinus) + ); +}); diff --git a/test/simplifyExpression/basicsSearch/testSimplify.js b/test/simplifyExpression/basicsSearch/testSimplify.js deleted file mode 100644 index d059750e..00000000 --- a/test/simplifyExpression/basicsSearch/testSimplify.js +++ /dev/null @@ -1,17 +0,0 @@ -const assert = require('assert'); - -const print = require('../../../lib/util/print'); - -const TestUtil = require('../../TestUtil'); - -function testSimplify(exprStr, outputStr, simplifyOperation) { - it(exprStr + ' -> ' + outputStr, function () { - const inputNode = TestUtil.parseAndFlatten(exprStr); - const newNode = simplifyOperation(inputNode).newNode; - assert.equal( - print.ascii(newNode), - outputStr); - }); -} - -module.exports = testSimplify; diff --git a/test/simplifyExpression/basicsSearch/testSimplify.test.ts b/test/simplifyExpression/basicsSearch/testSimplify.test.ts new file mode 100644 index 00000000..324e8c91 --- /dev/null +++ b/test/simplifyExpression/basicsSearch/testSimplify.test.ts @@ -0,0 +1,30 @@ +import assert = require("assert"); +import { printAscii } from "../../../lib/src/util/print"; +import { TestUtil } from "../../TestUtil"; +import * as math from "mathjs"; +import { simplify } from "../../../lib/src/simplifyExpression/simplify"; + +export function testSimplifyOperation(exprStr, outputStr, simplifyOperation) { + it(exprStr + " -> " + outputStr, function () { + const inputNode = TestUtil.parseAndFlatten(exprStr); + const newNode = simplifyOperation(inputNode).newNode; + assert.equal(printAscii(newNode), outputStr); + }); +} + +export function testSimplify( + inputString: string, + expectedOutputString: string, + debug = false +) { + it(inputString + " -> " + expectedOutputString, () => { + const parsed = math.parse(inputString); + const simplified = simplify(parsed, debug); + const printed = printAscii(simplified); + if (debug) { + console.log("parsed", parsed, JSON.stringify(parsed)); + console.log("printed", printed); + } + assert.deepEqual(printed, expectedOutputString); + }); +} diff --git a/test/simplifyExpression/breakUpNumeratorSearch/breakUpNumeratorSearch.test.js b/test/simplifyExpression/breakUpNumeratorSearch/breakUpNumeratorSearch.test.js deleted file mode 100644 index 73aef5c1..00000000 --- a/test/simplifyExpression/breakUpNumeratorSearch/breakUpNumeratorSearch.test.js +++ /dev/null @@ -1,16 +0,0 @@ -const breakUpNumeratorSearch = require('../../../lib/simplifyExpression/breakUpNumeratorSearch'); - -const TestUtil = require('../../TestUtil'); - -function testBreakUpNumeratorSearch(exprStr, outputStr) { - TestUtil.testSimplification(breakUpNumeratorSearch, exprStr, outputStr); -} - -describe('breakUpNumerator', function() { - const tests = [ - ['(x+3+y)/3', '(x / 3 + 3/3 + y / 3)'], - ['(2+x)/4', '(2/4 + x / 4)'], - ['2(x+3)/3', '2 * (x / 3 + 3/3)'], - ]; - tests.forEach(t => testBreakUpNumeratorSearch(t[0], t[1])); -}); diff --git a/test/simplifyExpression/breakUpNumeratorSearch/breakUpNumeratorSearch.test.ts b/test/simplifyExpression/breakUpNumeratorSearch/breakUpNumeratorSearch.test.ts new file mode 100644 index 00000000..c5ba3787 --- /dev/null +++ b/test/simplifyExpression/breakUpNumeratorSearch/breakUpNumeratorSearch.test.ts @@ -0,0 +1,16 @@ +import { breakUpNumeratorSearch } from "../../../lib/src/simplifyExpression/breakUpNumeratorSearch"; + +import { TestUtil } from "../../TestUtil"; + +function testBreakUpNumeratorSearch(exprStr, outputStr) { + TestUtil.testSimplification(breakUpNumeratorSearch, exprStr, outputStr); +} + +describe("breakUpNumerator", function () { + const tests = [ + ["(x+3+y)/3", "(x / 3 + 3/3 + y / 3)"], + ["(2+x)/4", "(2/4 + x / 4)"], + ["2(x+3)/3", "2 * (x / 3 + 3/3)"], + ]; + tests.forEach((t) => testBreakUpNumeratorSearch(t[0], t[1])); +}); diff --git a/test/simplifyExpression/collectAndCombineSearch/LikeTermCollector.test.js b/test/simplifyExpression/collectAndCombineSearch/LikeTermCollector.test.js deleted file mode 100644 index 050ad53d..00000000 --- a/test/simplifyExpression/collectAndCombineSearch/LikeTermCollector.test.js +++ /dev/null @@ -1,115 +0,0 @@ -const assert = require('assert'); - -const print = require('../../../lib/util/print'); - -const LikeTermCollector = require('../../../lib/simplifyExpression/collectAndCombineSearch/LikeTermCollector'); - -const TestUtil = require('../../TestUtil'); - -function testCollectLikeTerms(exprStr, outputStr, explanation='', debug=false) { - let description = `${exprStr} -> ${outputStr}`; - - if (explanation) { - description += ` (${explanation})`; - } - - it(description, () => { - const exprTree = TestUtil.parseAndFlatten(exprStr); - const collected = print.ascii(LikeTermCollector.collectLikeTerms(exprTree).newNode); - if (debug) { - // eslint-disable-next-line - console.log(collected); - } - assert.equal(collected, outputStr); - }); -} - -function testCanCollectLikeTerms(exprStr, canCollect, explanation) { - let description = `${exprStr} -> ${canCollect}`; - - if (explanation) { - description += ` (${explanation})`; - } - - it(description , () => { - const exprTree = TestUtil.parseAndFlatten(exprStr); - assert.equal( - LikeTermCollector.canCollectLikeTerms(exprTree), - canCollect); - }); -} - -describe('can collect like terms for addition', function () { - const tests = [ - ['2+2', false, 'because only one type'], - ['x^2+x^2', false, 'because only one type'], - ['x+2', false, 'because all types have only one'], - ['(x+2+x)', false, 'because in parenthesis, need to be collected first'], - ['x+2+x', true], - ['x^2 + 5 + x + x^2', true], - ['nthRoot(2) + nthRoot(2)', false], - ['nthRoot(x, 2) + nthRoot(x, 2) + 5', true], - ['7x + nthRoot(3*x, 2) + nthRoot(3*x, 2)', true], - ]; - tests.forEach(t => testCanCollectLikeTerms(t[0], t[1], t[2])); -}); - -describe('can collect like terms for multiplication', function () { - const tests = [ - ['2*2', false, 'because only one type'], - ['x^2 * 2x^2', true], - ['x * 2', false, 'because all types have only one'], - ['((2x^2)) * y * x * y^3', true], - ]; - tests.forEach(t => testCanCollectLikeTerms(t[0], t[1], t[2])); -}); - -describe('basic addition collect like terms, no exponents or coefficients', function() { - const tests = [ - ['2+x+7', 'x + (2 + 7)'], - ['x + 4 + x + 5', '(x + x) + (4 + 5)'], - ['x + 4 + y', 'x + 4 + y'], - ['x + 4 + x + 4/9 + y + 5/7', '(x + x) + y + 4 + (4/9 + 5/7)'], - ['x + 4 + x + 2^x + 5', '(x + x) + (4 + 5) + 2^x', - 'because 2^x is an \'other\''], - ['z + 2*(y + x) + 4 + z', '(z + z) + 4 + 2 * (y + x)', - ' 2*(y + x) is an \'other\' cause it has parens'], - ['nthRoot(2) + 100 + nthRoot(2)', '(nthRoot(2) + nthRoot(2)) + 100'], - ['y + nthRoot(x, 2) + 4y + nthRoot(x, 2)', '(nthRoot(x, 2) + nthRoot(x, 2)) + (y + 4y)'], - ['nthRoot(x, 2) + 2 + nthRoot(x, 2) + 5', '(nthRoot(x, 2) + nthRoot(x, 2)) + (2 + 5)'], - ]; - tests.forEach(t => testCollectLikeTerms(t[0], t[1], t[2])); -}); - -describe('collect like terms with exponents and coefficients', function() { - const tests = [ - ['x^2 + x + x^2 + x', '(x^2 + x^2) + (x + x)'], - ['y^2 + 5 + y^2 + 5', '(y^2 + y^2) + (5 + 5)'], - ['y + 5 + z^2', 'y + 5 + z^2'], - ['2x^2 + x + x^2 + 3x', '(2x^2 + x^2) + (x + 3x)'], - ['nthRoot(2)^3 + nthRoot(2)^3 - 6x', '(nthRoot(2)^3 + nthRoot(2)^3) - 6x'], - ['4x + 7 * nthRoot(11) - x - 2 * nthRoot(11)', '(7 * nthRoot(11) - 2 * nthRoot(11)) + (4x - x)'], - ]; - tests.forEach(t => testCollectLikeTerms(t[0], t[1], t[2])); -}); - -describe('collect like terms for multiplication', function() { - const tests = [ - ['2x^2 * y * x * y^3', '2 * (x^2 * x) * (y * y^3)'], - ['y^2 * 5 * y * 9', '(5 * 9) * (y^2 * y)'], - ['5y^2 * -4y * 9', '(5 * -4 * 9) * (y^2 * y)'], - ['5y^2 * -y * 9', '(5 * -1 * 9) * (y^2 * y)'], - ['y * 5 * (2+x) * y^2 * 1/3', '(5 * 1/3) * (y * y^2) * (2 + x)'], - ]; - tests.forEach(t => testCollectLikeTerms(t[0], t[1], t[2])); -}); - -describe('collect like terms for nthRoot multiplication', function() { - const tests = [ - ['nthRoot(x, 2) * nthRoot(x, 2)', 'nthRoot(x, 2) * nthRoot(x, 2)'], - ['nthRoot(x, 2) * nthRoot(x, 2) * 3', '3 * (nthRoot(x, 2) * nthRoot(x, 2))'], - ['nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3)', '(nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)'], - ['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))'], - ]; - tests.forEach(t => testCollectLikeTerms(t[0], t[1])); -}); diff --git a/test/simplifyExpression/collectAndCombineSearch/LikeTermCollector.test.ts b/test/simplifyExpression/collectAndCombineSearch/LikeTermCollector.test.ts new file mode 100644 index 00000000..8c234fb0 --- /dev/null +++ b/test/simplifyExpression/collectAndCombineSearch/LikeTermCollector.test.ts @@ -0,0 +1,142 @@ +import { LikeTermCollector } from "../../../lib/src/simplifyExpression/collectAndCombineSearch/LikeTermCollector"; + +import { TestUtil } from "../../TestUtil"; +import { printAscii } from "../../../lib/src/util/print"; +import assert = require("assert"); + +function testCollectLikeTerms( + exprStr, + outputStr, + explanation = "", + debug = false +) { + let description = `${exprStr} -> ${outputStr}`; + + if (explanation) { + description += ` (${explanation})`; + } + + it(description, () => { + const exprTree = TestUtil.parseAndFlatten(exprStr); + const collected = printAscii( + LikeTermCollector.collectLikeTerms(exprTree).newNode + ); + if (debug) { + // eslint-disable-next-line + console.log(collected); + } + assert.equal(collected, outputStr); + }); +} + +function testCanCollectLikeTerms(exprStr, canCollect, explanation) { + let description = `${exprStr} -> ${canCollect}`; + + if (explanation) { + description += ` (${explanation})`; + } + + it(description, () => { + const exprTree = TestUtil.parseAndFlatten(exprStr); + assert.equal(LikeTermCollector.canCollectLikeTerms(exprTree), canCollect); + }); +} + +describe("can collect like terms for addition", function () { + const tests = [ + ["2+2", false, "because only one type"], + ["x^2+x^2", false, "because only one type"], + ["x+2", false, "because all types have only one"], + ["(x+2+x)", false, "because in parenthesis, need to be collected first"], + ["x+2+x", true], + ["x^2 + 5 + x + x^2", true], + ["nthRoot(2) + nthRoot(2)", false], + ["nthRoot(x, 2) + nthRoot(x, 2) + 5", true], + ["7x + nthRoot(3*x, 2) + nthRoot(3*x, 2)", true], + ]; + tests.forEach((t) => testCanCollectLikeTerms(t[0], t[1], t[2])); +}); + +describe("can collect like terms for multiplication", function () { + const tests = [ + ["2*2", false, "because only one type"], + ["x^2 * 2x^2", true], + ["x * 2", false, "because all types have only one"], + ["((2x^2)) * y * x * y^3", true], + ]; + tests.forEach((t) => testCanCollectLikeTerms(t[0], t[1], t[2])); +}); + +describe("basic addition collect like terms, no exponents or coefficients", function () { + const tests = [ + ["2+x+7", "x + (2 + 7)"], + ["x + 4 + x + 5", "(x + x) + (4 + 5)"], + ["x + 4 + y", "x + 4 + y"], + ["x + 4 + x + 4/9 + y + 5/7", "(x + x) + y + 4 + (4/9 + 5/7)"], + [ + "x + 4 + x + 2^x + 5", + "(x + x) + (4 + 5) + 2^x", + "because 2^x is an 'other'", + ], + [ + "z + 2*(y + x) + 4 + z", + "(z + z) + 4 + 2 * (y + x)", + " 2*(y + x) is an 'other' cause it has parens", + ], + ["nthRoot(2) + 100 + nthRoot(2)", "(nthRoot(2) + nthRoot(2)) + 100"], + [ + "y + nthRoot(x, 2) + 4y + nthRoot(x, 2)", + "(nthRoot(x, 2) + nthRoot(x, 2)) + (y + 4y)", + ], + [ + "nthRoot(x, 2) + 2 + nthRoot(x, 2) + 5", + "(nthRoot(x, 2) + nthRoot(x, 2)) + (2 + 5)", + ], + ]; + tests.forEach((t) => testCollectLikeTerms(t[0], t[1], t[2])); +}); + +describe("collect like terms with exponents and coefficients", function () { + const tests = [ + ["x^2 + x + x^2 + x", "(x^2 + x^2) + (x + x)"], + ["y^2 + 5 + y^2 + 5", "(y^2 + y^2) + (5 + 5)"], + ["y + 5 + z^2", "y + 5 + z^2"], + ["2x^2 + x + x^2 + 3x", "(2x^2 + x^2) + (x + 3x)"], + ["nthRoot(2)^3 + nthRoot(2)^3 - 6x", "(nthRoot(2)^3 + nthRoot(2)^3) - 6x"], + [ + "4x + 7 * nthRoot(11) - x - 2 * nthRoot(11)", + "(7 * nthRoot(11) - 2 * nthRoot(11)) + (4x - x)", + ], + ]; + tests.forEach((t) => testCollectLikeTerms(t[0], t[1], t[2])); +}); + +describe("collect like terms for multiplication", function () { + const tests = [ + ["2x^2 * y * x * y^3", "2 * (x^2 * x) * (y * y^3)"], + ["y^2 * 5 * y * 9", "(5 * 9) * (y^2 * y)"], + ["5y^2 * -4y * 9", "(5 * -4 * 9) * (y^2 * y)"], + ["5y^2 * -y * 9", "(5 * -1 * 9) * (y^2 * y)"], + ["y * 5 * (2+x) * y^2 * 1/3", "(5 * 1/3) * (y * y^2) * (2 + x)"], + ]; + tests.forEach((t) => testCollectLikeTerms(t[0], t[1], t[2])); +}); + +describe("collect like terms for nthRoot multiplication", function () { + const tests = [ + ["nthRoot(x, 2) * nthRoot(x, 2)", "nthRoot(x, 2) * nthRoot(x, 2)"], + [ + "nthRoot(x, 2) * nthRoot(x, 2) * 3", + "3 * (nthRoot(x, 2) * nthRoot(x, 2))", + ], + [ + "nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3)", + "(nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)", + ], + [ + "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))", + ], + ]; + tests.forEach((t) => testCollectLikeTerms(t[0], t[1])); +}); diff --git a/test/simplifyExpression/collectAndCombineSearch/collectAndCombineSearch.test.js b/test/simplifyExpression/collectAndCombineSearch/collectAndCombineSearch.test.js deleted file mode 100644 index 39e705c8..00000000 --- a/test/simplifyExpression/collectAndCombineSearch/collectAndCombineSearch.test.js +++ /dev/null @@ -1,147 +0,0 @@ -const collectAndCombineSearch = require('../../../lib/simplifyExpression/collectAndCombineSearch'); - -const TestUtil = require('../../TestUtil'); - -function testCollectAndCombineSubsteps(exprString, outputList, outputStr) { - TestUtil.testSubsteps(collectAndCombineSearch, exprString, outputList, outputStr); -} - -function testSimpleCollectAndCombineSearch(exprString, outputStr) { - TestUtil.testSimplification(collectAndCombineSearch, exprString, outputStr); -} - -describe('combineNthRoots multiplication', function() { - const tests = [ - ['nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3)', - ['(nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)', - 'nthRoot(x * x, 2) * nthRoot(x, 3)'], - ], - ['nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3) * 3', - ['3 * (nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)', - '3 * nthRoot(x * x, 2) * nthRoot(x, 3)'], - ], - ['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))', - 'nthRoot(2 x * 2 x, 2) * (nthRoot(y, 4) * nthRoot(y ^ 3, 4))', - 'nthRoot(2 x * 2 x, 2) * nthRoot(y * y ^ 3, 4)'], - ], - ['nthRoot(x) * nthRoot(x)', - [], - 'nthRoot(x * x, 2)' - ], - ['nthRoot(3) * nthRoot(3)', - [], - 'nthRoot(3 * 3, 2)' - ], - ['nthRoot(5) * nthRoot(9x, 2)', - [], - 'nthRoot(5 * 9 x, 2)' - ] - ]; - tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1], t[2])); -}); - -describe('combinePolynomialTerms multiplication', function() { - const tests = [ - ['x^2 * x * x', - ['x^2 * x^1 * x^1', - 'x^(2 + 1 + 1)', - 'x^4'], - ], - ['y * y^3', - ['y^1 * y^3', - 'y^(1 + 3)', - 'y^4'], - ], - ['2x * x^2 * 5x', - ['(2 * 5) * (x * x^2 * x)', - '10 * (x * x^2 * x)', - '10x^4'], - '10x^4' - ], - ]; - tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1], t[2])); -}); - -describe('combinePolynomialTerms addition', function() { - const tests = [ - ['x+x', - ['1x + 1x', - '(1 + 1) * x', - '2x'] - ], - ['4y^2 + 7y^2 + y^2', - ['4y^2 + 7y^2 + 1y^2', - '(4 + 7 + 1) * y^2', - '12y^2'] - ], - ['2x + 4x + y', - ['(2x + 4x) + y', - '6x + y'], - '6x + y' - ], - ]; - tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1])); -}); - -describe('combineNthRootTerms addition', function() { - const tests = [ - ['nthRoot(x) + nthRoot(x)', - ['1 * nthRoot(x) + 1 * nthRoot(x)', - '(1 + 1) * nthRoot(x)', - '2 * nthRoot(x)'] - ], - ['4nthRoot(2)^2 + 7nthRoot(2)^2 + nthRoot(2)^2', - ['4 * nthRoot(2)^2 + 7 * nthRoot(2)^2 + 1 * nthRoot(2)^2', - '(4 + 7 + 1) * nthRoot(2)^2', - '12 * nthRoot(2)^2'] - ], - ['10nthRoot(5y) - 2nthRoot(5y)', - ['(10 - 2) * nthRoot(5 y)', - '8 * nthRoot(5 y)'], - ], - ]; - tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1])); -}); - -describe('combineConstantPowerTerms multiplication', function() { - const tests = [ - ['10^2 * 10', - ['10^2 * 10^1', - '10^(2 + 1)', - '10^3'], - ], - ['2 * 2^3', - ['2^1 * 2^3', - '2^(1 + 3)', - '2^4'], - ], - ['3^3 * 3 * 3', - ['3^3 * 3^1 * 3^1', - '3^(3 + 1 + 1)', - '3^5'], - ], - ]; - tests.forEach(t => testCollectAndCombineSubsteps(t[0], t[1], t[2])); -}); - -describe('collectAndCombineSearch with no substeps', function () { - const tests = [ - ['nthRoot(x, 2) * nthRoot(x, 2)', 'nthRoot(x * x, 2)'], - ['-nthRoot(x, 2) * nthRoot(x, 2)', '-1 * nthRoot(x * x, 2)'], - ['-nthRoot(x, 2) * -nthRoot(x, 2)', '1 * nthRoot(x * x, 2)'], - ['2x + 4x + x', '7x'], - ['x * x^2 * x', 'x^4'], - ['3*nthRoot(11) - 2*nthRoot(11)', '1 * nthRoot(11)'], - ['nthRoot(xy) + 2x + nthRoot(xy) + 5x', '2 * nthRoot(xy) + 7x'], - ]; - tests.forEach(t => testSimpleCollectAndCombineSearch(t[0], t[1])); -}); - -describe('collect and multiply like terms', function() { - const tests = [ - ['10^3 * 10^2', '10^5'], - ['2^4 * 2 * 2^4 * 2', '2^10'] - ]; - tests.forEach(t => testSimpleCollectAndCombineSearch(t[0], t[1])); -}); diff --git a/test/simplifyExpression/collectAndCombineSearch/collectAndCombineSearch.test.ts b/test/simplifyExpression/collectAndCombineSearch/collectAndCombineSearch.test.ts new file mode 100644 index 00000000..3787805a --- /dev/null +++ b/test/simplifyExpression/collectAndCombineSearch/collectAndCombineSearch.test.ts @@ -0,0 +1,125 @@ +import { collectAndCombineSearch } from "../../../lib/src/simplifyExpression/collectAndCombineSearch"; + +import { TestUtil } from "../../TestUtil"; + +function testCollectAndCombineSubsteps(exprString, outputList, outputStr?) { + TestUtil.testSubsteps( + collectAndCombineSearch, + exprString, + outputList, + outputStr + ); +} + +function testSimpleCollectAndCombineSearch(exprString, outputStr) { + TestUtil.testSimplification(collectAndCombineSearch, exprString, outputStr); +} + +describe("combineNthRoots multiplication", function () { + const tests = [ + [ + "nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3)", + [ + "(nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)", + "nthRoot(x * x, 2) * nthRoot(x, 3)", + ], + ], + [ + "nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 3) * 3", + [ + "3 * (nthRoot(x, 2) * nthRoot(x, 2)) * nthRoot(x, 3)", + "3 * nthRoot(x * x, 2) * nthRoot(x, 3)", + ], + ], + [ + "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))", + "nthRoot(2 x * 2 x, 2) * (nthRoot(y, 4) * nthRoot(y ^ 3, 4))", + "nthRoot(2 x * 2 x, 2) * nthRoot(y * y ^ 3, 4)", + ], + ], + ["nthRoot(x) * nthRoot(x)", [], "nthRoot(x * x, 2)"], + ["nthRoot(3) * nthRoot(3)", [], "nthRoot(3 * 3, 2)"], + ["nthRoot(5) * nthRoot(9x, 2)", [], "nthRoot(5 * 9 x, 2)"], + ]; + tests.forEach((t) => testCollectAndCombineSubsteps(t[0], t[1], t[2])); +}); + +describe("combinePolynomialTerms multiplication", function () { + const tests = [ + ["x^2 * x * x", ["x^2 * x^1 * x^1", "x^(2 + 1 + 1)", "x^4"]], + ["y * y^3", ["y^1 * y^3", "y^(1 + 3)", "y^4"]], + [ + "2x * x^2 * 5x", + ["(2 * 5) * (x * x^2 * x)", "10 * (x * x^2 * x)", "10x^4"], + "10x^4", + ], + ]; + tests.forEach((t) => testCollectAndCombineSubsteps(t[0], t[1], t[2])); +}); + +describe("combinePolynomialTerms addition", function () { + const tests = [ + ["x+x", ["1x + 1x", "(1 + 1) * x", "2x"]], + ["4y^2 + 7y^2 + y^2", ["4y^2 + 7y^2 + 1y^2", "(4 + 7 + 1) * y^2", "12y^2"]], + ["2x + 4x + y", ["(2x + 4x) + y", "6x + y"], "6x + y"], + ]; + tests.forEach((t) => testCollectAndCombineSubsteps(t[0], t[1])); +}); + +describe("combineNthRootTerms addition", function () { + const tests = [ + [ + "nthRoot(x) + nthRoot(x)", + [ + "1 * nthRoot(x) + 1 * nthRoot(x)", + "(1 + 1) * nthRoot(x)", + "2 * nthRoot(x)", + ], + ], + [ + "4nthRoot(2)^2 + 7nthRoot(2)^2 + nthRoot(2)^2", + [ + "4 * nthRoot(2)^2 + 7 * nthRoot(2)^2 + 1 * nthRoot(2)^2", + "(4 + 7 + 1) * nthRoot(2)^2", + "12 * nthRoot(2)^2", + ], + ], + [ + "10nthRoot(5y) - 2nthRoot(5y)", + ["(10 - 2) * nthRoot(5 y)", "8 * nthRoot(5 y)"], + ], + ]; + tests.forEach((t) => testCollectAndCombineSubsteps(t[0], t[1])); +}); + +describe("combineConstantPowerTerms multiplication", function () { + const tests = [ + ["10^2 * 10", ["10^2 * 10^1", "10^(2 + 1)", "10^3"]], + ["2 * 2^3", ["2^1 * 2^3", "2^(1 + 3)", "2^4"]], + ["3^3 * 3 * 3", ["3^3 * 3^1 * 3^1", "3^(3 + 1 + 1)", "3^5"]], + ]; + tests.forEach((t) => testCollectAndCombineSubsteps(t[0], t[1], t[2])); +}); + +describe("collectAndCombineSearch with no substeps", function () { + const tests = [ + ["nthRoot(x, 2) * nthRoot(x, 2)", "nthRoot(x * x, 2)"], + ["-nthRoot(x, 2) * nthRoot(x, 2)", "-1 * nthRoot(x * x, 2)"], + ["-nthRoot(x, 2) * -nthRoot(x, 2)", "1 * nthRoot(x * x, 2)"], + ["2x + 4x + x", "7x"], + ["x * x^2 * x", "x^4"], + ["3*nthRoot(11) - 2*nthRoot(11)", "1 * nthRoot(11)"], + ["nthRoot(xy) + 2x + nthRoot(xy) + 5x", "2 * nthRoot(xy) + 7x"], + ]; + tests.forEach((t) => testSimpleCollectAndCombineSearch(t[0], t[1])); +}); + +describe("collect and multiply like terms", function () { + const tests = [ + ["10^3 * 10^2", "10^5"], + ["2^4 * 2 * 2^4 * 2", "2^10"], + ]; + tests.forEach((t) => testSimpleCollectAndCombineSearch(t[0], t[1])); +}); diff --git a/test/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.test.js b/test/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.test.js deleted file mode 100644 index 972c2f57..00000000 --- a/test/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.test.js +++ /dev/null @@ -1,33 +0,0 @@ -const evaluateConstantSum = require('../../../lib/simplifyExpression/collectAndCombineSearch/evaluateConstantSum'); - -const TestUtil = require('../../TestUtil'); - -function testEvaluateConstantSum(exprString, outputList) { - const lastString = outputList[outputList.length - 1]; - TestUtil.testSubsteps(evaluateConstantSum, exprString, outputList, lastString); -} - -describe('evaluateConstantSum', function () { - const tests = [ - ['4/10 + 3/5', - ['4/10 + (3 * 2) / (5 * 2)', - '4/10 + (3 * 2) / 10', - '4/10 + 6/10', - '(4 + 6) / 10', - '10/10', - '1'] - ], - ['4/5 + 3/5 + 2', - ['2 + (4/5 + 3/5)', - '2 + 7/5', - '17/5'] - ], - ['9 + 4/5 + 1/5 + 2', - ['(9 + 2) + (4/5 + 1/5)', - '11 + (4/5 + 1/5)', - '11 + 1', - '12'] - ], - ]; - tests.forEach(t => testEvaluateConstantSum(t[0], t[1])); -}); diff --git a/test/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.test.ts b/test/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.test.ts new file mode 100644 index 00000000..431e4e22 --- /dev/null +++ b/test/simplifyExpression/collectAndCombineSearch/evaluateConstantSum.test.ts @@ -0,0 +1,35 @@ +import { evaluateConstantSum } from "../../../lib/src/simplifyExpression/collectAndCombineSearch/evaluateConstantSum"; + +import { TestUtil } from "../../TestUtil"; + +function testEvaluateConstantSum(exprString, outputList) { + const lastString = outputList[outputList.length - 1]; + TestUtil.testSubsteps( + evaluateConstantSum, + exprString, + outputList, + lastString + ); +} + +describe("evaluateConstantSum", function () { + const tests = [ + [ + "4/10 + 3/5", + [ + "4/10 + (3 * 2) / (5 * 2)", + "4/10 + (3 * 2) / 10", + "4/10 + 6/10", + "(4 + 6) / 10", + "10/10", + "1", + ], + ], + ["4/5 + 3/5 + 2", ["2 + (4/5 + 3/5)", "2 + 7/5", "17/5"]], + [ + "9 + 4/5 + 1/5 + 2", + ["(9 + 2) + (4/5 + 1/5)", "11 + (4/5 + 1/5)", "11 + 1", "12"], + ], + ]; + tests.forEach((t) => testEvaluateConstantSum(t[0], t[1])); +}); diff --git a/test/simplifyExpression/distributeSearch/distributeSearch.test.js b/test/simplifyExpression/distributeSearch/distributeSearch.test.js deleted file mode 100644 index 6dac7900..00000000 --- a/test/simplifyExpression/distributeSearch/distributeSearch.test.js +++ /dev/null @@ -1,110 +0,0 @@ -const distributeSearch = require('../../../lib/simplifyExpression/distributeSearch'); - -const TestUtil = require('../../TestUtil'); - -function testDistribute(exprStr, outputStr) { - TestUtil.testSimplification(distributeSearch, exprStr, outputStr); -} - -describe('distribute - into paren with addition', function () { - const tests = [ - ['-(x+3)', '(-x - 3)'], - ['-(x - 3)', '(-x + 3)'], - ['-(-x^2 + 3y^6)' , '(x^2 - 3y^6)'], - ]; - tests.forEach(t => testDistribute(t[0], t[1])); -}); - -describe('distribute - into paren with multiplication/division', function () { - const tests = [ - ['-(x*3)', '(-x * 3)'], - ['-(-x * 3)', '(x * 3)'], - ['-(-x^2 * 3y^6)', '(x^2 * 3y^6)'], - ]; - tests.forEach(t => testDistribute(t[0], t[1])); -}); - -function testDistributeSteps(exprString, outputList) { - const lastString = outputList[outputList.length - 1]; - TestUtil.testSubsteps(distributeSearch, exprString, outputList, lastString); -} - -describe('distribute', function () { - const tests = [ - ['x*(x+2+y)', - ['(x * x + x * 2 + x * y)', - '(x^2 + 2x + x * y)'] - ], - ['(x+2+y)*x*7', - ['(x * x + 2x + y * x) * 7', - '(x^2 + 2x + y * x) * 7'] - ], - ['(5+x)*(x+3)', - ['(5 * (x + 3) + x * (x + 3))', - '((5x + 15) + (x^2 + 3x))'] - ], - ['-2x^2 * (3x - 4)', - ['(-2x^2 * 3x - 2x^2 * -4)', - '(-6x^3 + 8x^2)'] - ], - ]; - tests.forEach(t => testDistributeSteps(t[0], t[1])); -}); - -describe('distribute with fractions', function () { - const tests = [ - // distribute the non-fraction term into the numerator(s) - ['(3 / x^2 + x / (x^2 + 3)) * (x^2 + 3)', - '((3 * (x^2 + 3)) / (x^2) + (x * (x^2 + 3)) / (x^2 + 3))', - ], - - // if both groupings have fraction, the rule does not apply - ['(3 / x^2 + x / (x^2 + 3)) * (5 / x + x^5)', - '((3 / (x^2) * 5 / x + 3 / (x^2) * x^5) + (x / (x^2 + 3) * 5 / x + x / (x^2 + 3) * x^5))', - ], - ]; - - const multiStepTests = [ - - ['(2 / x + 3x^2) * (x^3 + 1)', - ['((2 * (x^3 + 1)) / x + 3x^2 * (x^3 + 1))', - '((2 * (x^3 + 1)) / x + (3x^5 + 3x^2))'] - ], - - ['(2x + x^2) * (1 / (x^2 -4) + 4x^2)', - ['((1 * (2x + x^2)) / (x^2 - 4) + 4x^2 * (2x + x^2))', - '((1 * (2x + x^2)) / (x^2 - 4) + (8x^3 + 4x^4))'] - ], - - ['(2x + x^2) * (3x^2 / (x^2 -4) + 4x^2)', - ['((3x^2 * (2x + x^2)) / (x^2 - 4) + 4x^2 * (2x + x^2))', - '((3x^2 * (2x + x^2)) / (x^2 - 4) + (8x^3 + 4x^4))'] - ], - - ]; - - tests.forEach(t => testDistribute(t[0], t[1])); - - multiStepTests.forEach(t => testDistributeSteps(t[0], t[1])); -}); - -describe('expand base', function () { - const tests = [ - ['(nthRoot(x, 2))^2','nthRoot(x, 2) * nthRoot(x, 2)'], - ['(nthRoot(x, 2))^3','nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2)'], - ['3 * (nthRoot(x, 2))^4', '3 * nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2)'], - ['(nthRoot(x, 2) + nthRoot(x, 3))^2', '(nthRoot(x, 2) + nthRoot(x, 3)) * (nthRoot(x, 2) + nthRoot(x, 3))'], - ['(2x + 3)^2', '(2x + 3) * (2x + 3)'], - ['(x + 3 + 4)^2', '(x + 3 + 4) * (x + 3 + 4)'], - // These should not expand - // Needs to have a positive integer exponent > 1 - ['x + 2', 'x + 2'], - ['(x + 2)^-1', '(x + 2)^-1'], - ['(x + 1)^x', '(x + 1)^x'], - ['(x + 1)^(2x)', '(x + 1)^(2x)'], - ['(x + 1)^(1/2)', '(x + 1)^(1/2)'], - ['(x + 1)^2.5', '(x + 1)^2.5'], - ]; - - tests.forEach(t => testDistribute(t[0], t[1])); -}); diff --git a/test/simplifyExpression/distributeSearch/distributeSearch.test.ts b/test/simplifyExpression/distributeSearch/distributeSearch.test.ts new file mode 100644 index 00000000..3df4e052 --- /dev/null +++ b/test/simplifyExpression/distributeSearch/distributeSearch.test.ts @@ -0,0 +1,116 @@ +import { distributeSearch } from "../../../lib/src/simplifyExpression/distributeSearch"; + +import { TestUtil } from "../../TestUtil"; + +function testDistribute(exprStr, outputStr) { + TestUtil.testSimplification(distributeSearch, exprStr, outputStr); +} + +describe("distribute - into paren with addition", function () { + const tests = [ + ["-(x+3)", "(-x - 3)"], + ["-(x - 3)", "(-x + 3)"], + ["-(-x^2 + 3y^6)", "(x^2 - 3y^6)"], + ]; + tests.forEach((t) => testDistribute(t[0], t[1])); +}); + +describe("distribute - into paren with multiplication/division", function () { + const tests = [ + ["-(x*3)", "(-x * 3)"], + ["-(-x * 3)", "(x * 3)"], + ["-(-x^2 * 3y^6)", "(x^2 * 3y^6)"], + ]; + tests.forEach((t) => testDistribute(t[0], t[1])); +}); + +function testDistributeSteps(exprString, outputList) { + const lastString = outputList[outputList.length - 1]; + TestUtil.testSubsteps(distributeSearch, exprString, outputList, lastString); +} + +describe("distribute", function () { + const tests = [ + ["x*(x+2+y)", ["(x * x + x * 2 + x * y)", "(x^2 + 2x + x * y)"]], + ["(x+2+y)*x*7", ["(x * x + 2x + y * x) * 7", "(x^2 + 2x + y * x) * 7"]], + [ + "(5+x)*(x+3)", + ["(5 * (x + 3) + x * (x + 3))", "((5x + 15) + (x^2 + 3x))"], + ], + ["-2x^2 * (3x - 4)", ["(-2x^2 * 3x - 2x^2 * -4)", "(-6x^3 + 8x^2)"]], + ]; + tests.forEach((t) => testDistributeSteps(t[0], t[1])); +}); + +describe("distribute with fractions", function () { + const tests = [ + // distribute the non-fraction term into the numerator(s) + [ + "(3 / x^2 + x / (x^2 + 3)) * (x^2 + 3)", + "((3 * (x^2 + 3)) / (x^2) + (x * (x^2 + 3)) / (x^2 + 3))", + ], + + // if both groupings have fraction, the rule does not apply + [ + "(3 / x^2 + x / (x^2 + 3)) * (5 / x + x^5)", + "((3 / (x^2) * 5 / x + 3 / (x^2) * x^5) + (x / (x^2 + 3) * 5 / x + x / (x^2 + 3) * x^5))", + ], + ]; + + const multiStepTests = [ + [ + "(2 / x + 3x^2) * (x^3 + 1)", + [ + "((2 * (x^3 + 1)) / x + 3x^2 * (x^3 + 1))", + "((2 * (x^3 + 1)) / x + (3x^5 + 3x^2))", + ], + ], + + [ + "(2x + x^2) * (1 / (x^2 -4) + 4x^2)", + [ + "((1 * (2x + x^2)) / (x^2 - 4) + 4x^2 * (2x + x^2))", + "((1 * (2x + x^2)) / (x^2 - 4) + (8x^3 + 4x^4))", + ], + ], + + [ + "(2x + x^2) * (3x^2 / (x^2 -4) + 4x^2)", + [ + "((3x^2 * (2x + x^2)) / (x^2 - 4) + 4x^2 * (2x + x^2))", + "((3x^2 * (2x + x^2)) / (x^2 - 4) + (8x^3 + 4x^4))", + ], + ], + ]; + + tests.forEach((t) => testDistribute(t[0], t[1])); + + multiStepTests.forEach((t) => testDistributeSteps(t[0], t[1])); +}); + +describe("expand base", function () { + const tests = [ + ["(nthRoot(x, 2))^2", "nthRoot(x, 2) * nthRoot(x, 2)"], + ["(nthRoot(x, 2))^3", "nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2)"], + [ + "3 * (nthRoot(x, 2))^4", + "3 * nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2) * nthRoot(x, 2)", + ], + [ + "(nthRoot(x, 2) + nthRoot(x, 3))^2", + "(nthRoot(x, 2) + nthRoot(x, 3)) * (nthRoot(x, 2) + nthRoot(x, 3))", + ], + ["(2x + 3)^2", "(2x + 3) * (2x + 3)"], + ["(x + 3 + 4)^2", "(x + 3 + 4) * (x + 3 + 4)"], + // These should not expand + // Needs to have a positive integer exponent > 1 + ["x + 2", "x + 2"], + ["(x + 2)^-1", "(x + 2)^-1"], + ["(x + 1)^x", "(x + 1)^x"], + ["(x + 1)^(2x)", "(x + 1)^(2x)"], + ["(x + 1)^(1/2)", "(x + 1)^(1/2)"], + ["(x + 1)^2.5", "(x + 1)^2.5"], + ]; + + tests.forEach((t) => testDistribute(t[0], t[1])); +}); diff --git a/test/simplifyExpression/divisionSearch/divisionSearch.test.js b/test/simplifyExpression/divisionSearch/divisionSearch.test.js deleted file mode 100644 index 2d4a3a5b..00000000 --- a/test/simplifyExpression/divisionSearch/divisionSearch.test.js +++ /dev/null @@ -1,21 +0,0 @@ -const divisionSearch = require('../../../lib/simplifyExpression/divisionSearch'); - -const TestUtil = require('../../TestUtil'); - -function testSimplifyDivision(exprStr, outputStr) { - TestUtil.testSimplification(divisionSearch, exprStr, outputStr); -} - -describe('simplifyDivision', function () { - const tests = [ - ['6/x/5', '6 / (x * 5)'], - ['-(6/x/5)', '-(6 / (x * 5))'], - ['-6/x/5', '-6 / (x * 5)'], - ['(2+2)/x/6/(y-z)','(2 + 2) / (x * 6 * (y - z))'], - ['2/x', '2 / x'], - ['x/(2/3)', 'x * 3/2'], - ['x / (y/(z+a))', 'x * (z + a) / y'], - ['x/((2+z)/(3/y))', 'x * (3 / y) / (2 + z)'], - ]; - tests.forEach(t => testSimplifyDivision(t[0], t[1])); -}); diff --git a/test/simplifyExpression/divisionSearch/divisionSearch.test.ts b/test/simplifyExpression/divisionSearch/divisionSearch.test.ts new file mode 100644 index 00000000..b9ac42b7 --- /dev/null +++ b/test/simplifyExpression/divisionSearch/divisionSearch.test.ts @@ -0,0 +1,21 @@ +import { divisionSearch } from "../../../lib/src/simplifyExpression/divisionSearch"; + +import { TestUtil } from "../../TestUtil"; + +function testSimplifyDivision(exprStr, outputStr) { + TestUtil.testSimplification(divisionSearch, exprStr, outputStr); +} + +describe("simplifyDivision", function () { + const tests = [ + ["6/x/5", "6 / (x * 5)"], + ["-(6/x/5)", "-(6 / (x * 5))"], + ["-6/x/5", "-6 / (x * 5)"], + ["(2+2)/x/6/(y-z)", "(2 + 2) / (x * 6 * (y - z))"], + ["2/x", "2 / x"], + ["x/(2/3)", "x * 3/2"], + ["x / (y/(z+a))", "x * (z + a) / y"], + ["x/((2+z)/(3/y))", "x * (3 / y) / (2 + z)"], + ]; + tests.forEach((t) => testSimplifyDivision(t[0], t[1])); +}); diff --git a/test/simplifyExpression/fractionsSearch/addConstantAndFraction.test.js b/test/simplifyExpression/fractionsSearch/addConstantAndFraction.test.js deleted file mode 100644 index 0219bb8b..00000000 --- a/test/simplifyExpression/fractionsSearch/addConstantAndFraction.test.js +++ /dev/null @@ -1,32 +0,0 @@ -const addConstantAndFraction = require('../../../lib/simplifyExpression/fractionsSearch/addConstantAndFraction'); - -const TestUtil = require('../../TestUtil'); - -function testAddConstantAndFraction(exprString, outputList) { - const lastString = outputList[outputList.length - 1]; - TestUtil.testSubsteps(addConstantAndFraction, exprString, outputList, lastString); -} - -describe('addConstantAndFraction', function () { - const tests = [ - ['7 + 1/2', - ['14/2 + 1/2', - '(14 + 1) / 2', - '15/2'] - ], - ['5/6 + 3', - ['5/6 + 18/6', - '(5 + 18) / 6', - '23/6'], - ], - ['1/2 + 5.8', - ['0.5 + 5.8', - '6.3'], - ], - ['1/3 + 5.8', - ['0.3333 + 5.8', - '6.1333'] - ], - ]; - tests.forEach(t => testAddConstantAndFraction(t[0], t[1])); -}); diff --git a/test/simplifyExpression/fractionsSearch/addConstantAndFraction.test.ts b/test/simplifyExpression/fractionsSearch/addConstantAndFraction.test.ts new file mode 100644 index 00000000..d5a44474 --- /dev/null +++ b/test/simplifyExpression/fractionsSearch/addConstantAndFraction.test.ts @@ -0,0 +1,23 @@ +import { addConstantAndFraction } from "../../../lib/src/simplifyExpression/fractionsSearch/addConstantAndFraction"; + +import { TestUtil } from "../../TestUtil"; + +function testAddConstantAndFraction(exprString, outputList) { + const lastString = outputList[outputList.length - 1]; + TestUtil.testSubsteps( + addConstantAndFraction, + exprString, + outputList, + lastString + ); +} + +describe("addConstantAndFraction", function () { + const tests = [ + ["7 + 1/2", ["14/2 + 1/2", "(14 + 1) / 2", "15/2"]], + ["5/6 + 3", ["5/6 + 18/6", "(5 + 18) / 6", "23/6"]], + ["1/2 + 5.8", ["0.5 + 5.8", "6.3"]], + ["1/3 + 5.8", ["0.3333 + 5.8", "6.1333"]], + ]; + tests.forEach((t) => testAddConstantAndFraction(t[0], t[1])); +}); diff --git a/test/simplifyExpression/fractionsSearch/addConstantFractions.test.js b/test/simplifyExpression/fractionsSearch/addConstantFractions.test.js deleted file mode 100644 index c023d94c..00000000 --- a/test/simplifyExpression/fractionsSearch/addConstantFractions.test.js +++ /dev/null @@ -1,38 +0,0 @@ -const addConstantFractions = require('../../../lib/simplifyExpression/fractionsSearch/addConstantFractions'); - -const TestUtil = require('../../TestUtil'); - -function testAddConstantFractions(exprString, outputList) { - const lastString = outputList[outputList.length - 1]; - TestUtil.testSubsteps(addConstantFractions, exprString, outputList, lastString); -} - -describe('addConstantFractions', function () { - const tests = [ - ['4/5 + 3/5', - ['(4 + 3) / 5', - '7/5'] - ], - ['4/10 + 3/5', - ['4/10 + (3 * 2) / (5 * 2)', - '4/10 + (3 * 2) / 10', - '4/10 + 6/10', - '(4 + 6) / 10', - '10/10', - '1'] - ], - ['4/9 + 3/5', - ['(4 * 5) / (9 * 5) + (3 * 9) / (5 * 9)', - '(4 * 5) / 45 + (3 * 9) / 45', - '20/45 + 27/45', - '(20 + 27) / 45', - '47/45'] - ], - ['4/5 - 4/5', - ['(4 - 4) / 5', - '0/5', - '0'] - ], - ]; - tests.forEach(t => testAddConstantFractions(t[0], t[1])); -}); diff --git a/test/simplifyExpression/fractionsSearch/addConstantFractions.test.ts b/test/simplifyExpression/fractionsSearch/addConstantFractions.test.ts new file mode 100644 index 00000000..66ecaf23 --- /dev/null +++ b/test/simplifyExpression/fractionsSearch/addConstantFractions.test.ts @@ -0,0 +1,42 @@ +import { addConstantFractions } from "../../../lib/src/simplifyExpression/fractionsSearch/addConstantFractions"; + +import { TestUtil } from "../../TestUtil"; + +function testAddConstantFractions(exprString, outputList) { + const lastString = outputList[outputList.length - 1]; + TestUtil.testSubsteps( + addConstantFractions, + exprString, + outputList, + lastString + ); +} + +describe("addConstantFractions", function () { + const tests = [ + ["4/5 + 3/5", ["(4 + 3) / 5", "7/5"]], + [ + "4/10 + 3/5", + [ + "4/10 + (3 * 2) / (5 * 2)", + "4/10 + (3 * 2) / 10", + "4/10 + 6/10", + "(4 + 6) / 10", + "10/10", + "1", + ], + ], + [ + "4/9 + 3/5", + [ + "(4 * 5) / (9 * 5) + (3 * 9) / (5 * 9)", + "(4 * 5) / 45 + (3 * 9) / 45", + "20/45 + 27/45", + "(20 + 27) / 45", + "47/45", + ], + ], + ["4/5 - 4/5", ["(4 - 4) / 5", "0/5", "0"]], + ]; + tests.forEach((t) => testAddConstantFractions(t[0], t[1])); +}); diff --git a/test/simplifyExpression/fractionsSearch/cancelLikeTerms.test.js b/test/simplifyExpression/fractionsSearch/cancelLikeTerms.test.js deleted file mode 100644 index 6fe79df6..00000000 --- a/test/simplifyExpression/fractionsSearch/cancelLikeTerms.test.js +++ /dev/null @@ -1,38 +0,0 @@ -const cancelLikeTerms = require('../../../lib/simplifyExpression/fractionsSearch/cancelLikeTerms'); - -const TestUtil = require('../../TestUtil'); - -function testCancelLikeTerms(exprStr, expectedStr) { - TestUtil.testSimplification(cancelLikeTerms, exprStr, expectedStr); -} - -describe('cancel like terms', function () { - const tests = [ - ['2/2', '1'], - ['x^2/x^2', '1'], - ['x^3/x^2', 'x^(3 - (2))'], // parens will be removed at end of step - ['(x^3*y)/x^2', '(x^(3 - (2)) * y)'], - ['-(7+x)^8/(7+x)^2', '-(7 + x)^(8 - (2))'], - ['(2x^2 * 5) / (2x^2)', '5'], // these parens have to stay around 2x^2 to be parsed correctly. - ['(x^2 * y) / x', '(x^(2 - (1)) * y)'], - ['2x^2 / (2x^2 * 5)', '1/5'], - ['x / (x^2*y)', 'x^(1 - (2)) / y'], - ['(4x^2) / (5x^2)', '(4) / (5)'], - ['(2x+5)^8 / (2x+5)^2', '(2x + 5)^(8 - (2))'], - ['(4x^3) / (5x^2)', '(4x^(3 - (2))) / (5)'], - ['-x / -x', '1'], - ['2/ (4x)', '1 / (2x)'], - ['2/ (4x^2)', '1 / (2x^2)'], - ['2 a / a', '2'], - ['(35 * nthRoot (7)) / (5 * nthRoot(5))', '(7 * nthRoot(7)) / nthRoot(5)'], - ['3/(9r^2)', '1 / (3r^2)'], - ['6/(2x)', '3 / (x)'], - ['(40 * x) / (20 * y)', '(2x) / (y)'], - ['(20 * x) / (40 * y)', '(x) / (2y)'], - ['20x / (40y)', 'x / (2y)'], - ['60x / (40y)', '3x / (2y)'], - ['4x / (2y)', '2x / (y)'] - ]; - - tests.forEach(t => testCancelLikeTerms(t[0], t[1])); -}); diff --git a/test/simplifyExpression/fractionsSearch/cancelLikeTerms.test.ts b/test/simplifyExpression/fractionsSearch/cancelLikeTerms.test.ts new file mode 100644 index 00000000..8c1bc7d7 --- /dev/null +++ b/test/simplifyExpression/fractionsSearch/cancelLikeTerms.test.ts @@ -0,0 +1,38 @@ +import { cancelLikeTerms } from "../../../lib/src/simplifyExpression/fractionsSearch/cancelLikeTerms"; + +import { TestUtil } from "../../TestUtil"; + +function testCancelLikeTerms(exprStr, expectedStr) { + TestUtil.testSimplification(cancelLikeTerms, exprStr, expectedStr); +} + +describe("cancel like terms", function () { + const tests = [ + ["2/2", "1"], + ["x^2/x^2", "1"], + ["x^3/x^2", "x^(3 - (2))"], // parens will be removed at end of step + ["(x^3*y)/x^2", "(x^(3 - (2)) * y)"], + ["-(7+x)^8/(7+x)^2", "-(7 + x)^(8 - (2))"], + ["(2x^2 * 5) / (2x^2)", "5"], // these parens have to stay around 2x^2 to be parsed correctly. + ["(x^2 * y) / x", "(x^(2 - (1)) * y)"], + ["2x^2 / (2x^2 * 5)", "1/5"], + ["x / (x^2*y)", "x^(1 - (2)) / y"], + ["(4x^2) / (5x^2)", "(4) / (5)"], + ["(2x+5)^8 / (2x+5)^2", "(2x + 5)^(8 - (2))"], + ["(4x^3) / (5x^2)", "(4x^(3 - (2))) / (5)"], + ["-x / -x", "1"], + ["2/ (4x)", "1 / (2x)"], + ["2/ (4x^2)", "1 / (2x^2)"], + ["2 a / a", "2"], + ["(35 * nthRoot (7)) / (5 * nthRoot(5))", "(7 * nthRoot(7)) / nthRoot(5)"], + ["3/(9r^2)", "1 / (3r^2)"], + ["6/(2x)", "3 / (x)"], + ["(40 * x) / (20 * y)", "(2x) / (y)"], + ["(20 * x) / (40 * y)", "(x) / (2y)"], + ["20x / (40y)", "x / (2y)"], + ["60x / (40y)", "3x / (2y)"], + ["4x / (2y)", "2x / (y)"], + ]; + + tests.forEach((t) => testCancelLikeTerms(t[0], t[1])); +}); diff --git a/test/simplifyExpression/fractionsSearch/divideByGCD.test.js b/test/simplifyExpression/fractionsSearch/divideByGCD.test.js deleted file mode 100644 index b04ebc12..00000000 --- a/test/simplifyExpression/fractionsSearch/divideByGCD.test.js +++ /dev/null @@ -1,39 +0,0 @@ -const divideByGCD = require('../../../lib/simplifyExpression/fractionsSearch/divideByGCD'); - -const TestUtil = require('../../TestUtil'); - -function testDivideByGCD(exprStr, outputStr) { - TestUtil.testSimplification(divideByGCD, exprStr, outputStr); -} - -function testDivideByGCDSubsteps(exprString, outputList, outputStr) { - TestUtil.testSubsteps(divideByGCD, exprString, outputList, outputStr); -} - -describe('simplifyFraction', function() { - const tests = [ - ['2/4', '1/2'], - ['9/3', '3'], - ['12/27', '4/9'], - ['1/-3', '-1/3'], - ['-3/-2', '3/2'], - ['-1/-1', '1'], - ]; - tests.forEach(t => testDivideByGCD(t[0], t[1])); -}); - -describe('simplifyFraction', function() { - const tests = [ - ['15/6', - ['(5 * 3) / (2 * 3)', - '5/2'], - '5/2', - ], - ['24/40', - ['(3 * 8) / (5 * 8)', - '3/5'], - '3/5', - ] - ]; - tests.forEach(t => testDivideByGCDSubsteps(t[0], t[1], t[2])); -}); diff --git a/test/simplifyExpression/fractionsSearch/divideByGCD.test.ts b/test/simplifyExpression/fractionsSearch/divideByGCD.test.ts new file mode 100644 index 00000000..c821d150 --- /dev/null +++ b/test/simplifyExpression/fractionsSearch/divideByGCD.test.ts @@ -0,0 +1,31 @@ +import { divideByGCD } from "../../../lib/src/simplifyExpression/fractionsSearch/divideByGCD"; + +import { TestUtil } from "../../TestUtil"; + +function testDivideByGCD(exprStr, outputStr) { + TestUtil.testSimplification(divideByGCD, exprStr, outputStr); +} + +function testDivideByGCDSubsteps(exprString, outputList, outputStr) { + TestUtil.testSubsteps(divideByGCD, exprString, outputList, outputStr); +} + +describe("simplifyFraction", function () { + const tests = [ + ["2/4", "1/2"], + ["9/3", "3"], + ["12/27", "4/9"], + ["1/-3", "-1/3"], + ["-3/-2", "3/2"], + ["-1/-1", "1"], + ]; + tests.forEach((t) => testDivideByGCD(t[0], t[1])); +}); + +describe("simplifyFraction", function () { + const tests = [ + ["15/6", ["(5 * 3) / (2 * 3)", "5/2"], "5/2"], + ["24/40", ["(3 * 8) / (5 * 8)", "3/5"], "3/5"], + ]; + tests.forEach((t) => testDivideByGCDSubsteps(t[0], t[1], t[2])); +}); diff --git a/test/simplifyExpression/fractionsSearch/simplifyFractionSigns.test.js b/test/simplifyExpression/fractionsSearch/simplifyFractionSigns.test.js deleted file mode 100644 index c4aa000a..00000000 --- a/test/simplifyExpression/fractionsSearch/simplifyFractionSigns.test.js +++ /dev/null @@ -1,15 +0,0 @@ -const simplifyFractionSigns = require('../../../lib/simplifyExpression/fractionsSearch/simplifyFractionSigns'); - -const TestUtil = require('../../TestUtil'); - -function testSimplifyFractionSigns(exprStr, outputStr) { - TestUtil.testSimplification(simplifyFractionSigns, exprStr, outputStr); -} - -describe('simplify signs', function() { - const tests = [ - ['-12x / -27', '12x / 27'], - ['x / -y', '-x / y'], - ]; - tests.forEach(t => testSimplifyFractionSigns(t[0], t[1])); -}); diff --git a/test/simplifyExpression/fractionsSearch/simplifyFractionSigns.test.ts b/test/simplifyExpression/fractionsSearch/simplifyFractionSigns.test.ts new file mode 100644 index 00000000..4fad94bb --- /dev/null +++ b/test/simplifyExpression/fractionsSearch/simplifyFractionSigns.test.ts @@ -0,0 +1,15 @@ +import { simplifyFractionSigns } from "../../../lib/src/simplifyExpression/fractionsSearch/simplifyFractionSigns"; + +import { TestUtil } from "../../TestUtil"; + +function testSimplifyFractionSigns(exprStr, outputStr) { + TestUtil.testSimplification(simplifyFractionSigns, exprStr, outputStr); +} + +describe("simplify signs", function () { + const tests = [ + ["-12x / -27", "12x / 27"], + ["x / -y", "-x / y"], + ]; + tests.forEach((t) => testSimplifyFractionSigns(t[0], t[1])); +}); diff --git a/test/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.test.js b/test/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.test.js deleted file mode 100644 index 1d16748d..00000000 --- a/test/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.test.js +++ /dev/null @@ -1,20 +0,0 @@ -const simplifyPolynomialFraction = require('../../../lib/simplifyExpression/fractionsSearch/simplifyPolynomialFraction'); - -const TestUtil = require('../../TestUtil'); - -function testSimplifyPolynomialFraction(exprStr, outputStr) { - TestUtil.testSimplification(simplifyPolynomialFraction, exprStr, outputStr); -} - -describe('simplifyPolynomialFraction', function() { - const tests = [ - ['2x/4', '1/2 x'], - ['9y/3', '3y'], - ['y/-3', '-1/3 y'], - ['-3y/-2', '3/2 y'], - ['-y/-1', 'y'], - ['12z^2/27', '4/9 z^2'], - ['1.6x / 1.6', 'x'], - ]; - tests.forEach(t => testSimplifyPolynomialFraction(t[0], t[1])); -}); diff --git a/test/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.test.ts b/test/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.test.ts new file mode 100644 index 00000000..02c27d43 --- /dev/null +++ b/test/simplifyExpression/fractionsSearch/simplifyPolynomialFraction.test.ts @@ -0,0 +1,20 @@ +import { simplifyPolynomialFraction } from "../../../lib/src/simplifyExpression/fractionsSearch/simplifyPolynomialFraction"; + +import { TestUtil } from "../../TestUtil"; + +function testSimplifyPolynomialFraction(exprStr, outputStr) { + TestUtil.testSimplification(simplifyPolynomialFraction, exprStr, outputStr); +} + +describe("simplifyPolynomialFraction", function () { + const tests = [ + ["2x/4", "1/2 x"], + ["9y/3", "3y"], + ["y/-3", "-1/3 y"], + ["-3y/-2", "3/2 y"], + ["-y/-1", "y"], + ["12z^2/27", "4/9 z^2"], + ["1.6x / 1.6", "x"], + ]; + tests.forEach((t) => testSimplifyPolynomialFraction(t[0], t[1])); +}); diff --git a/test/simplifyExpression/functionsSearch/absoluteValue.test.js b/test/simplifyExpression/functionsSearch/absoluteValue.test.js deleted file mode 100644 index 923c6561..00000000 --- a/test/simplifyExpression/functionsSearch/absoluteValue.test.js +++ /dev/null @@ -1,15 +0,0 @@ -const absoluteValue = require('../../../lib/simplifyExpression/functionsSearch/absoluteValue'); - -const TestUtil = require('../../TestUtil'); - -function testAbsoluteValue(exprString, outputStr) { - TestUtil.testSimplification(absoluteValue, exprString, outputStr); -} - -describe('abs', function () { - const tests = [ - ['abs(4)', '4'], - ['abs(-5)', '5'], - ]; - tests.forEach(t => testAbsoluteValue(t[0], t[1])); -}); diff --git a/test/simplifyExpression/functionsSearch/absoluteValue.test.ts b/test/simplifyExpression/functionsSearch/absoluteValue.test.ts new file mode 100644 index 00000000..10999015 --- /dev/null +++ b/test/simplifyExpression/functionsSearch/absoluteValue.test.ts @@ -0,0 +1,15 @@ +import { absoluteValue } from "../../../lib/src/simplifyExpression/functionsSearch/absoluteValue"; + +import { TestUtil } from "../../TestUtil"; + +function testAbsoluteValue(exprString, outputStr) { + TestUtil.testSimplification(absoluteValue, exprString, outputStr); +} + +describe("abs", function () { + const tests = [ + ["abs(4)", "4"], + ["abs(-5)", "5"], + ]; + tests.forEach((t) => testAbsoluteValue(t[0], t[1])); +}); diff --git a/test/simplifyExpression/functionsSearch/nthRoot.test.js b/test/simplifyExpression/functionsSearch/nthRoot.test.js deleted file mode 100644 index 4931a982..00000000 --- a/test/simplifyExpression/functionsSearch/nthRoot.test.js +++ /dev/null @@ -1,94 +0,0 @@ -const NthRoot = require('../../../lib/simplifyExpression/functionsSearch/nthRoot'); - -const TestUtil = require('../../TestUtil'); - -function testNthRoot(exprString, outputStr) { - TestUtil.testSimplification(NthRoot.nthRoot, exprString, outputStr); -} - -describe('simplify nthRoot', function () { - const tests = [ - ['nthRoot(4)', '2'], - ['nthRoot(8, 3)', '2'], - ['nthRoot(5 * 7)', 'nthRoot(5 * 7)'], - ['nthRoot(4, 3)', 'nthRoot(4, 3)'], - ['nthRoot(12)', '2 * nthRoot(3, 2)'], - ['nthRoot(36)', '6'], - ['nthRoot(72)', '2 * 3 * nthRoot(2, 2)'], - ['nthRoot(x^2)', 'x'], - ['nthRoot(x ^ 3)', 'nthRoot(x ^ 3)'], - ['nthRoot(x^3, 3)', 'x'], - ['nthRoot(-2)', 'nthRoot(-2)'], - ['nthRoot(2 ^ x, x)', '2'], - ['nthRoot(x ^ (1/2), 1/2)', 'x'], - ['nthRoot(2 * 2, 2)', '2'], - ['nthRoot(3 * 2 * 3 * 2, 2)', '2 * 3'], - ['nthRoot(36*x)', '2 * 3 * nthRoot(x, 2)'], - ['nthRoot(2 * 18 * x ^ 2, 2)', '2 * 3 * x'], - ['nthRoot(x * x, 2)', 'x'], - ['nthRoot(x * x * (2 + 3), 2)', 'x * nthRoot((2 + 3), 2)'], - ['nthRoot(64, 3)', '4'], - ['nthRoot(35937, 3)', '33'], - ]; - tests.forEach(t => testNthRoot(t[0], t[1])); -}); - -function testNthRootSteps(exprString, outputList) { - const lastString = outputList[outputList.length - 1]; - TestUtil.testSubsteps(NthRoot.nthRoot, exprString, outputList, lastString); -} - -describe('nthRoot steps', function () { - const tests = [ - ['nthRoot(12)', - ['nthRoot(2 * 2 * 3)', - 'nthRoot((2 * 2) * 3, 2)', - 'nthRoot(2 ^ 2 * 3, 2)', - 'nthRoot(2 ^ 2, 2) * nthRoot(3, 2)', - '2 * nthRoot(3, 2)'] - ], - ['nthRoot(72)', - ['nthRoot(2 * 2 * 2 * 3 * 3)', - 'nthRoot((2 * 2) * 2 * (3 * 3), 2)', - 'nthRoot(2 ^ 2 * 2 * 3 ^ 2, 2)', - 'nthRoot(2 ^ 2, 2) * nthRoot(2, 2) * nthRoot(3 ^ 2, 2)', - '2 * nthRoot(2, 2) * 3', - '2 * 3 * nthRoot(2, 2)'] - ], - ['nthRoot(36*x)', - ['nthRoot(2 * 2 * 3 * 3 * x)', - 'nthRoot((2 * 2) * (3 * 3) * x, 2)', - 'nthRoot(2 ^ 2 * 3 ^ 2 * x, 2)', - 'nthRoot(2 ^ 2, 2) * nthRoot(3 ^ 2, 2) * nthRoot(x, 2)', - '2 * 3 * nthRoot(x, 2)'] - ], - ['nthRoot(2 * 18 * x ^ 2, 2)', - ['nthRoot(2 * 2 * 3 * 3 * x ^ 2, 2)', - 'nthRoot((2 * 2) * (3 * 3) * x ^ 2, 2)', - 'nthRoot(2 ^ 2 * 3 ^ 2 * x ^ 2, 2)', - 'nthRoot(2 ^ 2, 2) * nthRoot(3 ^ 2, 2) * nthRoot(x ^ 2, 2)', - '2 * 3 * x'] - ], - ['nthRoot(32, 3)', - ['nthRoot(2 * 2 * 2 * 2 * 2, 3)', - 'nthRoot((2 * 2 * 2) * (2 * 2), 3)', - 'nthRoot(2 ^ 3 * 2 ^ 2, 3)', - 'nthRoot(2 ^ 3, 3) * nthRoot(2 ^ 2, 3)', - '2 * nthRoot(2 ^ 2, 3)'] - ], - ['nthRoot(32, 4)', - ['nthRoot(2 * 2 * 2 * 2 * 2, 4)', - 'nthRoot((2 * 2 * 2 * 2) * 2, 4)', - 'nthRoot(2 ^ 4 * 2, 4)', - 'nthRoot(2 ^ 4, 4) * nthRoot(2, 4)', - '2 * nthRoot(2, 4)'] - ], - ['nthRoot(2 * 2 * 3 * 2, 3)', - ['nthRoot((2 * 2 * 2) * 3, 3)', - 'nthRoot(2 ^ 3 * 3, 3)', - 'nthRoot(2 ^ 3, 3) * nthRoot(3, 3)', - '2 * nthRoot(3, 3)'] - ], - ]; - tests.forEach(t => testNthRootSteps(t[0], t[1])); -}); diff --git a/test/simplifyExpression/functionsSearch/nthRoot.test.ts b/test/simplifyExpression/functionsSearch/nthRoot.test.ts new file mode 100644 index 00000000..fc66b7b1 --- /dev/null +++ b/test/simplifyExpression/functionsSearch/nthRoot.test.ts @@ -0,0 +1,114 @@ +import { TestUtil } from "../../TestUtil"; +import { nthRoot } from "../../../lib/src/simplifyExpression/functionsSearch/nthRoot"; + +function testNthRoot(exprString, outputStr) { + TestUtil.testSimplification(nthRoot, exprString, outputStr); +} + +describe("simplify nthRoot", function () { + const tests = [ + ["nthRoot(4)", "2"], + ["nthRoot(8, 3)", "2"], + ["nthRoot(5 * 7)", "nthRoot(5 * 7)"], + ["nthRoot(4, 3)", "nthRoot(4, 3)"], + ["nthRoot(12)", "2 * nthRoot(3, 2)"], + ["nthRoot(36)", "6"], + ["nthRoot(72)", "2 * 3 * nthRoot(2, 2)"], + ["nthRoot(x^2)", "x"], + ["nthRoot(x ^ 3)", "nthRoot(x ^ 3)"], + ["nthRoot(x^3, 3)", "x"], + ["nthRoot(-2)", "nthRoot(-2)"], + ["nthRoot(2 ^ x, x)", "2"], + ["nthRoot(x ^ (1/2), 1/2)", "x"], + ["nthRoot(2 * 2, 2)", "2"], + ["nthRoot(3 * 2 * 3 * 2, 2)", "2 * 3"], + ["nthRoot(36*x)", "2 * 3 * nthRoot(x, 2)"], + ["nthRoot(2 * 18 * x ^ 2, 2)", "2 * 3 * x"], + ["nthRoot(x * x, 2)", "x"], + ["nthRoot(x * x * (2 + 3), 2)", "x * nthRoot((2 + 3), 2)"], + ["nthRoot(64, 3)", "4"], + ["nthRoot(35937, 3)", "33"], + ]; + tests.forEach((t) => testNthRoot(t[0], t[1])); +}); + +function testNthRootSteps(exprString, outputList) { + const lastString = outputList[outputList.length - 1]; + TestUtil.testSubsteps(nthRoot, exprString, outputList, lastString); +} + +describe("nthRoot steps", function () { + const tests = [ + [ + "nthRoot(12)", + [ + "nthRoot(2 * 2 * 3)", + "nthRoot((2 * 2) * 3, 2)", + "nthRoot(2 ^ 2 * 3, 2)", + "nthRoot(2 ^ 2, 2) * nthRoot(3, 2)", + "2 * nthRoot(3, 2)", + ], + ], + [ + "nthRoot(72)", + [ + "nthRoot(2 * 2 * 2 * 3 * 3)", + "nthRoot((2 * 2) * 2 * (3 * 3), 2)", + "nthRoot(2 ^ 2 * 2 * 3 ^ 2, 2)", + "nthRoot(2 ^ 2, 2) * nthRoot(2, 2) * nthRoot(3 ^ 2, 2)", + "2 * nthRoot(2, 2) * 3", + "2 * 3 * nthRoot(2, 2)", + ], + ], + [ + "nthRoot(36*x)", + [ + "nthRoot(2 * 2 * 3 * 3 * x)", + "nthRoot((2 * 2) * (3 * 3) * x, 2)", + "nthRoot(2 ^ 2 * 3 ^ 2 * x, 2)", + "nthRoot(2 ^ 2, 2) * nthRoot(3 ^ 2, 2) * nthRoot(x, 2)", + "2 * 3 * nthRoot(x, 2)", + ], + ], + [ + "nthRoot(2 * 18 * x ^ 2, 2)", + [ + "nthRoot(2 * 2 * 3 * 3 * x ^ 2, 2)", + "nthRoot((2 * 2) * (3 * 3) * x ^ 2, 2)", + "nthRoot(2 ^ 2 * 3 ^ 2 * x ^ 2, 2)", + "nthRoot(2 ^ 2, 2) * nthRoot(3 ^ 2, 2) * nthRoot(x ^ 2, 2)", + "2 * 3 * x", + ], + ], + [ + "nthRoot(32, 3)", + [ + "nthRoot(2 * 2 * 2 * 2 * 2, 3)", + "nthRoot((2 * 2 * 2) * (2 * 2), 3)", + "nthRoot(2 ^ 3 * 2 ^ 2, 3)", + "nthRoot(2 ^ 3, 3) * nthRoot(2 ^ 2, 3)", + "2 * nthRoot(2 ^ 2, 3)", + ], + ], + [ + "nthRoot(32, 4)", + [ + "nthRoot(2 * 2 * 2 * 2 * 2, 4)", + "nthRoot((2 * 2 * 2 * 2) * 2, 4)", + "nthRoot(2 ^ 4 * 2, 4)", + "nthRoot(2 ^ 4, 4) * nthRoot(2, 4)", + "2 * nthRoot(2, 4)", + ], + ], + [ + "nthRoot(2 * 2 * 3 * 2, 3)", + [ + "nthRoot((2 * 2 * 2) * 3, 3)", + "nthRoot(2 ^ 3 * 3, 3)", + "nthRoot(2 ^ 3, 3) * nthRoot(3, 3)", + "2 * nthRoot(3, 3)", + ], + ], + ]; + tests.forEach((t) => testNthRootSteps(t[0], t[1])); +}); diff --git a/test/simplifyExpression/multiplyFractionsSearch/multiplyFractionsSearch.test.js b/test/simplifyExpression/multiplyFractionsSearch/multiplyFractionsSearch.test.js deleted file mode 100644 index b36b2d1b..00000000 --- a/test/simplifyExpression/multiplyFractionsSearch/multiplyFractionsSearch.test.js +++ /dev/null @@ -1,30 +0,0 @@ -const multiplyFractionsSearch = require('../../../lib/simplifyExpression//multiplyFractionsSearch'); - -const TestUtil = require('../../TestUtil'); - -function testMultiplyFractionsSearch(exprString, outputStr) { - TestUtil.testSimplification(multiplyFractionsSearch, exprString, outputStr); -} - -describe('multiplyFractions', function () { - const tests = [ - ['3 * 1/5 * 5/9', '(3 * 1 * 5) / (5 * 9)'], - ['3/7 * 10/11', '(3 * 10) / (7 * 11)'], - ['2 * 5/x', '(2 * 5) / x'], - ['2 * (5/x)', '(2 * 5) / x'], - ['(5/x) * (2/x)', '(5 * 2) / (x * x)'], - ['(5/x) * x', '(5x) / x'], - ['2x * 9/x', '(2x * 9) / x'], - ['-3/8 * 2/4', '(-3 * 2) / (8 * 4)'], - ['(-1/2) * 4/5', '(-1 * 4) / (2 * 5)'], - ['4 * (-1/x)', '(4 * -1) / x'], - ['x * 2y / x', '(x * 2y) / x'], - ['x/z * 1/2', '(x * 1) / (z * 2)'], - ['(6y / x) * 4x', '(6y * 4x) / x'], - ['2x * y / z * 10', '(2x * y * 10) / z'], - ['-(1/2) * (1/2)', '(-1 * 1) / (2 * 2)'], - ['x * -(1/x)', '(x * -1) / x'], - ['-(5/y) * -(x/y)', '(-5 * -x) / (y * y)'], - ]; - tests.forEach(t => testMultiplyFractionsSearch(t[0], t[1])); -}); diff --git a/test/simplifyExpression/multiplyFractionsSearch/multiplyFractionsSearch.test.ts b/test/simplifyExpression/multiplyFractionsSearch/multiplyFractionsSearch.test.ts new file mode 100644 index 00000000..f36546d0 --- /dev/null +++ b/test/simplifyExpression/multiplyFractionsSearch/multiplyFractionsSearch.test.ts @@ -0,0 +1,30 @@ +import { multiplyFractionsSearch } from "../../../lib/src/simplifyExpression//multiplyFractionsSearch"; + +import { TestUtil } from "../../TestUtil"; + +function testMultiplyFractionsSearch(exprString, outputStr) { + TestUtil.testSimplification(multiplyFractionsSearch, exprString, outputStr); +} + +describe("multiplyFractions", function () { + const tests = [ + ["3 * 1/5 * 5/9", "(3 * 1 * 5) / (5 * 9)"], + ["3/7 * 10/11", "(3 * 10) / (7 * 11)"], + ["2 * 5/x", "(2 * 5) / x"], + ["2 * (5/x)", "(2 * 5) / x"], + ["(5/x) * (2/x)", "(5 * 2) / (x * x)"], + ["(5/x) * x", "(5x) / x"], + ["2x * 9/x", "(2x * 9) / x"], + ["-3/8 * 2/4", "(-3 * 2) / (8 * 4)"], + ["(-1/2) * 4/5", "(-1 * 4) / (2 * 5)"], + ["4 * (-1/x)", "(4 * -1) / x"], + ["x * 2y / x", "(x * 2y) / x"], + ["x/z * 1/2", "(x * 1) / (z * 2)"], + ["(6y / x) * 4x", "(6y * 4x) / x"], + ["2x * y / z * 10", "(2x * y * 10) / z"], + ["-(1/2) * (1/2)", "(-1 * 1) / (2 * 2)"], + ["x * -(1/x)", "(x * -1) / x"], + ["-(5/y) * -(x/y)", "(-5 * -x) / (y * y)"], + ]; + tests.forEach((t) => testMultiplyFractionsSearch(t[0], t[1])); +}); diff --git a/test/simplifyExpression/oneStep.test.js b/test/simplifyExpression/oneStep.test.js deleted file mode 100644 index 8df6c754..00000000 --- a/test/simplifyExpression/oneStep.test.js +++ /dev/null @@ -1,97 +0,0 @@ -const assert = require('assert'); - -const print = require('../../lib/util/print'); - -const ChangeTypes = require('../../lib/ChangeTypes'); -const simplifyExpression = require('../../lib/simplifyExpression'); - -function testOneStep(exprStr, outputStr, debug=false) { - const steps = simplifyExpression(exprStr); - if (!steps.length) { - return exprStr; - } - const nodeStatus = steps[0]; - if (debug) { - if (!nodeStatus.changeType) { - throw Error('missing or bad change type'); - } - // eslint-disable-next-line - console.log(nodeStatus.changeType); - // eslint-disable-next-line - console.log(print.ascii(nodeStatus.newNode)); - } - it(exprStr + ' -> ' + outputStr, function () { - assert.deepEqual( - print.ascii(nodeStatus.newNode), - outputStr); - }); -} - -describe('arithmetic stepping', function() { - const tests = [ - ['(2+2)', '4'], - ['(2+2)*5', '4 * 5'], - ['5*(2+2)', '5 * 4'], - ['2*(2+2) + 2^3', '2 * 4 + 2^3'], - ['6*6', '36'], - ]; - tests.forEach(t => testOneStep(t[0], t[1])); -}); - -describe('adding symbols without breaking things', function() { - // nothing old breaks - const tests = [ - ['2+x', '2 + x'], - ['(2+2)*x', '4x'], - ['(2+2)*x+3', '4x + 3'], - ]; - tests.forEach(t => testOneStep(t[0], t[1])); -}); - -describe('collecting like terms within the context of the stepper', function() { - const tests = [ - ['2+x+7', 'x + 9'], // substeps not tested here -// ['2x^2 * y * x * y^3', '2 * x^3 * y^4'], // substeps not tested here - ]; - tests.forEach(t => testOneStep(t[0], t[1])); -}); - -describe('collects and combines like terms', function() { - const tests = [ - ['(x + x) + (x^2 + x^2)', '2x + (x^2 + x^2)'], // substeps not tested here - ['10 + (y^2 + y^2)', '10 + 2y^2'], // substeps not tested here - ['10y^2 + 1/2 y^2 + 3/2 y^2', '12y^2'], // substeps not tested here - ['x + y + y^2', 'x + y + y^2'], - ['2x^(2+1)', '2x^3'], - ]; - tests.forEach(t => testOneStep(t[0], t[1])); -}); - -describe('stepThrough returning no steps', function() { - it('12x^2 already simplified', function () { - assert.deepEqual( - simplifyExpression('12x^2'), - []); - }); - it('2*5x^2 + sqrt(5) has unsupported sqrt', function () { - assert.deepEqual( - simplifyExpression('2*5x^2 + sqrt(5)'), - []); - }); -}); - -describe('keeping parens in important places, on printing', function() { - testOneStep('5 + (3*6) + 2 / (x / y)', '5 + (3 * 6) + 2 * y / x'); - testOneStep('-(x + y) + 5+3', '8 - (x + y)'); -}); - -describe('fractions', function() { - testOneStep('2 + 5/2 + 3', '5 + 5/2'); // collect and combine without substeps -}); - -describe('simplifyDoubleUnaryMinus step actually happens', function () { - it('22 - (-7) -> 22 + 7', function() { - const steps = simplifyExpression('22 - (-7)'); - assert.equal(steps[0].changeType, ChangeTypes.RESOLVE_DOUBLE_MINUS); - }); -}); diff --git a/test/simplifyExpression/oneStep.test.ts b/test/simplifyExpression/oneStep.test.ts new file mode 100644 index 00000000..cc734b8b --- /dev/null +++ b/test/simplifyExpression/oneStep.test.ts @@ -0,0 +1,90 @@ +import { printAscii } from "../../lib/src/util/print"; + +import { ChangeTypes } from "../../lib/src/ChangeTypes"; +import { simplifyExpression } from "../../lib/src/simplifyExpression"; +import assert = require("assert"); + +function testOneStep(exprStr, outputStr, debug = false) { + const steps = simplifyExpression(exprStr); + if (!steps.length) { + return exprStr; + } + const nodeStatus = steps[0]; + if (debug) { + if (!nodeStatus.changeType) { + throw Error("missing or bad change type"); + } + // eslint-disable-next-line + console.log(nodeStatus.changeType); + // eslint-disable-next-line + console.log(printAscii(nodeStatus.newNode)); + } + it(exprStr + " -> " + outputStr, function () { + assert.deepEqual(printAscii(nodeStatus.newNode), outputStr); + }); +} + +describe("arithmetic stepping", function () { + const tests = [ + ["(2+2)", "4"], + ["(2+2)*5", "4 * 5"], + ["5*(2+2)", "5 * 4"], + ["2*(2+2) + 2^3", "2 * 4 + 2^3"], + ["6*6", "36"], + ]; + tests.forEach((t) => testOneStep(t[0], t[1])); +}); + +describe("adding symbols without breaking things", function () { + // nothing old breaks + const tests = [ + ["2+x", "2 + x"], + ["(2+2)*x", "4x"], + ["(2+2)*x+3", "4x + 3"], + ]; + tests.forEach((t) => testOneStep(t[0], t[1])); +}); + +describe("collecting like terms within the context of the stepper", function () { + const tests = [ + ["2+x+7", "x + 9"], // substeps not tested here + // ['2x^2 * y * x * y^3', '2 * x^3 * y^4'], // substeps not tested here + ]; + tests.forEach((t) => testOneStep(t[0], t[1])); +}); + +describe("collects and combines like terms", function () { + const tests = [ + ["(x + x) + (x^2 + x^2)", "2x + (x^2 + x^2)"], // substeps not tested here + ["10 + (y^2 + y^2)", "10 + 2y^2"], // substeps not tested here + ["10y^2 + 1/2 y^2 + 3/2 y^2", "12y^2"], // substeps not tested here + ["x + y + y^2", "x + y + y^2"], + ["2x^(2+1)", "2x^3"], + ]; + tests.forEach((t) => testOneStep(t[0], t[1])); +}); + +describe("stepThrough returning no steps", function () { + it("12x^2 already simplified", function () { + assert.deepEqual(simplifyExpression("12x^2"), []); + }); + it("2*5x^2 + sqrt(5) has unsupported sqrt", function () { + assert.deepEqual(simplifyExpression("2*5x^2 + sqrt(5)"), []); + }); +}); + +describe("keeping parens in important places, on printing", function () { + testOneStep("5 + (3*6) + 2 / (x / y)", "5 + (3 * 6) + 2 * y / x"); + testOneStep("-(x + y) + 5+3", "8 - (x + y)"); +}); + +describe("fractions", function () { + testOneStep("2 + 5/2 + 3", "5 + 5/2"); // collect and combine without substeps +}); + +describe("simplifyDoubleUnaryMinus step actually happens", function () { + it("22 - (-7) -> 22 + 7", function () { + const steps = simplifyExpression("22 - (-7)"); + assert.equal(steps[0].changeType, ChangeTypes.RESOLVE_DOUBLE_MINUS); + }); +}); diff --git a/test/simplifyExpression/simplify.test.js b/test/simplifyExpression/simplify.test.js deleted file mode 100644 index d6a30840..00000000 --- a/test/simplifyExpression/simplify.test.js +++ /dev/null @@ -1,195 +0,0 @@ -const assert = require('assert'); -const math = require('mathjs'); - -const print = require('../../lib/util/print'); - -const simplify = require('../../lib/simplifyExpression/simplify'); - -function testSimplify(exprStr, outputStr, debug=false) { - it(exprStr + ' -> ' + outputStr, function () { - assert.deepEqual( - print.ascii(simplify(math.parse(exprStr), debug)), - outputStr); - }); -} - -describe('simplify (arithmetic)', function () { - const tests = [ - ['(2+2)*5', '20'], - ['(8+(-4))*5', '20'], - ['5*(2+2)*10', '200'], - ['(2+(2)+7)', '11'], - ['(8-2) * 2^2 * (1+1) / (4 /2) / 5', '24/5'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('collects and combines like terms', function() { - const tests = [ - ['x^2 + 3x*(-4x) + 5x^3 + 3x^2 + 6', '5x^3 - 8x^2 + 6'], - ['2x^2 * y * x * y^3', '2x^3 * y^4'], - ['4y*3*5', '60y'], - ['(2x^2 - 4) + (4x^2 + 3)', '6x^2 - 1'], - ['(2x^1 + 4) + (4x^2 + 3)', '4x^2 + 2x + 7'], - ['y * 2x * 10', '20x * y'], - ['x^y * x^z', 'x^(y + z)'], - ['x^(3+y) + x^(3+y)+ 4', '2x^(3 + y) + 4'], - ['x^2 + 3x*(-4x) + 5x^3 + 3x^2 + 6', '5x^3 - 8x^2 + 6'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - - -describe('can simplify with division', function () { - const tests = [ - ['2 * 4 / 5 * 10 + 3', '19'], - ['2x * 5x / 2', '5x^2'], - ['2x * 4x / 5 * 10 + 3', '16x^2 + 3'], - ['2x * 4x / 2 / 4', 'x^2'], - ['2x * y / z * 10', '(20x * y) / z'], - ['2x * 4x / 5 * 10 + 3', '16x^2 + 3'], - ['2x/x', '2'], - ['2x/4/3', '1/6 x'], - ['((2+x)(3+x))/(2+x)', '3 + x'], - ['(20 * x) / (5 * (40 * y))', 'x / (10y)'], - ['400 * z / ((20 * x) / (5 * (40 * y)))', '(4000y * z) / x'], - ['20x / (40y)', 'x / (2y)'], - ['60x / (40y)', '3x / (2y)'], - ['4x / (2y)', '2x / y'] - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); - // TODO: factor the numerator to cancel out with denominator - // e.g. (x^2 - 3 + 2)/(x-2) -> (x-1) -}); - -describe('subtraction support', function() { - const tests = [ - ['-(-(2+3))', '5'], - ['-(-5)', '5'], - ['-(-(2+x))', '2 + x'], - ['-------5', '-5'], - ['--(-----5) + 6', '1'], - ['x^2 + 3 - x*x', '3'], - ['-(2*x) * -(2 + 2)', '8x'], - ['(x-4)-5', 'x - 9'], - ['5-x-4', '-x + 1'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('support for more * and ( that come from latex conversion', function () { - const tests = [ - ['(3*x)*(4*x)', '12x^2'], - ['(12*z^(2))/27', '4/9 z^2'], - ['x^2 - 12x^2 + 5x^2 - 7', '-6x^2 - 7'], - ['-(12 x ^ 2)', '-12x^2'] - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('distribution', function () { - const tests = [ - ['(3*x)*(4*x)', '12x^2'], - ['(3+x)*(4+x)*(x+5)', 'x^3 + 12x^2 + 47x + 60'], - ['-2x^2 * (3x - 4)', '-6x^3 + 8x^2'], - ['x^2 - x^2*(12 + 5x) - 7', '-5x^3 - 11x^2 - 7'], - ['(5+x)*(x+3)', 'x^2 + 8x + 15'], - ['(x-2)(x-4)', 'x^2 - 6x + 8'], - ['- x*y^4 (6x * y^2 + 5x*y - 3x)', - '-6x^2 * y^6 - 5x^2 * y^5 + 3x^2 * y^4'], - ['(nthRoot(x, 2))^2', 'x'], - ['(nthRoot(x, 2))^4', 'x^2'], - ['3 * (nthRoot(x, 2))^2', '3x'], - ['(nthRoot(x, 2))^6 * (nthRoot(x, 3))^3', 'x^4'], - ['(x - 2)^2', 'x^2 - 4x + 4'], - ['(3x + 5)^2', '9x^2 + 30x + 25'], - ['(2x + 3)^2','4x^2 + 12x + 9'], - ['(x + 3 + 4)^2', 'x^2 + 14x + 49'], - // TODO: ideally this can happen in one step - // the current substeps are (nthRoot(x^2, 2))^2 -> nthRoot(x^2, 2) * nthRoot(x^2, 2) - // -> x * x -> x - ['(nthRoot(x, 2) * nthRoot(x, 2))^2', 'x^2'], - // TODO: fix nthRoot to evaluate nthRoot(x^3, 2) - ['(nthRoot(x, 2))^3', 'nthRoot(x ^ 3, 2)'], - ['3 * nthRoot(x, 2) * (nthRoot(x, 2))^2', '3 * nthRoot(x ^ 3, 2)'], - // TODO: expand power for base with multiplication - //['(nthRoot(x, 2) * nthRoot(x, 3))^2', '(nthRoot(x, 2) * nthRoot(x, 3))^2'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('fractions', function() { - const tests = [ - ['5x + (1/2)x', '11/2 x'], - ['x + x/2', '3/2 x'], - ['1 + 1/2', '3/2'], - ['2 + 5/2 + 3', '15/2'], - ['9/18-5/18', '2/9'], - ['2(x+3)/3', '2x / 3 + 2'], - ['(2 / x) * x', '2'], - ['5/18 - 9/18', '-2/9'], - ['9/18', '1/2'], - ['x/(2/3) + 5', '3/2 x + 5'], - ['(2+x)/6', '1/3 + x / 6'] - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('floating point', function() { - testSimplify('1.983*10', '19.83'); -}); - -describe('cancelling out', function() { - const tests = [ - ['(x^3*y)/x^2 + 5', 'x * y + 5'], - ['(x^(2)+y^(2))/(5x-6x) + 5', '-x - y^2 / x + 5'], - ['( p ^ ( 2) + 1)/( p ^ ( 2) + 1)', '1'], - ['(-x)/(x)', '-1'], - ['(x)/(-x)', '-1'], - ['((2x^3 y^2)/(-x^2 y^5))^(-2)', '(-2x * y^-3)^-2'], - ['(1+2a)/a', '1 / a + 2'], - ['(x ^ 4 * y + -(x ^ 2 * y ^ 2)) / (-x ^ 2 * y)', '-x^2 + y'], - ['6 / (2x^2)', '3 / (x^2)'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('absolute value support', function() { - const tests = [ - ['(x^3*y)/x^2 + abs(-5)', 'x * y + 5'], - ['-6 + -5 - abs(-4) + -10 - 3 abs(-4)', '-37'], - ['5*abs((2+2))*10', '200'], - ['5x + (1/abs(-2))x', '11/2 x'], - ['abs(5/18-abs(9/-18))', '2/9'], - // handle parens around abs() - ['( abs( -3) )/(3)', '1'], - ['- abs( -40)', '-40'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('nthRoot support', function() { - const tests = [ - ['nthRoot(4x, 2)', '2 * nthRoot(x, 2)'], - ['2 * nthRoot(4x, 2)', '4 * nthRoot(x, 2)'], - ['(x^3*y)/x^2 + nthRoot(4x, 2)', 'x * y + 2 * nthRoot(x, 2)'], - ['2 + nthRoot(4)', '4'], - ['x * nthRoot(x^4, 2)', 'x^3'], - ['x * nthRoot(2 + 2, 3)', 'x * nthRoot(4, 3)'], - ['x * nthRoot((2 + 2) * 2, 3)', '2x'], - ['nthRoot(x * (2 + 3) * x, 2)', 'x * nthRoot(5, 2)'] - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('handles unnecessary parens at root level', function() { - const tests = [ - ['(x+(y))', 'x + y'], - ['((x+y) + ((z^3)))', 'x + y + z^3'], - ]; - tests.forEach(t => testSimplify(t[0], t[1], t[2])); -}); - -describe('keeping parens in important places, on printing', function() { - testSimplify('2 / (3x^2) + 5', '2 / (3x^2) + 5'); -}); diff --git a/test/simplifyExpression/simplify.test.ts b/test/simplifyExpression/simplify.test.ts new file mode 100644 index 00000000..67dddbe0 --- /dev/null +++ b/test/simplifyExpression/simplify.test.ts @@ -0,0 +1,225 @@ +import * as math from "mathjs"; + +import { simplify } from "../../lib/src/simplifyExpression/simplify"; +import { printAscii } from "../../lib/src/util/print"; +import assert = require("assert"); + +function testSimplify( + inputString: string, + expectedOutputString: string, + debug = false +) { + it(inputString + " -> " + expectedOutputString, () => { + assert.deepEqual( + printAscii(simplify(math.parse(inputString), debug)), + expectedOutputString + ); + }); +} + +describe("mixed cases", () => { + const tests = [ + ["x+x+y+y", "2x + 2y"], + ["((a^3+b)/c)/(1/c^2)", "a^3 * c + b * c"], // working but not 100% nice: a^3 * c + b * c + ["(a-1)*(a+1)*(a+2)", "a^3 + 2a^2 - a - 2"], + // ['(z/6-3/z)^2', '1/36 z^2 - 1 + 9 / (z^2)'], // NOT working, it gets the wrong solution: 1/36 z^2 - 7/2 + 9 / (z^2) ... + // ['7a^2+28a+28', '7*(a+2)^2'], // NOT working, it gets the wrong solution: 1/36 z^2 - 7/2 + 9 / (z^2) ... + + // Distributivgesetz + ["(3a-2)(3a+2)", "9a^2 - 4"], + + // Binom + // ['(x+y)^2+(x-y)^2+(-x+y)^2+(-x-y)^2', '4x^2 + 4y^2'], // NOT working, yields: 4x^2 + 4y^2 + x * y + y * x + x * -y - y * x - x * y + y * -x + x * y + x * y ... WTF => can't handle multiple variables well + ["(2-x)^2-(2-x)(2+x)+(2+x)^2", "3x^2 + 4"], + + // ["(1/x+1/y)/(1/x-1/y)", "y+x/y-x"] not working + // ["(7x/8b) / (14x/10b^2)", "5b/8"] not working + // ["(a^2+2*a+1)/(a+1)", "a+1"] not working + // ["((a+b)/b)*(a/(-a-b))", "-a/b"], not working, yields a^2 / (b * -a - b^2) + a / (-a - b) + // ["(x^2+6x+9)/(5y-5) * (10-10y)/(x^2+5x+6)", "(-2x-6)/(x+2)"], // not working, yields: very long thing... + // ["(1/x+1/y)/(1/x-1/y)", "y+x/y-x"] not working + // ["(7x/8b) / (14x/10b^2)", "5b/8"] not working + // ["(a^2+2*a+1)/(a+1)", "a+1"] not working + // ["((w^2+w^3)/(w+1))/w^3", "1w"], // not working, yields: w^-1 / (w + 1) + 1 / (w + 1) + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("simplify (arithmetic)", () => { + const tests = [ + ["(2+2)*5", "20"], + ["(8+(-4))*5", "20"], + ["5*(2+2)*10", "200"], + ["(2+(2)+7)", "11"], + ["(8-2) * 2^2 * (1+1) / (4 /2) / 5", "24/5"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("collects and combines like terms", () => { + const tests = [ + ["x^2 + 3x*(-4x) + 5x^3 + 3x^2 + 6", "5x^3 - 8x^2 + 6"], + ["2x^2 * y * x * y^3", "2x^3 * y^4"], + ["4y*3*5", "60y"], + ["(2x^2 - 4) + (4x^2 + 3)", "6x^2 - 1"], + ["(2x^1 + 4) + (4x^2 + 3)", "4x^2 + 2x + 7"], + ["y * 2x * 10", "20x * y"], + ["x^y * x^z", "x^(y + z)"], + ["x^(3+y) + x^(3+y)+ 4", "2x^(3 + y) + 4"], + ["x^2 + 3x*(-4x) + 5x^3 + 3x^2 + 6", "5x^3 - 8x^2 + 6"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("can simplify with division", () => { + const tests = [ + ["2 * 4 / 5 * 10 + 3", "19"], + ["2x * 5x / 2", "5x^2"], + ["2x * 4x / 5 * 10 + 3", "16x^2 + 3"], + ["2x * 4x / 2 / 4", "x^2"], + ["2x * y / z * 10", "(20x * y) / z"], + ["2x * 4x / 5 * 10 + 3", "16x^2 + 3"], + ["2x/x", "2"], + ["2x/4/3", "1/6 x"], + ["((2+x)(3+x))/(2+x)", "3 + x"], + ["(20 * x) / (5 * (40 * y))", "x / (10y)"], + ["400 * z / ((20 * x) / (5 * (40 * y)))", "(4000y * z) / x"], + ["20x / (40y)", "x / (2y)"], + ["60x / (40y)", "3x / (2y)"], + ["4x / (2y)", "2x / y"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); + // TODO: factor the numerator to cancel out with denominator + // e.g. (x^2 - 3 + 2)/(x-2) -> (x-1) +}); + +describe("subtraction support", () => { + const tests = [ + ["-(-(2+3))", "5"], + ["-(-5)", "5"], + ["-(-(2+x))", "2 + x"], + ["-------5", "-5"], + ["--(-----5) + 6", "1"], + ["x^2 + 3 - x*x", "3"], + ["-(2*x) * -(2 + 2)", "8x"], + ["(x-4)-5", "x - 9"], + ["5-x-4", "-x + 1"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("support for more * and ( that come from latex conversion", () => { + const tests = [ + ["(3*x)*(4*x)", "12x^2"], + ["(12*z^(2))/27", "4/9 z^2"], + ["x^2 - 12x^2 + 5x^2 - 7", "-6x^2 - 7"], + ["-(12 x ^ 2)", "-12x^2"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("distribution", () => { + const tests = [ + ["(3*x)*(4*x)", "12x^2"], + ["(3+x)*(4+x)*(x+5)", "x^3 + 12x^2 + 47x + 60"], + ["-2x^2 * (3x - 4)", "-6x^3 + 8x^2"], + ["x^2 - x^2*(12 + 5x) - 7", "-5x^3 - 11x^2 - 7"], + ["(5+x)*(x+3)", "x^2 + 8x + 15"], + ["(x-2)(x-4)", "x^2 - 6x + 8"], + ["- x*y^4 (6x * y^2 + 5x*y - 3x)", "-6x^2 * y^6 - 5x^2 * y^5 + 3x^2 * y^4"], + ["(nthRoot(x, 2))^2", "x"], + ["(nthRoot(x, 2))^4", "x^2"], + ["3 * (nthRoot(x, 2))^2", "3x"], + ["(nthRoot(x, 2))^6 * (nthRoot(x, 3))^3", "x^4"], + ["(x - 2)^2", "x^2 - 4x + 4"], + ["(3x + 5)^2", "9x^2 + 30x + 25"], + ["(2x + 3)^2", "4x^2 + 12x + 9"], + ["(x + 3 + 4)^2", "x^2 + 14x + 49"], + // TODO: ideally this can happen in one step + // the current substeps are (nthRoot(x^2, 2))^2 -> nthRoot(x^2, 2) * nthRoot(x^2, 2) + // -> x * x -> x + ["(nthRoot(x, 2) * nthRoot(x, 2))^2", "x^2"], + // TODO: fix nthRoot to evaluate nthRoot(x^3, 2) + ["(nthRoot(x, 2))^3", "nthRoot(x ^ 3, 2)"], + ["3 * nthRoot(x, 2) * (nthRoot(x, 2))^2", "3 * nthRoot(x ^ 3, 2)"], + // TODO: expand power for base with multiplication + //['(nthRoot(x, 2) * nthRoot(x, 3))^2', '(nthRoot(x, 2) * nthRoot(x, 3))^2'], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("fractions", () => { + const tests = [ + ["5x + (1/2)x", "11/2 x"], + ["x + x/2", "3/2 x"], + ["1 + 1/2", "3/2"], + ["2 + 5/2 + 3", "15/2"], + ["9/18-5/18", "2/9"], + ["2(x+3)/3", "2x / 3 + 2"], + ["(2 / x) * x", "2"], + ["5/18 - 9/18", "-2/9"], + ["9/18", "1/2"], + ["x/(2/3) + 5", "3/2 x + 5"], + ["(2+x)/6", "1/3 + x / 6"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("floating point", () => { + testSimplify("1.983*10", "19.83"); +}); + +describe("cancelling out", () => { + const tests = [ + ["(x^3*y)/x^2 + 5", "x * y + 5"], + ["(x^(2)+y^(2))/(5x-6x) + 5", "-x - y^2 / x + 5"], + ["( p ^ ( 2) + 1)/( p ^ ( 2) + 1)", "1"], + ["(-x)/(x)", "-1"], + ["(x)/(-x)", "-1"], + ["((2x^3 y^2)/(-x^2 y^5))^(-2)", "(-2x * y^-3)^-2"], + ["(1+2a)/a", "1 / a + 2"], + ["(x ^ 4 * y + -(x ^ 2 * y ^ 2)) / (-x ^ 2 * y)", "-x^2 + y"], + ["6 / (2x^2)", "3 / (x^2)"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("absolute value support", () => { + const tests = [ + ["(x^3*y)/x^2 + abs(-5)", "x * y + 5"], + ["-6 + -5 - abs(-4) + -10 - 3 abs(-4)", "-37"], + ["5*abs((2+2))*10", "200"], + ["5x + (1/abs(-2))x", "11/2 x"], + ["abs(5/18-abs(9/-18))", "2/9"], + // handle parens around abs() + ["( abs( -3) )/(3)", "1"], + ["- abs( -40)", "-40"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("nthRoot support", () => { + const tests = [ + ["nthRoot(4x, 2)", "2 * nthRoot(x, 2)"], + ["2 * nthRoot(4x, 2)", "4 * nthRoot(x, 2)"], + ["(x^3*y)/x^2 + nthRoot(4x, 2)", "x * y + 2 * nthRoot(x, 2)"], + ["2 + nthRoot(4)", "4"], + ["x * nthRoot(x^4, 2)", "x^3"], + ["x * nthRoot(2 + 2, 3)", "x * nthRoot(4, 3)"], + ["x * nthRoot((2 + 2) * 2, 3)", "2x"], + ["nthRoot(x * (2 + 3) * x, 2)", "x * nthRoot(5, 2)"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("handles unnecessary parens at root level", () => { + const tests = [ + ["(x+(y))", "x + y"], + ["((x+y) + ((z^3)))", "x + y + z^3"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); + +describe("keeping parens in important places, on printing", () => { + testSimplify("2 / (3x^2) + 5", "2 / (3x^2) + 5"); +}); diff --git a/test/solveEquation/solveEquation.test.js b/test/solveEquation/solveEquation.test.js deleted file mode 100644 index c92d304a..00000000 --- a/test/solveEquation/solveEquation.test.js +++ /dev/null @@ -1,180 +0,0 @@ -const assert = require('assert'); - -const ChangeTypes = require('../../lib/ChangeTypes'); -const solveEquation = require('../../lib/solveEquation'); - -const NO_STEPS = 'no-steps'; - -function testSolve(equationString, outputStr, debug=false) { - const steps = solveEquation(equationString, debug); - let lastStep; - if (steps.length === 0) { - lastStep = NO_STEPS; - } - else { - lastStep = steps[steps.length -1].newEquation.ascii(); - } - it(equationString + ' -> ' + outputStr, (done) => { - assert.equal(lastStep, outputStr); - done(); - }); -} - -describe('solveEquation for =', function () { - const tests = [ - // can't solve this because two symbols: g and x -- so there's no steps - ['g *( x ) = ( x - 4) ^ ( 2) - 3', NO_STEPS], - // can't solve this because we don't deal with inequalities yet - // See: https://www.cymath.com/answer.php?q=(%20x%20)%2F(%202x%20%2B%207)%20%3E%3D%204 - ['( x )/( 2x + 7) >= 4', NO_STEPS], - ['y - x - 2 = 3*2', 'y = 8 + x'], - ['2y - x - 2 = x', 'y = x + 1'], - ['x = 1', NO_STEPS], - ['2 = x', 'x = 2'], - ['2 + -3 = x', 'x = -1'], - ['x + 3 = 4', 'x = 1'], - ['2x - 3 = 0', 'x = 3/2'], - ['x/3 - 2 = -1', 'x = 3'], - ['5x/2 + 2 = 3x/2 + 10', 'x = 8'], - ['2x - 1 = -x', 'x = 1/3'], - ['2 - x = -4 + x', 'x = 3'], - ['2x/3 = 2', 'x = 3'], - ['2x - 3 = x', 'x = 3'], - ['8 - 2a = a + 3 - 1', 'a = 2'], - ['2 - x = 4', 'x = -2'], - ['2 - 4x = x', 'x = 2/5'], - ['9x + 4 - 3 = 2x', 'x = -1/7'], - ['9x + 4 - 3 = -2x', 'x = -1/11'], - ['5x + (1/2)x = 27 ', 'x = 54/11'], - ['2x/3 = 2x - 4 ', 'x = 3'], - ['(-2/3)x + 3/7 = 1/2', 'x = -3/28'], - ['-9/4v + 4/5 = 7/8 ', 'v = -1/30'], - // TODO: update test once we have root support - ['x^2 - 2 = 0', 'x^2 = 2'], - ['x/(2/3) = 1', 'x = 2/3'], - ['(x+1)/3 = 4', 'x = 11'], - ['2(x+3)/3 = 2', 'x = 0'], - ['- q - 4.36= ( 2.2q )/( 1.8)', 'q = -1.962'], - ['5x^2 - 5x - 30 = 0', 'x = [-2, 3]'], - ['x^2 + 3x + 2 = 0', 'x = [-1, -2]'], - ['x^2 - x = 0', 'x = [0, 1]'], - ['x^2 + 2x - 15 = 0', 'x = [3, -5]'], - ['x^2 + 2x = 0', 'x = [0, -2]'], - ['x^2 - 4 = 0', 'x = [-2, 2]'], - // Perfect square - ['x^2 + 2x + 1 = 0', 'x = [-1, -1]'], - ['x^2 + 4x + 4 = 0', 'x = [-2, -2]'], - ['x^2 - 6x + 9 = 0', 'x = [3, 3]'], - ['(x + 4)^2 = 0', 'x = [-4, -4]'], - ['(x - 5)^2 = 0', 'x = [5, 5]'], - // Difference of squares - ['4x^2 - 81 = 0', 'x = [-9 / 2, 9 / 2]'], - ['x^2 - 9 = 0', 'x = [-3, 3]'], - ['16y^2 - 25 = 0', 'y = [-5 / 4, 5 / 4]'], - // Some weird edge cases (we only support a leading term with coeff 1) - ['x * x + 12x + 36 = 0', 'x = [-6, -6]'], - ['x * x - 2x + 1 = 0', 'x = [1, 1]'], - ['0 = x^2 + 3x + 2', 'x = [-1, -2]'], - ['0 = x * x + 3x + 2', 'x = [-1, -2]'], - ['x * x + (x + x) + 1 = 0', 'x = [-1, -1]'], - ['0 = x * x + (x + x) + 1', 'x = [-1, -1]'], - ['(x^3 / x) + (3x - x) + 1 = 0', 'x = [-1, -1]'], - ['0 = (x^3 / x) + (3x - x) + 1', 'x = [-1, -1]'], - // Solve for roots before expanding - ['2^7 (x + 2) = 0', 'x = -2'], - ['(x + y) (x + 2) = 0', 'x = [-y, -2]'], - ['(33 + 89) (x - 99) = 0', 'x = 99'], - ['(x - 1)(x - 5)(x + 5) = 0', 'x = [1, 5, -5]'], - ['x^2 (x - 5)^2 = 0', 'x = [0, 0, 5, 5]'], - ['x^2 = 0', 'x = [0, 0]'], - ['x^(2) = 0', 'x = [0, 0]'], - ['(x+2)^2 -x^2 = 4(x+1)', '4 = 4'], - ['2/x = 1', 'x = 2'], - ['2/(4x) = 1', 'x = 1/2'], - ['2/(8 - 4x) = 1/2', 'x = 1'], - ['2/(1 + 1 + 4x) = 1/3', 'x = 1'], - ['(3 + x) / (x^2 + 3) = 1', 'x = [0, 1]'], - ['6/x + 8/(2x) = 10', 'x = 1'], - ['(x+1)=4', 'x = 3'], - ['((x)/(4))=4', 'x = 16'] - // TODO: fix these cases, fail because lack of factoring support, for complex #s, - // for taking the sqrt of both sides, etc - // ['(x + y) (y + 2) = 0', 'y = -y'], - // ['((x-2)^2) = 0', 'NO_STEPS'], - // ['x * x (x - 5)^2 = 0', 'NO_STEPS'], - // ['x^6 - x', NO_STEPS], - // ['4x^2 - 25y^2', ''], - // ['(x^2 + 2x + 1) (x^2 + 3x + 2) = 0', ''], - // ['(2x^2 - 1)(x^2 - 5)(x^2 + 5) = 0', ''], - // ['(-x ^ 2 - 4x + 2)(-3x^2 - 6x + 3) = 0', ''], - // ['x^2 = -2x - 1', 'x = -1'], - // TODO: figure out what to do about errors from rounding midway through - // this gives us 6.3995 when it should actually be 6.4 :( - // ['x - 3.4= ( x - 2.5)/( 1.3)', 'x = 6.4'] - ]; - tests.forEach(t => testSolve(t[0], t[1], t[2])); -}); - -describe('solveEquation for non = comparators', function() { - const tests = [ - ['x + 2 > 3', 'x > 1'], - ['2x < 6', 'x < 3'], - ['-x > 1', 'x < -1'], - ['2 - x < 3', 'x > -1'], - ['9.5j / 6+ 5.5j >= 3( 5j - 2)', 'j <= 0.7579'] - ]; - tests.forEach(t => testSolve(t[0], t[1], t[2])); -}); - -function testSolveConstantEquation(equationString, expectedChange, debug=false) { - const steps = solveEquation(equationString, debug); - const actualChange = steps[steps.length -1].changeType; - it(equationString + ' -> ' + expectedChange, (done) => { - assert.equal(actualChange, expectedChange); - done(); - }); -} - -describe('constant comparison support', function () { - const tests = [ - ['1 = 2', ChangeTypes.STATEMENT_IS_FALSE], - ['3 + 5 = 8', ChangeTypes.STATEMENT_IS_TRUE], - ['1 = 2', ChangeTypes.STATEMENT_IS_FALSE], - ['2 - 3 = 5', ChangeTypes.STATEMENT_IS_FALSE], - ['2 > 1', ChangeTypes.STATEMENT_IS_TRUE], - ['2/3 > 1/3', ChangeTypes.STATEMENT_IS_TRUE], - ['1 > 2', ChangeTypes.STATEMENT_IS_FALSE], - ['1/3 > 2/3', ChangeTypes.STATEMENT_IS_FALSE], - ['1 >= 1', ChangeTypes.STATEMENT_IS_TRUE], - ['2 >= 1', ChangeTypes.STATEMENT_IS_TRUE], - ['1 >= 2', ChangeTypes.STATEMENT_IS_FALSE], - ['2 < 1', ChangeTypes.STATEMENT_IS_FALSE], - ['2/3 < 1/3', ChangeTypes.STATEMENT_IS_FALSE], - ['1 < 2', ChangeTypes.STATEMENT_IS_TRUE], - ['1/3 < 2/3', ChangeTypes.STATEMENT_IS_TRUE], - ['1 <= 1', ChangeTypes.STATEMENT_IS_TRUE], - ['2 <= 1', ChangeTypes.STATEMENT_IS_FALSE], - ['1 <= 2', ChangeTypes.STATEMENT_IS_TRUE], - ['( 1) = ( 14)', ChangeTypes.STATEMENT_IS_FALSE], - // TODO: when we support fancy exponent and sqrt things - // ['(1/64)^(-5/6) = 32', ChangeTypes.STATEMENT_IS_TRUE], - // With variables that cancel - ['( r )/( ( r ) ) = ( 1)/( 10)', ChangeTypes.STATEMENT_IS_FALSE], - ['5 + (x - 5) = x', ChangeTypes.STATEMENT_IS_TRUE], - ['4x - 4= 4x', ChangeTypes.STATEMENT_IS_FALSE], - ]; - tests.forEach(t => testSolveConstantEquation(t[0], t[1], t[2])); -}); - -function testEquationError(equationString, debug=false) { - it(equationString + ' throws error', (done) => { - assert.throws(() => solveEquation(equationString, debug),Error); - done(); - }); -} - -describe('solveEquation errors', function() { - const tests = [ - ]; - tests.forEach(t => testEquationError(t[0], t[1])); -}); diff --git a/test/solveEquation/solveEquation.test.ts b/test/solveEquation/solveEquation.test.ts new file mode 100644 index 00000000..dc1ec75f --- /dev/null +++ b/test/solveEquation/solveEquation.test.ts @@ -0,0 +1,200 @@ +import { ChangeTypes } from "../../lib/src/ChangeTypes"; +import { solveEquation } from "../../lib/src/solveEquation"; +import assert = require("assert"); + +const NO_STEPS = "no-steps"; + +function testSolve(equationString, outputStr, debug = false) { + const steps = solveEquation(equationString, debug); + let lastStep; + if (steps.length === 0) { + lastStep = NO_STEPS; + } else { + lastStep = steps[steps.length - 1].newEquation.ascii(); + } + it(equationString + " -> " + outputStr, (done) => { + assert.equal(lastStep, outputStr); + done(); + }); +} + +describe("mixed cases", () => { + const tests = [ + ["3(x+2)=12", "x = 2"], + ["5x=20+5", "x = 5"], + + // binom + // ['x^2+4=4x', 'x=2'], NOT working, no steps + ]; + tests.forEach((t) => testSolve(t[0], t[1])); +}); + +describe("solveEquation for =", function () { + const tests = [ + // can't solve this because two symbols: g and x -- so there's no steps + ["g *( x ) = ( x - 4) ^ ( 2) - 3", NO_STEPS], + + // can't solve this because we don't deal with inequalities yet + // See: https://www.cymath.com/answer.php?q=(%20x%20)%2F(%202x%20%2B%207)%20%3E%3D%204 + ["( x )/( 2x + 7) >= 4", NO_STEPS], + + ["y - x - 2 = 3*2", "y = 8 + x"], + ["2y - x - 2 = x", "y = x + 1"], + ["x = 1", NO_STEPS], + ["2 = x", "x = 2"], + ["2 + -3 = x", "x = -1"], + ["x + 3 = 4", "x = 1"], + ["2x - 3 = 0", "x = 3/2"], + ["x/3 - 2 = -1", "x = 3"], + ["5x/2 + 2 = 3x/2 + 10", "x = 8"], + ["2x - 1 = -x", "x = 1/3"], + ["2 - x = -4 + x", "x = 3"], + ["2x/3 = 2", "x = 3"], + ["2x - 3 = x", "x = 3"], + ["8 - 2a = a + 3 - 1", "a = 2"], + ["2 - x = 4", "x = -2"], + ["2 - 4x = x", "x = 2/5"], + ["9x + 4 - 3 = 2x", "x = -1/7"], + ["9x + 4 - 3 = -2x", "x = -1/11"], + ["5x + (1/2)x = 27 ", "x = 54/11"], + ["2x/3 = 2x - 4 ", "x = 3"], + ["(-2/3)x + 3/7 = 1/2", "x = -3/28"], + ["-9/4v + 4/5 = 7/8 ", "v = -1/30"], + + // TODO: update test once we have root support + ["x^2 - 2 = 0", "x^2 = 2"], + ["x/(2/3) = 1", "x = 2/3"], + ["(x+1)/3 = 4", "x = 11"], + ["2(x+3)/3 = 2", "x = 0"], + ["- q - 4.36= ( 2.2q )/( 1.8)", "q = -1.962"], + ["5x^2 - 5x - 30 = 0", "x = [-2, 3]"], + ["x^2 + 3x + 2 = 0", "x = [-1, -2]"], + ["x^2 - x = 0", "x = [0, 1]"], + ["x^2 + 2x - 15 = 0", "x = [3, -5]"], + ["x^2 + 2x = 0", "x = [0, -2]"], + ["x^2 - 4 = 0", "x = [-2, 2]"], + + // Perfect square + ["x^2 + 2x + 1 = 0", "x = [-1, -1]"], + ["x^2 + 4x + 4 = 0", "x = [-2, -2]"], + ["x^2 - 6x + 9 = 0", "x = [3, 3]"], + ["(x + 4)^2 = 0", "x = [-4, -4]"], + ["(x - 5)^2 = 0", "x = [5, 5]"], + + // Difference of squares + ["4x^2 - 81 = 0", "x = [-9 / 2, 9 / 2]"], + ["x^2 - 9 = 0", "x = [-3, 3]"], + ["16y^2 - 25 = 0", "y = [-5 / 4, 5 / 4]"], + + // Some weird edge cases (we only support a leading term with coeff 1) + ["x * x + 12x + 36 = 0", "x = [-6, -6]"], + ["x * x - 2x + 1 = 0", "x = [1, 1]"], + ["0 = x^2 + 3x + 2", "x = [-1, -2]"], + ["0 = x * x + 3x + 2", "x = [-1, -2]"], + ["x * x + (x + x) + 1 = 0", "x = [-1, -1]"], + ["0 = x * x + (x + x) + 1", "x = [-1, -1]"], + ["(x^3 / x) + (3x - x) + 1 = 0", "x = [-1, -1]"], + ["0 = (x^3 / x) + (3x - x) + 1", "x = [-1, -1]"], + + // Solve for roots before expanding + ["2^7 (x + 2) = 0", "x = -2"], + ["(x + y) (x + 2) = 0", "x = [-y, -2]"], + ["(33 + 89) (x - 99) = 0", "x = 99"], + ["(x - 1)(x - 5)(x + 5) = 0", "x = [1, 5, -5]"], + ["x^2 (x - 5)^2 = 0", "x = [0, 0, 5, 5]"], + ["x^2 = 0", "x = [0, 0]"], + ["x^(2) = 0", "x = [0, 0]"], + ["(x+2)^2 -x^2 = 4(x+1)", "4 = 4"], + ["2/x = 1", "x = 2"], + ["2/(4x) = 1", "x = 1/2"], + ["2/(8 - 4x) = 1/2", "x = 1"], + ["2/(1 + 1 + 4x) = 1/3", "x = 1"], + ["(3 + x) / (x^2 + 3) = 1", "x = [0, 1]"], + ["6/x + 8/(2x) = 10", "x = 1"], + ["(x+1)=4", "x = 3"], + ["((x)/(4))=4", "x = 16"], + + // TODO: fix these cases, fail because lack of factoring support, for complex #s, + // for taking the sqrt of both sides, etc + // ['(x + y) (y + 2) = 0', 'y = -y'], + // ['((x-2)^2) = 0', 'NO_STEPS'], + // ['x * x (x - 5)^2 = 0', 'NO_STEPS'], + // ['x^6 - x', NO_STEPS], + // ['4x^2 - 25y^2', ''], + // ['(x^2 + 2x + 1) (x^2 + 3x + 2) = 0', ''], + // ['(2x^2 - 1)(x^2 - 5)(x^2 + 5) = 0', ''], + // ['(-x ^ 2 - 4x + 2)(-3x^2 - 6x + 3) = 0', ''], + // ['x^2 = -2x - 1', 'x = -1'], + // TODO: figure out what to do about errors from rounding midway through + // this gives us 6.3995 when it should actually be 6.4 :( + // ['x - 3.4= ( x - 2.5)/( 1.3)', 'x = 6.4'] + ]; + tests.forEach((t) => testSolve(t[0], t[1])); +}); + +describe("solveEquation for non = comparators", function () { + const tests = [ + ["x + 2 > 3", "x > 1"], + ["2x < 6", "x < 3"], + ["-x > 1", "x < -1"], + ["2 - x < 3", "x > -1"], + ["9.5j / 6+ 5.5j >= 3( 5j - 2)", "j <= 0.7579"], + ]; + tests.forEach((t) => testSolve(t[0], t[1])); +}); + +function testSolveConstantEquation( + equationString, + expectedChange, + debug = false +) { + const steps = solveEquation(equationString, debug); + const actualChange = steps[steps.length - 1].changeType; + it(equationString + " -> " + expectedChange, (done) => { + assert.equal(actualChange, expectedChange); + done(); + }); +} + +describe("constant comparison support", function () { + const tests = [ + ["1 = 2", ChangeTypes.STATEMENT_IS_FALSE], + ["3 + 5 = 8", ChangeTypes.STATEMENT_IS_TRUE], + ["1 = 2", ChangeTypes.STATEMENT_IS_FALSE], + ["2 - 3 = 5", ChangeTypes.STATEMENT_IS_FALSE], + ["2 > 1", ChangeTypes.STATEMENT_IS_TRUE], + ["2/3 > 1/3", ChangeTypes.STATEMENT_IS_TRUE], + ["1 > 2", ChangeTypes.STATEMENT_IS_FALSE], + ["1/3 > 2/3", ChangeTypes.STATEMENT_IS_FALSE], + ["1 >= 1", ChangeTypes.STATEMENT_IS_TRUE], + ["2 >= 1", ChangeTypes.STATEMENT_IS_TRUE], + ["1 >= 2", ChangeTypes.STATEMENT_IS_FALSE], + ["2 < 1", ChangeTypes.STATEMENT_IS_FALSE], + ["2/3 < 1/3", ChangeTypes.STATEMENT_IS_FALSE], + ["1 < 2", ChangeTypes.STATEMENT_IS_TRUE], + ["1/3 < 2/3", ChangeTypes.STATEMENT_IS_TRUE], + ["1 <= 1", ChangeTypes.STATEMENT_IS_TRUE], + ["2 <= 1", ChangeTypes.STATEMENT_IS_FALSE], + ["1 <= 2", ChangeTypes.STATEMENT_IS_TRUE], + ["( 1) = ( 14)", ChangeTypes.STATEMENT_IS_FALSE], + // TODO: when we support fancy exponent and sqrt things + // ['(1/64)^(-5/6) = 32', ChangeTypes.STATEMENT_IS_TRUE], + // With variables that cancel + ["( r )/( ( r ) ) = ( 1)/( 10)", ChangeTypes.STATEMENT_IS_FALSE], + ["5 + (x - 5) = x", ChangeTypes.STATEMENT_IS_TRUE], + ["4x - 4= 4x", ChangeTypes.STATEMENT_IS_FALSE], + ]; + tests.forEach((t) => testSolveConstantEquation(t[0], t[1])); +}); + +function testEquationError(equationString, debug = false) { + it(equationString + " throws error", (done) => { + assert.throws(() => solveEquation(equationString, debug), Error); + done(); + }); +} + +describe("solveEquation errors", function () { + const tests = []; + tests.forEach((t) => testEquationError(t[0], t[1])); +}); diff --git a/test/syntax/syntax.test.ts b/test/syntax/syntax.test.ts new file mode 100644 index 00000000..10025e52 --- /dev/null +++ b/test/syntax/syntax.test.ts @@ -0,0 +1,9 @@ +import { testSimplify } from "../simplifyExpression/basicsSearch/testSimplify.test"; + +describe("test understanding of syntax", () => { + const tests = [ + // ["fraction(1,2)+fraction(1,2)", "1"], not working yet + ["1/2+1/2", "1"], + ]; + tests.forEach((t) => testSimplify(t[0], t[1])); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 00000000..753b4613 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "declaration": true, + "outDir": "./dist", + "sourceMap": true + }, + "typeRoots": [ + "../node_modules" + ], + "types": [ + "jasmine" + ], + "include": [ + "./**/*.ts", + "../lib/src/**/*.ts" + ] +} diff --git a/test/util/Util.test.js b/test/util/Util.test.js deleted file mode 100644 index a1706f84..00000000 --- a/test/util/Util.test.js +++ /dev/null @@ -1,22 +0,0 @@ -const assert = require('assert'); - -const Util = require('../../lib/util/Util'); - -describe('appendToArrayInObject', function () { - it('creates empty array', function () { - const object = {}; - Util.appendToArrayInObject(object, 'key', 'value'); - assert.deepEqual( - object, - {'key': ['value']} - ); - }); - it('appends to array if it exists', function () { - const object = {'key': ['old_value']}; - Util.appendToArrayInObject(object, 'key', 'new_value'); - assert.deepEqual( - object, - {'key': ['old_value', 'new_value']} - ); - }); -}); diff --git a/test/util/flattenOperands.test.js b/test/util/flattenOperands.test.js deleted file mode 100644 index 63a554ba..00000000 --- a/test/util/flattenOperands.test.js +++ /dev/null @@ -1,109 +0,0 @@ -const assert = require('assert'); -const math = require('mathjs'); - -const print = require('../../lib/util/print'); - -const Node = require('../../lib/node'); -const TestUtil = require('../TestUtil'); - -function testFlatten(exprStr, afterNode, debug=false) { - const flattened = TestUtil.parseAndFlatten(exprStr); - if (debug) { - // eslint-disable-next-line - console.log(print.ascii(flattened)); - } - TestUtil.removeComments(flattened); - TestUtil.removeComments(afterNode); - it(print.ascii(flattened), function() { - assert.deepEqual(flattened, afterNode); - }); -} - -// to create nodes, for testing -const opNode = Node.Creator.operator; -const constNode = Node.Creator.constant; -const symbolNode = Node.Creator.symbol; -const parenNode = Node.Creator.parenthesis; - -describe('flattens + and *', function () { - const tests = [ - ['2+2', math.parse('2+2')], - ['2+2+7', opNode('+', [constNode(2), constNode(2), constNode(7)])], - ['9*8*6+3+4', - opNode('+', [ - opNode('*', [constNode(9), constNode(8), constNode(6)]), - constNode(3), - constNode(4)])], - ['5*(2+3+2)*10', - opNode('*', [ - constNode(5), - parenNode(opNode('+', [constNode(2), constNode(3),constNode(2)])), - constNode(10)])], - // keeps the polynomial term - ['9x*8*6+3+4', - opNode('+', [ - opNode('*', [math.parse('9x'), constNode(8), constNode(6)]), - constNode(3), - constNode(4)])], - ['9x*8*6+3y^2+4', - opNode('+', [ - opNode('*', [math.parse('9x'), constNode(8), constNode(6)]), - math.parse('3y^2'), - constNode(4)])], - // doesn't flatten - ['2 x ^ (2 + 1) * y', math.parse('2 x ^ (2 + 1) * y')], - ['2 x ^ (2 + 1 + 2) * y', - opNode('*', [ - opNode('*', [constNode(2), - opNode('^', [symbolNode('x'), parenNode( - opNode('+', [constNode(2), constNode(1), constNode(2)]))]), - ], true), symbolNode('y')]) - ], - ['3x*4x', opNode('*', [math.parse('3x'), math.parse('4x')])] - ]; - tests.forEach(t => testFlatten(t[0], t[1])); -}); - -describe('flattens division', function () { - const tests = [ - // groups x/4 and continues to flatten * - ['2 * x / 4 * 6 ', - opNode('*', [opNode('/', [ - math.parse('2x'), math.parse('4')]), constNode(6)])], - ['2*3/4/5*6', - opNode('*', [constNode(2), math.parse('3/4/5'), constNode(6)])], - // combines coefficient with x - ['x / (4 * x) / 8', - math.parse('x / (4x) / 8')], - ['2 x * 4 x / 8', - opNode('*', [math.parse('2x'), opNode( - '/', [math.parse('4x'), constNode(8)])])], - ]; - tests.forEach(t => testFlatten(t[0], t[1])); -}); - -describe('subtraction', function () { - const tests = [ - ['1 + 2 - 3 - 4 + 5', - opNode('+', [ - constNode(1), constNode(2), constNode(-3), constNode(-4), constNode(5)])], - ['x - 3', opNode('+', [symbolNode('x'), constNode(-3)])], - ['x + 4 - (y+4)', - opNode('+', [symbolNode('x'), constNode(4), math.parse('-(y+4)')])], - ]; - tests.forEach(t => testFlatten(t[0], t[1])); -}); - -describe('flattens nested functions', function () { - const tests = [ - ['nthRoot(11)(x+y)', - math.parse('nthRoot(11) * (x+y)')], - ['abs(3)(1+2)', - math.parse('abs(3) * (1+2)')], - ['nthRoot(2)(nthRoot(18)+4*nthRoot(3))', - math.parse('nthRoot(2) * (nthRoot(18)+4*nthRoot(3))')], - ['nthRoot(6,3)(10+4x)', - math.parse('nthRoot(6,3) * (10+4x)')] - ]; - tests.forEach(t => testFlatten(t[0], t[1])); -}); diff --git a/test/util/flattenOperands.test.ts b/test/util/flattenOperands.test.ts new file mode 100644 index 00000000..8cac4f71 --- /dev/null +++ b/test/util/flattenOperands.test.ts @@ -0,0 +1,149 @@ +import * as math from "mathjs"; + +import { TestUtil } from "../TestUtil"; +import { printAscii } from "../../lib/src/util/print"; +import assert = require("assert"); +import { NodeCreator } from "../../lib/src/node/Creator"; + +function testFlatten(exprStr, afterNode, debug = false) { + const flattened = TestUtil.parseAndFlatten(exprStr); + if (debug) { + // eslint-disable-next-line + console.log(printAscii(flattened)); + } + TestUtil.removeComments(flattened); + TestUtil.removeComments(afterNode); + it(printAscii(flattened), function () { + assert.deepEqual(flattened, afterNode); + }); +} + +// to create nodes, for testing +const opNode = NodeCreator.operator; +const constNode = NodeCreator.constant; +const symbolNode = NodeCreator.symbol; +const parenNode = NodeCreator.parenthesis; + +describe("flattens + and *", function () { + const tests = [ + ["2+2", math.parse("2+2")], + ["2+2+7", opNode("+", [constNode(2), constNode(2), constNode(7)])], + [ + "9*8*6+3+4", + opNode("+", [ + opNode("*", [constNode(9), constNode(8), constNode(6)]), + constNode(3), + constNode(4), + ]), + ], + [ + "5*(2+3+2)*10", + opNode("*", [ + constNode(5), + parenNode(opNode("+", [constNode(2), constNode(3), constNode(2)])), + constNode(10), + ]), + ], + // keeps the polynomial term + [ + "9x*8*6+3+4", + opNode("+", [ + opNode("*", [math.parse("9x"), constNode(8), constNode(6)]), + constNode(3), + constNode(4), + ]), + ], + [ + "9x*8*6+3y^2+4", + opNode("+", [ + opNode("*", [math.parse("9x"), constNode(8), constNode(6)]), + math.parse("3y^2"), + constNode(4), + ]), + ], + // doesn't flatten + ["2 x ^ (2 + 1) * y", math.parse("2 x ^ (2 + 1) * y")], + [ + "2 x ^ (2 + 1 + 2) * y", + opNode("*", [ + opNode( + "*", + [ + constNode(2), + opNode("^", [ + symbolNode("x"), + parenNode( + opNode("+", [constNode(2), constNode(1), constNode(2)]) + ), + ]), + ], + true + ), + symbolNode("y"), + ]), + ], + ["3x*4x", opNode("*", [math.parse("3x"), math.parse("4x")])], + ]; + tests.forEach((t) => testFlatten(t[0], t[1])); +}); + +describe("flattens division", function () { + const tests = [ + // groups x/4 and continues to flatten * + [ + "2 * x / 4 * 6 ", + opNode("*", [ + opNode("/", [math.parse("2x"), math.parse("4")]), + constNode(6), + ]), + ], + [ + "2*3/4/5*6", + opNode("*", [constNode(2), math.parse("3/4/5"), constNode(6)]), + ], + // combines coefficient with x + ["x / (4 * x) / 8", math.parse("x / (4x) / 8")], + [ + "2 x * 4 x / 8", + opNode("*", [ + math.parse("2x"), + opNode("/", [math.parse("4x"), constNode(8)]), + ]), + ], + ]; + tests.forEach((t) => testFlatten(t[0], t[1])); +}); + +describe("subtraction", function () { + const tests = [ + [ + "1 + 2 - 3 - 4 + 5", + opNode("+", [ + constNode(1), + constNode(2), + constNode(-3), + constNode(-4), + constNode(5), + ]), + ], + ["x - 3", opNode("+", [symbolNode("x"), constNode(-3)])], + [ + "x + 4 - (y+4)", + opNode("+", [symbolNode("x"), constNode(4), math.parse("-(y+4)")]), + ], + ]; + tests.forEach((t) => testFlatten(t[0], t[1])); +}); + +describe("flattens nested functions", function () { + const tests = [ + ["nthRoot(11)(x+y)", math.parse("nthRoot(11) * (x+y)")], + ["abs(3)(1+2)", math.parse("abs(3) * (1+2)")], + [ + "nthRoot(2)(nthRoot(18)+4*nthRoot(3))", + math.parse("nthRoot(2) * (nthRoot(18)+4*nthRoot(3))"), + ], + ["nthRoot(6,3)(10+4x)", math.parse("nthRoot(6,3) * (10+4x)")], + ]; + tests.forEach((t) => testFlatten(t[0], t[1])); +}); diff --git a/test/util/print.test.js b/test/util/print.test.js deleted file mode 100644 index 3fbe787b..00000000 --- a/test/util/print.test.js +++ /dev/null @@ -1,64 +0,0 @@ -const math = require('mathjs'); - -const Node = require('../../lib/node'); -const print = require('../../lib/util/print'); - -const TestUtil = require('../TestUtil'); - -// to create nodes, for testing -const opNode = Node.Creator.operator; -const constNode = Node.Creator.constant; -const symbolNode = Node.Creator.symbol; - -function testPrintStr(exprStr, outputStr) { - const input = math.parse(exprStr); - TestUtil.testFunctionOutput(print.ascii, input, outputStr); -} - -function testLatexPrintStr(exprStr, outputStr) { - const input = TestUtil.parseAndFlatten(exprStr); - TestUtil.testFunctionOutput(print.latex, input, outputStr); -} - -function testPrintNode(node, outputStr) { - TestUtil.testFunctionOutput(print.ascii, node, outputStr); -} - -describe('print asciimath', function () { - const tests = [ - ['2+3+4', '2 + 3 + 4'], - ['2 + (4 - x) + - 4', '2 + (4 - x) - 4'], - ['2/3 x^2', '2/3 x^2'], - ['-2/3', '-2/3'], - ]; - tests.forEach(t => testPrintStr(t[0], t[1])); -}); - -describe('print latex', function() { - const tests = [ - ['2+3+4', '2+3+4'], - ['2 + (4 - x) - 4', '2+\\left(4 - x\\right) - 4'], - ['2/3 x^2', '\\frac{2}{3}~{ x}^{2}'], - ['-2/3', '\\frac{-2}{3}'], - ['2*x+4y', '2~ x+4~ y'], - ]; - tests.forEach(t => testLatexPrintStr(t[0],t[1])); -}); - -describe('print with parenthesis', function () { - const tests = [ - [opNode('*', [ - opNode('+', [constNode(2), constNode(3)]), - symbolNode('x') - ]), '(2 + 3) * x'], - [opNode('^', [ - opNode('-', [constNode(7), constNode(4)]), - symbolNode('x') - ]), '(7 - 4)^x'], - [opNode('/', [ - opNode('+', [constNode(9), constNode(2)]), - symbolNode('x') - ]), '(9 + 2) / x'], - ]; - tests.forEach(t => testPrintNode(t[0], t[1])); -}); diff --git a/test/util/print.test.ts b/test/util/print.test.ts new file mode 100644 index 00000000..2902958a --- /dev/null +++ b/test/util/print.test.ts @@ -0,0 +1,63 @@ +import * as math from "mathjs"; + +import { TestUtil } from "../TestUtil"; +import { NodeCreator } from "../../lib/src/node/Creator"; +import { printAscii, printLatex } from "../../lib/src/util/print"; + +// to create nodes, for testing +const opNode = NodeCreator.operator; +const constNode = NodeCreator.constant; +const symbolNode = NodeCreator.symbol; + +function testPrintStr(exprStr, outputStr) { + const input = math.parse(exprStr); + TestUtil.testFunctionOutput(printAscii, input, outputStr); +} + +function testLatexPrintStr(exprStr, outputStr) { + const input = TestUtil.parseAndFlatten(exprStr); + TestUtil.testFunctionOutput(printLatex, input, outputStr); +} + +function testPrintNode(node, outputStr) { + TestUtil.testFunctionOutput(printAscii, node, outputStr); +} + +describe("printAsciimath", function () { + const tests = [ + ["2+3+4", "2 + 3 + 4"], + ["2 + (4 - x) + - 4", "2 + (4 - x) - 4"], + ["2/3 x^2", "2/3 x^2"], + ["-2/3", "-2/3"], + ]; + tests.forEach((t) => testPrintStr(t[0], t[1])); +}); + +describe("print latex", function () { + const tests = [ + ["2+3+4", "2+3+4"], + ["2 + (4 - x) - 4", "2+\\left(4 - x\\right) - 4"], + ["2/3 x^2", "\\frac{2}{3}~{ x}^{2}"], + ["-2/3", "\\frac{-2}{3}"], + ["2*x+4y", "2~ x+4~ y"], + ]; + tests.forEach((t) => testLatexPrintStr(t[0], t[1])); +}); + +describe("print with parenthesis", function () { + const tests = [ + [ + opNode("*", [opNode("+", [constNode(2), constNode(3)]), symbolNode("x")]), + "(2 + 3) * x", + ], + [ + opNode("^", [opNode("-", [constNode(7), constNode(4)]), symbolNode("x")]), + "(7 - 4)^x", + ], + [ + opNode("/", [opNode("+", [constNode(9), constNode(2)]), symbolNode("x")]), + "(9 + 2) / x", + ], + ]; + tests.forEach((t) => testPrintNode(t[0], t[1])); +}); diff --git a/test/util/removeUnnecessaryParens.test.js b/test/util/removeUnnecessaryParens.test.js deleted file mode 100644 index 9d7520ab..00000000 --- a/test/util/removeUnnecessaryParens.test.js +++ /dev/null @@ -1,30 +0,0 @@ -const math = require('mathjs'); - -const print = require('../../lib/util/print'); -const removeUnnecessaryParens = require('../../lib/util/removeUnnecessaryParens'); - -const TestUtil = require('../TestUtil'); - -function testRemoveUnnecessaryParens(exprStr, outputStr) { - const input = removeUnnecessaryParens(math.parse(exprStr)); - TestUtil.testFunctionOutput(print.ascii, input, outputStr); -} - -describe('removeUnnecessaryParens', function () { - const tests = [ - ['(x+4) + 12', 'x + 4 + 12'], - ['-(x+4x) + 12', '-(x + 4x) + 12'], - ['x + (12)', 'x + 12'], - ['x + (y)', 'x + y'], - ['x + -(y)', 'x - y'], - ['((3 - 5)) * x', '(3 - 5) * x'], - ['((3 - 5)) * x', '(3 - 5) * x'], - ['(((-5)))', '-5'], - ['((4+5)) + ((2^3))', '(4 + 5) + 2^3'], - ['(2x^6 + -50 x^2) - (x^4)', '2x^6 - 50x^2 - x^4'], - ['(x+4) - (12 + x)', 'x + 4 - (12 + x)'], - ['(2x)^2', '(2x)^2'], - ['((4+x)-5)^(2)', '(4 + x - 5)^2'], - ]; - tests.forEach(t => testRemoveUnnecessaryParens(t[0], t[1])); -}); diff --git a/test/util/removeUnnecessaryParens.test.ts b/test/util/removeUnnecessaryParens.test.ts new file mode 100644 index 00000000..bd4d9908 --- /dev/null +++ b/test/util/removeUnnecessaryParens.test.ts @@ -0,0 +1,30 @@ +import * as math from "mathjs"; + +import { printAscii } from "../../lib/src/util/print"; +import { removeUnnecessaryParens } from "../../lib/src/util/removeUnnecessaryParens"; + +import { TestUtil } from "../TestUtil"; + +function testRemoveUnnecessaryParens(exprStr, outputStr) { + const input = removeUnnecessaryParens(math.parse(exprStr)); + TestUtil.testFunctionOutput(printAscii, input, outputStr); +} + +describe("removeUnnecessaryParens", function () { + const tests = [ + ["(x+4) + 12", "x + 4 + 12"], + ["-(x+4x) + 12", "-(x + 4x) + 12"], + ["x + (12)", "x + 12"], + ["x + (y)", "x + y"], + ["x + -(y)", "x - y"], + ["((3 - 5)) * x", "(3 - 5) * x"], + ["((3 - 5)) * x", "(3 - 5) * x"], + ["(((-5)))", "-5"], + ["((4+5)) + ((2^3))", "(4 + 5) + 2^3"], + ["(2x^6 + -50 x^2) - (x^4)", "2x^6 - 50x^2 - x^4"], + ["(x+4) - (12 + x)", "x + 4 - (12 + x)"], + ["(2x)^2", "(2x)^2"], + ["((4+x)-5)^(2)", "(4 + x - 5)^2"], + ]; + tests.forEach((t) => testRemoveUnnecessaryParens(t[0], t[1])); +}); diff --git a/test/util/util.test.ts b/test/util/util.test.ts new file mode 100644 index 00000000..5b176b70 --- /dev/null +++ b/test/util/util.test.ts @@ -0,0 +1,15 @@ +import { appendToArrayInObject } from "../../lib/src/util/Util"; +import assert = require("assert"); + +describe("appendToArrayInObject", function () { + it("creates empty array", function () { + const object = {}; + appendToArrayInObject(object, "key", "value"); + assert.deepEqual(object, { key: ["value"] }); + }); + it("appends to array if it exists", function () { + const object = { key: ["old_value"] }; + appendToArrayInObject(object, "key", "new_value"); + assert.deepEqual(object, { key: ["old_value", "new_value"] }); + }); +});