Mathematics with MathJS
One of the language design decisions in JavaScript has followed in C’s footsteps by providing implicit type conversions based on what operators you give it. For example in JavaScript, you can add true + true
to get 2
. While it may be convenient in some cases, this has been a pain point for many developers trying to locate and remedy bugs in their programs and makes it more difficult for developers new to the language to get their desired result. So to save yourself from the headache of potential unintended type coercion with mathematics, you can be explicit with using MathJS and better guarantee the results you want.
MathJS is a very robust mathematics library that provides the capability to perform calculations in many additional math categories. It also provides unit conversion with minimal setup for custom unit types.
MathJS is the Swiss Army knife for having peace of mind when working with numbers, conversions, and mathematics in JavaScript.
Setting Up TDD Environment for Trying MathJS
I wanted to find an enjoyable way to test out most any JavaScript library from the comfort of my computer console. And here’s an elegant and easy way to get started — you’ll need to have NodeJS and Yarn installed on your machine.
npm is an alternative to Yarn you may use, but I won’t be giving instructions for that.
For testing, we’ll use Mocha and Chai, and for elegance, we’ll be writing our code in CoffeeScript.
To ensure that our tests are able to run from global commands on our system, we’ll need to run the following commands:
yarn global add mocha yarn global add coffee-script
Next we’ll create our project directory and initialize a project with yarn
.
mkdir MathJS cd MathJS yarn init
Now add Mocha and Chai to the project’s development dependencies and add MathJS as a primary dependency:
yarn add mocha chai --dev yarn add mathjs yarn install
Now make the folders for the project.
kdir bin src test
And we’ll create files for testing with Mocha. We’ll first put defaults for Mocha in test/mocha.opts
.
--reporter spec --require coffee-script/register
Then we’ll create a shorthand for executing the tests by making a file bin/test
.
#!/bin/bash mocha "test/**/*Test.{js,coffee}"
Change it to an executable with chmod +x bin/test
, and now you can type bin/test
on the command line whenever you want to run your tests.
An alternative way to simplify running tests is to use the cake
executable that is provided globally when we installed CoffeeScript globally. This works like Ruby’s rake
command; similarly you need to create a Cakefile
to define the different executable tasks you want defined. It’s a nice system, and I do recommend learning it if you’d like.
Now if you’re familiar with having your tests run automatically whenever a change is committed to a file, you can do that here as well. mocha
has some command line flag options for watching files. You would basically need to tack on --watch --watch-extensions js,coffee
.
The problem with this system though is that it remembers everything between each run, so both of your sequential test runs have conflicts with the code already loaded. So you need to be a bit more creative in getting this feature enabled.
For anyone on Ubuntu or using an Ubuntu shell in Windows, Mac, or other platform, you can use the inotifywait
command to implement something. Here’s what I’ve written that may work for you.
#!/bin/bash test_watch() { clear inotifywait --quiet --recursive --monitor --format "Changed: %w%f" \ --event close_write --exclude '(\.sw|~|[[:digit:]]+|node_modules)' \ . | bin/test; test_watch } test_watch
You can save that in bin/watch
or make an equivalent entry in your Cakefile
and be sure to change bin/watch
to executable with chmod
.
+x
The inotifywait
can only take one exclude flag. You may change the regex matchers to better fit your personal needs.
With that, you can split your terminal editor window with something like tmux, Vim, or the terminal program itself and watch live change results appear in one window while you code in another.
For testing, we’re going to have a default test helper file to load default code to use in all our test files. Create the file test/testHelper.coffee
and putting the following line of code in it:
global.expect = require('chai').expect
Likewise in the src directory we’re going to do the same. Create the file src/app.coffee
and put the following line in it.
global.MathJS = require('mathjs')
TDD with MathJS
First we’ll create two files, one for source and one for testing.
touch src/basicMath.coffee touch test/basicMathTest.coffee
Then we’ll open up test/basicMathTest.coffee
and write our first failing test.
require './testHelper' require '../src/basicMath' describe 'Math basics', -> it 'input of 1 and 1 should be 2', -> expect(add 1, "1").to.equal 2
This test is testing the function add
, which we have not defined yet. The parameters have one number as a string, as this is a common input we end up with when calculating things in JavaScript. Now you can go ahead and run your test with bin/test
and see your first failing message.
Math basics 1) input of 1 and 1 should be 2 0 passing (21ms) 1 failing 1) Math basics input of 1 and 1 should be 2: ReferenceError: add is not defined at Context.<anonymous> (test/basicMathTest.coffee:8:7)</anonymous>
The next step for TDD is to work toward green. Now we’ll define an add method to our source code in src/basicMath.coffee
. We’ll give it the generic JavaScript adding operator for this next test.
require './app' add = (a, b) -> a + b global.add = add
The global.add = add
exports the add
function to be globally available. Now we run bin/test
to see our new error message.
Math basics 1) input of 1 and 1 should be 2 0 passing (24ms) 1 failing 1) Math basics input of 1 and 1 should be 2: AssertionError: expected '11' to equal 2 at Context.<anonymous> (test/basicMathTest.coffee:6:27)</anonymous>
Here we now see that the add
method is correctly available to call, but the error message is showing us that when we added 1 + "1"
we got a string of 11
. This is part of the coercion behavior that JavaScript has for these types when adding. Now let’s change it to use MathJS and see our results.
add = (a, b) -> MathJS.add(a, b)
Run our tests and we get:
Math basics ✓ input of 1 and 1 should be 2 1 passing (33ms)
Now this example of using MathJS.add
is a contrived example but does demonstrate that MathJS has cleared up our worries of unwanted type coercion. If we really wanted to make MathJS.add
globally available as add, we simply could have done global.add = MathJS.add
. This was merely to demonstrate code structure for writing and using code with TDD.
Now on to MathJS specifics.
MathJS
MathJS is pretty exhaustive in the amount of features it contains, and I will only be highlighting some aspects as they are very well documented. But I feel it is important nonetheless to share and raise awareness of what you can do with MathJS.
# You can simplify mathmatical expressions with MathJS.simplify('x * y * -x / (x ^ 2)').toString() # => '-y' # Find the symbolic derivative of an expression MathJS.derivative('2x^2 + 3x + 4', 'x').toString() # => '4 * x + 3' # Evaluate expressions MathJS.eval('2 inch to cm') # => 5.08 cm
You can chain your method calls together.
MathJS.chain(5).multiply(5).add(4).done() # => 29
You may define your own unit measurements and override existing ones.
math.createUnit('mile', '1609.347218694', {override: true}})
You can choose different types of the number you return, like having a fraction be the return type.
math.config({ number: 'Fraction' })
These are just a small glimpse into what you may do with MathJS.
Some things to keep in mind with MathJS
When you want to perform division with MathJS, it’s better to use the fraction methods included with it rather than divide. MathJS has included an entirely separate library just for dealing with fractions, and it’s much more accurate in producing desired results.
Also, when you’re writing methods that return values after a calculation, there is a MathJS object that’s generally returned from many of the methods; you may want to simplify that back down to a number by passing it to MathJS.number
.
When writing a function that needs to perform division and return a number with something like 1250 - ((450-98)*37)/5000
where each of those numbers could be variables or parameters, it would look like:
MathJS.number( MathJS.subtract( 1250 MathJS.fraction( MathJS.chain(450).subtract(98).multiply(37).done() 5000 ) ) )
This produces a good decimal number result.
When trying to deal with some quirks, like round-off errors in JavaScript where you add 0.1 + 0.2
and get 0.30000000000000004
, even with MathJS.add
you need to either format it with precision, use Fractions
, or use BigNumbers
.
To format it with precision, you would do the following.
number = MathJS.add(0.1, 0.2) MathJS.format(number, {precision: 14}) # => '0.3'
Note that the format method will return a string type. As long as you continue using MathJS for further calculations on the returned value, there shouldn’t be an issue. Otherwise pass it to MathJS.number
to convert the string to an integer or float.
Counting to Change
In an example where you would like to calculate the exact coin change from an amount, you could use TDD to invent the process one step at a time. With MathJS’s unit conversion feature, you can completely skip writing the implementation and simply define quantity relations of one coin to another.
So a penny will be the smallest unit of 1, and everything else can relate to that. Using the MathJS.createUnit
method, we can define all the relations in one go.
MathJS.createUnit({ 'penny': aliases: ['pennies'] 'nickel': definition: '5 pennies' aliases: ['nickels'] 'dime': definition: '2 nickels' aliases: ['dimes'] 'quarter': definition: '5 nickels' aliases: ['quarters'] 'halfdollar': definition: '2 quarters' aliases: ['halfdollars'] 'dollar': definition: '4 quarters' aliases: ['dollars'] })
With this, we have everything we need to convert a dollar and change amount into the proper coinage. Here’s how that looks after the TDD work as demonstrated above — the src/coinCounter.coffee
file looks like:
require './app' # # The MathJS.createUnit code shown # above goes here # defaultDenoms = -> [ 'dollars' 'halfdollars' 'quarters' 'dimes' 'nickels' 'pennies' ] count_change = (amount, denominations) -> denoms = denominations ? defaultDenoms() MathJS. unit(amount). splitUnit(denoms). toString() global.count_change = count_change
And the tests in test/coinCounterTest.coffee
to show splitting into coins:
require './testHelper' require '../src/coinCounter' describe 'Makes change', -> it 'should produce default denominations of change', -> expect( count_change('831 pennies') ) . to . equal ' 8 dollars,\ 0 halfdollars,\ 1 quarters,\ 0 dimes,\ 1 nickels,\ 1 pennies ' it 'can handle specific small change only', -> denominations = ['dimes', 'nickels', 'pennies'] expect( count_change('831 pennies', denominations) ) . to . equal ' 83 dimes,\ 0 nickels,\ 1 pennies '
And with running bin/test
, we get all tests passing green.
We didn’t need to do any conversion logic of our own — we simply let MathJS take the unit comparisons we gave it and do the work for us.
My only nitpick about the results is that whenever a value of 1
is returned for any denomination, it would be really nice if it used the singular word for it. But that’s easy enough to program for if it’s a requirement.
Summary
Not only does MathJS make calculations safer to do in JavaScript, but it gives us so much that we can do with the robustness of its library. Now we can do finance, Latex, unit conversions, algebra, and much more with the well designed library MathJS. Enjoy!
Published on Web Code Geeks with permission by Daniel P. Clark, partner at our WCG program. See the original article here: Mathematics with MathJS Opinions expressed by Web Code Geeks contributors are their own. |