From f1ea4989276a4f715bb41a38136f73d81966d098 Mon Sep 17 00:00:00 2001 From: josdejong Date: Sat, 15 Mar 2014 15:27:05 +0100 Subject: [PATCH] Added some more unit tests --- bin/cli.js | 2 +- docs/functions.md | 1 + docs/{extend.md => import.md} | 22 ++++- docs/index.md | 2 +- examples/import.js | 16 ++- lib/chaining/Selector.js | 3 +- lib/function/utils/import.js | 26 +++-- lib/function/utils/typeof.js | 2 +- lib/type/Complex.js | 3 +- lib/type/Help.js | 8 +- lib/type/Index.js | 3 +- lib/type/Matrix.js | 2 +- lib/type/Range.js | 3 +- lib/type/Unit.js | 2 +- lib/util/object.js | 8 +- test/function/probability/random.test.js | 12 +-- test/function/utils/clone.test.js | 6 ++ test/function/utils/format.test.js | 6 ++ test/function/utils/import.test.js | 32 ++++++ test/function/utils/print.test.js | 24 ++++- test/function/utils/typeof.test.js | 32 +++++- test/type/Help.test.js | 121 ++++++++++++++++++++++- test/util/number.test.js | 2 +- 23 files changed, 289 insertions(+), 49 deletions(-) rename docs/{extend.md => import.md} (68%) diff --git a/bin/cli.js b/bin/cli.js index e5d2a0aef..888d7e0fd 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -44,7 +44,7 @@ var mathjs = require('../index'), parser = math.parser(), fs = require('fs'); -var PRECISION = 14; // digits +var PRECISION = 14; // decimals /** * auto complete a text diff --git a/docs/functions.md b/docs/functions.md index 0eb78d88f..439c9734e 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -137,6 +137,7 @@ math.add('hello ', 'world!'); // String 'hello world!' - math.clone(x) - math.forEach(x, callback) - math.format(value [, precision]) +- math.ifElse(conditionalExpr, trueExpr, falseExpr) - math.import(filename | object, override) - math.map(x, callback) - math.print(template, values [, precision]) diff --git a/docs/extend.md b/docs/import.md similarity index 68% rename from docs/extend.md rename to docs/import.md index f8c5a0aa9..fef6f55e9 100644 --- a/docs/extend.md +++ b/docs/import.md @@ -1,9 +1,29 @@ -# Extend +# Import The library can easily be extended with functions and variables using the `import` function. The function `import` accepts a filename or an object with functions and variables. +Function `import` has the following syntax: + +```js + math.import(object: Object [, options: Object]) + math.import(moduleName: String [, options: Object]) +``` + +The first argument can be a module name or an object. The optional second +argument can be an object with options. The following options are available: + +- `{Boolean} override` + If true, existing functions will be overwritten. False by default. +- `{Boolean} wrap` + If true (default), the functions will be wrapped in a wrapper function which + converts data types like Matrix to primitive data types like Array. + The wrapper is needed when extending math.js with libraries which do not + support the math.js data types. + +Math.js can be extended with functions and variables: + ```js // create an instance of math.js var math = require('mathjs')(); diff --git a/docs/index.md b/docs/index.md index d2d9ec064..5460b93cc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,6 @@ - [Complex Numbers](datatypes/complex_numbers.md) - [Matrices](datatypes/matrices.md) - [Units](datatypes/units.md) -- [Extension](extend.md) +- [Import](import.md) - [Configuration](configuration.md) - [Command Line Interface](command_line_interface.md) diff --git a/examples/import.js b/examples/import.js index 65b1d751c..a76b66af9 100644 --- a/examples/import.js +++ b/examples/import.js @@ -1,7 +1,7 @@ /** * Math.js can easily be extended with functions and variables using the - * `import` function. The function `import` accepts a filename or an object - * with functions and variables. + * `import` function. The function `import` accepts a module name or an object + * containing functions and variables. */ // load math.js and create an instance @@ -78,3 +78,15 @@ if (math.eig) { var b = [9, 8, 3]; print(math.solve(A, b)); // [2, -1, 3] } + +/** + * By default, the function import does not allow overriding existing functions. + * Existing functions can be overridden by specifying option `override=true` + */ +math.import({ + pi: 3.14 +}, { + override: true +}); + +print(math.pi); // returns 3.14 instead of 3.141592653589793 diff --git a/lib/chaining/Selector.js b/lib/chaining/Selector.js index 9c77cdd09..d8127849d 100644 --- a/lib/chaining/Selector.js +++ b/lib/chaining/Selector.js @@ -21,8 +21,7 @@ module.exports = function (math) { */ function Selector (value) { if (!(this instanceof Selector)) { - throw new SyntaxError( - 'Selector constructor must be called with the new operator'); + throw new SyntaxError('Constructor must be called with the new operator'); } if (value instanceof Selector) { diff --git a/lib/function/utils/import.js b/lib/function/utils/import.js index 20b4a6532..c5ba5bdb0 100644 --- a/lib/function/utils/import.js +++ b/lib/function/utils/import.js @@ -10,8 +10,8 @@ module.exports = function (math) { isUnit = Unit.isUnit; /** - * Import functions from an object or a file - * @param {function | String | Object} object + * Import functions from an object or a module + * @param {String | Object} object * @param {Object} [options] Available options: * {Boolean} override * If true, existing functions will be @@ -27,6 +27,11 @@ module.exports = function (math) { */ // TODO: return status information math['import'] = function math_import(object, options) { + var num = arguments.length; + if (num != 1 && num != 2) { + throw new math.error.ArgumentsError('import', num, 1, 2); + } + var name; var opts = { override: false, @@ -47,19 +52,7 @@ module.exports = function (math) { throw new Error('Cannot load file: require not available.'); } } - else if (isSupportedType(object)) { - // a single function - name = object.name; - if (name) { - if (opts.override || math[name] === undefined) { - _import(name, object, opts); - } - } - else { - throw new Error('Cannot import an unnamed function or object'); - } - } - else if (object instanceof Object) { + else if (typeof object === 'object') { // a map with functions for (name in object) { if (object.hasOwnProperty(name)) { @@ -73,6 +66,9 @@ module.exports = function (math) { } } } + else { + throw new TypeError('Object or module name expected'); + } }; /** diff --git a/lib/function/utils/typeof.js b/lib/function/utils/typeof.js index 780384fb7..51d990be8 100644 --- a/lib/function/utils/typeof.js +++ b/lib/function/utils/typeof.js @@ -35,7 +35,7 @@ module.exports = function (math) { if (x instanceof Unit) return 'unit'; if (x instanceof Index) return 'index'; if (x instanceof Range) return 'range'; - if (x instanceof Help) return 'matrix'; + if (x instanceof Help) return 'help'; if (x instanceof math.chaining.Selector) return 'selector'; } diff --git a/lib/type/Complex.js b/lib/type/Complex.js index dcd1315ad..1badae28c 100644 --- a/lib/type/Complex.js +++ b/lib/type/Complex.js @@ -25,8 +25,7 @@ var util = require('../util/index'), */ function Complex(re, im) { if (!(this instanceof Complex)) { - throw new SyntaxError( - 'Complex constructor must be called with the new operator'); + throw new SyntaxError('Constructor must be called with the new operator'); } switch (arguments.length) { diff --git a/lib/type/Help.js b/lib/type/Help.js index 40e2b677d..2a82e54e4 100644 --- a/lib/type/Help.js +++ b/lib/type/Help.js @@ -14,6 +14,12 @@ var util = require('../util/index'), * @constructor */ function Help (math, doc) { + if (!(this instanceof Help)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } + + // TODO: throw an error when math or doc is not provided + this.math = math; this.doc = doc; } @@ -80,7 +86,7 @@ Help.prototype.toString = function () { * Export the help object to JSON */ Help.prototype.toJSON = function () { - return object.extend({}, this.doc); + return object.clone(this.doc); }; // exports diff --git a/lib/type/Index.js b/lib/type/Index.js index 611955185..2ae6612a2 100644 --- a/lib/type/Index.js +++ b/lib/type/Index.js @@ -30,8 +30,7 @@ var util = require('../util/index'), */ function Index(ranges) { if (!(this instanceof Index)) { - throw new SyntaxError( - 'Index constructor must be called with the new operator'); + throw new SyntaxError('Constructor must be called with the new operator'); } this._ranges = []; diff --git a/lib/type/Matrix.js b/lib/type/Matrix.js index fae6a3900..a9e73027c 100644 --- a/lib/type/Matrix.js +++ b/lib/type/Matrix.js @@ -33,7 +33,7 @@ var util = require('../util/index'), function Matrix(data) { if (!(this instanceof Matrix)) { throw new SyntaxError( - 'Matrix constructor must be called with the new operator'); + 'Constructor must be called with the new operator'); } if (data instanceof Matrix) { diff --git a/lib/type/Range.js b/lib/type/Range.js index 498680c8f..f30b7c072 100644 --- a/lib/type/Range.js +++ b/lib/type/Range.js @@ -34,8 +34,7 @@ var util = require('../util/index'), */ function Range(start, end, step) { if (!(this instanceof Range)) { - throw new SyntaxError( - 'Range constructor must be called with the new operator'); + throw new SyntaxError('Constructor must be called with the new operator'); } if (start != null && !number.isNumber(start)) { diff --git a/lib/type/Unit.js b/lib/type/Unit.js index 8448f0e0a..ae547792c 100644 --- a/lib/type/Unit.js +++ b/lib/type/Unit.js @@ -23,7 +23,7 @@ var util = require('../util/index'), */ function Unit(value, unit) { if (!(this instanceof Unit)) { - throw new Error('Unit constructor must be called with the new operator'); + throw new Error('Constructor must be called with the new operator'); } if (value != null && !isNumber(value)) { diff --git a/lib/util/object.js b/lib/util/object.js index 6aafa63d4..12c03ce48 100644 --- a/lib/util/object.js +++ b/lib/util/object.js @@ -32,13 +32,19 @@ exports.clone = function clone(x) { // object if (x instanceof Object) { + if (x instanceof Number) return new Number(x.valueOf()); + if (x instanceof String) return new String(x.valueOf()); + if (x instanceof Boolean) return new Boolean(x.valueOf()); + if (x instanceof Date) return new Date(x.valueOf()); + if (x instanceof RegExp) throw new TypeError('Cannot clone ' + x); // TODO: clone a RegExp + var m = {}; for (var key in x) { if (x.hasOwnProperty(key)) { m[key] = clone(x[key]); } } - return x; + return m; } // this should never happen diff --git a/test/function/probability/random.test.js b/test/function/probability/random.test.js index c337e0e6c..4d6fee6b6 100644 --- a/test/function/probability/random.test.js +++ b/test/function/probability/random.test.js @@ -59,7 +59,7 @@ describe('distribution', function () { describe('random', function() { var originalRandom; - it('should pick uniformely distributed numbers in [0, 1]', function() { + it('should pick uniformly distributed numbers in [0, 1]', function() { var picked = []; _.times(1000, function() { @@ -69,7 +69,7 @@ describe('distribution', function () { }); - it('should pick uniformely distributed numbers in [min, max]', function() { + it('should pick uniformly distributed numbers in [min, max]', function() { var picked = []; _.times(1000, function() { @@ -78,7 +78,7 @@ describe('distribution', function () { assertUniformDistribution(picked, -10, 10); }); - it('should pick uniformely distributed random matrix, with elements in [0, 1]', function() { + it('should pick uniformly distributed random matrix, with elements in [0, 1]', function() { var picked = [], matrices = [], size = [2, 3, 4]; @@ -99,7 +99,7 @@ describe('distribution', function () { assertUniformDistribution(picked, 0, 1); }); - it('should pick uniformely distributed random matrix, with elements in [min, max]', function() { + it('should pick uniformly distributed random matrix, with elements in [min, max]', function() { var picked = [], matrices = [], size = [2, 3, 4]; @@ -131,7 +131,7 @@ describe('distribution', function () { describe('randomInt', function() { - it('should pick uniformely distributed integers in [min, max)', function() { + it('should pick uniformly distributed integers in [min, max)', function() { var picked = []; _.times(10000, function() { @@ -141,7 +141,7 @@ describe('distribution', function () { assertUniformDistributionInt(picked, -15, -5); }); - it('should pick uniformely distributed random matrix, with elements in [min, max)', function() { + it('should pick uniformly distributed random matrix, with elements in [min, max)', function() { var picked = [], matrices = [], size = [2, 3, 4]; diff --git a/test/function/utils/clone.test.js b/test/function/utils/clone.test.js index 5378d3e20..8368c6947 100644 --- a/test/function/utils/clone.test.js +++ b/test/function/utils/clone.test.js @@ -10,6 +10,11 @@ describe('clone', function() { assert.strictEqual(b, 1); }); + it('should throw an error on wrong number of arguments', function() { + assert.throws (function () {math.clone()}, math.error.ArgumentsError); + assert.throws (function () {math.clone(2, 4)}, math.error.ArgumentsError); + }); + it('should clone a bignumber', function() { var a = math.bignumber('2.3e500'); var b = math.clone(a); @@ -21,6 +26,7 @@ describe('clone', function() { var a = 'hello world'; var b = math.clone(a); a = 'bye!'; + assert.strictEqual(a, 'bye!'); assert.strictEqual(b, 'hello world'); }); diff --git a/test/function/utils/format.test.js b/test/function/utils/format.test.js index 40c842921..374187e81 100644 --- a/test/function/utils/format.test.js +++ b/test/function/utils/format.test.js @@ -80,4 +80,10 @@ describe('format', function() { assert.equal(math.format(oneThird, 18), '0.333333333333333333'); }); }); + + it('should throw an error on wrong number of arguments', function() { + assert.throws (function () {math.format()}, math.error.ArgumentsError); + assert.throws (function () {math.format(1, 2, 3)}, math.error.ArgumentsError); + }); + }); \ No newline at end of file diff --git a/test/function/utils/import.test.js b/test/function/utils/import.test.js index ebd292aab..b99d0228e 100644 --- a/test/function/utils/import.test.js +++ b/test/function/utils/import.test.js @@ -99,4 +99,36 @@ describe('import', function() { approx.equal(estFollowers, 1422.431464053916); }); + it.skip('should throw an error when trying to load a module when no module loader is available', function () { + // TODO: how to temporarily override the global function require? + var orig = require; + require = undefined; + + assert.throws(function () {math.import('numbers');}, /Cannot load file: require not available/); + + require = orig; + }); + + it('should throw an error in case of wrong number of arguments', function () { + assert.throws (function () {math.import()}, math.error.ArgumentsError); + assert.throws (function () {math.import('', {}, 3)}, math.error.ArgumentsError); + + }); + + it('should throw an error in case of wrong type of arguments', function () { + assert.throws(function () {math.import(2)}, /Object or module name expected/); + assert.throws(function () {math.import(function () {})}, /Object or module name expected/); + }); + + it('should ignore properties on Object', function () { + Object.prototype.foo = 123; + + math.import({bar: 456}); + + assert(!math.hasOwnProperty('foo')); + assert(math.hasOwnProperty('bar')); + + delete Object.prototype.foo; + }); + }); \ No newline at end of file diff --git a/test/function/utils/print.test.js b/test/function/utils/print.test.js index 6af2ec240..bfc711502 100644 --- a/test/function/utils/print.test.js +++ b/test/function/utils/print.test.js @@ -17,8 +17,30 @@ describe('print', function() { }), 'hello, first last!'); }); - it('should round interpolate values to provided precision', function() { + it('should round interpolate values with provided precision', function() { assert.equal(math.print('pi=$pi', {pi: math.pi}, 3), 'pi=3.14'); }); + it('should leave unresolved variables untouched', function() { + assert.equal(math.print('$a,$b', {b: 2}), '$a,2'); + assert.equal(math.print('$a.value,$b.value', {a: {}, b: {value: 2}}), '$a.value,2'); + }); + + it('should leave trailing point intact', function() { + assert.equal(math.print('Hello $name.', {name: 'user'}), 'Hello user.'); + assert.equal(math.print('Hello $name...', {name: 'user'}), 'Hello user...'); + assert.equal(math.print('Hello $user.name.', {user: {name: 'user'}}), 'Hello user.'); + }); + + it('should throw an error on wrong number of arguments', function() { + assert.throws (function () {math.print()}, math.error.ArgumentsError); + assert.throws (function () {math.print('')}, math.error.ArgumentsError); + assert.throws (function () {math.print('', {}, 6, 2)}, math.error.ArgumentsError); + }); + + it('should throw an error on wrong type of arguments', function() { + assert.throws (function () {math.print(2, {})}, TypeError); + assert.throws (function () {math.print('', 2)}, TypeError); + }); + }); \ No newline at end of file diff --git a/test/function/utils/typeof.test.js b/test/function/utils/typeof.test.js index ec97a81ee..519b5dc6b 100644 --- a/test/function/utils/typeof.test.js +++ b/test/function/utils/typeof.test.js @@ -1,5 +1,11 @@ // test typeof var assert = require('assert'), + Index = require('../../../lib/type/Index'), + Range = require('../../../lib/type/Range'), + Matrix = require('../../../lib/type/Matrix'), + Help = require('../../../lib/type/Help'), + Unit = require('../../../lib/type/Unit'), + Complex = require('../../../lib/type/Complex'), math = require('../../../index')(); describe('typeof', function() { @@ -20,6 +26,7 @@ describe('typeof', function() { }); it('should return complex type for a complex number', function() { + assert.equal(math.typeof(new Complex(2,3)), 'complex'); assert.equal(math.typeof(math.complex(2,3)), 'complex'); }); @@ -28,11 +35,18 @@ describe('typeof', function() { assert.equal(math.typeof(new Array()), 'array'); }); - it('should return matrix type for a matrix', function() { - assert.equal(math.typeof(math.matrix()), 'matrix'); + it('should return array type for an array', function() { + assert.equal(math.typeof([1,2,3]), 'array'); + assert.equal(math.typeof(new Array()), 'array'); }); - it('should return unit type for a unit', function() { + it('should return matrix type for a matrix', function() { + assert.equal(math.typeof(math.matrix()), 'matrix'); + assert.equal(math.typeof(new Matrix()), 'matrix'); + }); + + it('should return unit type for a unit', function() { + assert.equal(math.typeof(new Unit(5, 'cm')), 'unit'); assert.equal(math.typeof(math.unit('5cm')), 'unit'); }); @@ -63,6 +77,18 @@ describe('typeof', function() { assert.equal(math.typeof(math.select(3)), 'selector'); }); + it('should return function type for an index', function() { + assert.equal(math.typeof(new Index([0, 10])), 'index'); + }); + + it('should return function type for a range', function() { + assert.equal(math.typeof(new Range(0, 10)), 'range'); + }); + + it('should return function type for a help object', function() { + assert.equal(math.typeof(new Help()), 'help'); + }); + it('should return object type for an object', function() { assert.equal(math.typeof({}), 'object'); assert.equal(math.typeof(new Object()), 'object'); diff --git a/test/type/Help.test.js b/test/type/Help.test.js index da05d1c43..4e53a9005 100644 --- a/test/type/Help.test.js +++ b/test/type/Help.test.js @@ -1,14 +1,125 @@ // test Help var assert = require('assert'), + Help = require('../../lib/type/Help'), math = require('../../index')(); -var help = new math.type.Help(math, math.expression.docs.sin); - describe('help', function() { - + var doc = { + 'name': 'add', + 'category': 'Operators', + 'syntax': [ + 'x + y', + 'add(x, y)' + ], + 'description': 'Add two values.', + 'examples': [ + '2.1 + 3.6', + 'ans - 3.6' + ], + 'seealso': [ + 'subtract' + ] + }; + it('should generate the help for a function', function() { - assert.deepEqual(help.doc.name, 'sin'); - assert.deepEqual(help.doc, math.expression.docs.sin); + var help = new Help(math, doc); + + assert(help instanceof Help); + assert.deepEqual(help.doc.name, 'add'); + assert.deepEqual(help.doc, doc); + }); + + it('should throw an error when constructed without new operator', function() { + assert.throws(function () { + Help(math, math.expression.docs.sin); + }, /Constructor must be called with the new operator/) + }); + + it('should test whether an object is a Help object', function() { + var help = new Help(math, doc); + + assert.equal(Help.isHelp(help), true); + assert.equal(Help.isHelp(new Date()), false); + assert.equal(Help.isHelp({}), false); + }); + + it('should stringify a help', function() { + var help = new Help(math, doc); + assert.equal(help.toString(), + '\nName: add\n' + + '\n'+ + 'Category: Operators\n' + + '\n' + + 'Description:\n' + + ' Add two values.\n' + + '\n' + + 'Syntax:\n' + + ' x + y\n' + + ' add(x, y)\n' + + '\n' + + 'Examples:\n' + + ' 2.1 + 3.6\n' + + ' 5.7\n' + + ' ans - 3.6\n' + + ' 2.1\n' + + '\n' + + 'See also: subtract\n'); + }); + + it('should stringify a help with empty doc', function() { + var help = new Help(math); + assert.equal(help.toString(), '\n'); + }); + + it('should stringify a help without doc', function() { + var help = new Help(math); + assert.equal(help.toString(), '\n'); + }); + + it('should stringify a doc with empty example', function() { + var help = new Help(math, { + 'name': 'add', + 'examples': [ + '2 + 3', + '' + ] + }); + + assert.equal(help.toString(), + '\nName: add\n' + + '\n'+ + 'Examples:\n' + + ' 2 + 3\n' + + ' 5\n' + + ' \n' + + '\n'); + }); + + it('should stringify a doc with example throwing an error', function() { + var help = new Help(math, { + 'name': 'add', + 'examples': [ + '2 ++ 3' + ] + }); + + assert.equal(help.toString(), + '\nName: add\n' + + '\n'+ + 'Examples:\n' + + ' 2 ++ 3\n' + + ' SyntaxError: Value expected (char 4)\n' + + '\n'); + }); + + it('should export doc to JSON', function() { + var help = new Help(math, doc); + var json = help.toJSON(); + assert.deepEqual(json, doc); + json.name = 'foo'; // this should not alter the original doc + json.examples.push('2 + 3'); // this should not alter the original doc + assert.equal(doc.name, 'add'); + assert.notEqual(json.examples.length, doc.examples.length); }); }); \ No newline at end of file diff --git a/test/util/number.test.js b/test/util/number.test.js index b2d0d1a54..d55b3b7fc 100644 --- a/test/util/number.test.js +++ b/test/util/number.test.js @@ -314,7 +314,7 @@ describe('number', function() { describe('bignumber', function () { before (function () { - math.type.BigNumber.config(20); // ensure the precision is 20 digits, the default + BigNumber.config(20); // ensure the precision is 20 digits, the default }); it('should format big numbers', function() {