Mathematics with MathJS

Development

Reading Time: 8 minutes

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.

mkdir 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!

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.



We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.