diff --git a/README.md b/README.md index 9ef2d3a..fb79724 100644 --- a/README.md +++ b/README.md @@ -20,34 +20,69 @@ For use in the browser, use [browserify](https://github.com/substack/node-browse var nanmedian = require( 'compute-nanmedian' ); ``` -#### nanmedian( arr[, sorted] ) +#### nanmedian( arr[, options] ) -Computes the median of a numeric `array`. If the input `array` is already sorted in __ascending__ order, set the `sorted` flag to `true`. +Computes the median of an `array` ignoring non-numeric values. For unsorted primitive `arrays`, ``` javascript -var unsorted = [ 5, null, 3, 2, 4, null ], - sorted = [ null, 2, 3, 4, null, 5 ]; +var unsorted = [ 5, null, 3, 2, 4, null ]; var m1 = nanmedian( unsorted ); // returns 3.5 +``` + +The function accepts two `options`: +* `sorted`: `boolean` flag indicating if the input `array` is sorted in __ascending__ order. Default: `false`. +* `accessor`: accessor `function` for accessing values in object `arrays`. + +If the input `array` is already sorted in __ascending__ order, set the `sorted` option to `true`. -var m2 = nanmedian( sorted, true ); +``` javascript +var sorted = [ null, 2, 3, 4, null, 5 ]; + +var m2 = nanmedian( sorted, { 'sorted': true }); // returns 3.5 ``` +For object `arrays`, provide an accessor `function` for accessing `array` values + +``` javascript +var data = [ + [1,5], + [2,null], + [3,3], + [4,2], + [5,4], + [6,null] +]; + +function getValue( d ) { + return d[ 1 ]; +} + +var m3 = nanmedian( data, { + 'sorted': false, + 'accessor': getValue +}); +// returns 3.5 +``` + +__Note__: if provided an `array` which does not contain any numeric values, the function returns `null`. + + ## Examples ``` javascript -var data = new Array( 1001 ); +var nanmedian = require( 'compute-nanmedian' ); +var data = new Array( 1001 ); for ( var i = 0; i < data.length; i++ ) { - if( i % 2 === 0 ){ - data[ i ] = Math.round( Math.random() * 100 ); - } else { - data[ i ] = null; - } + if ( i % 2 === 0 ) { + data[ i ] = null; + } else { + data[ i ] = Math.round( Math.random() * 100 ); + } } - console.log( nanmedian( data ) ); ``` @@ -57,9 +92,12 @@ To run the example code from the top-level application directory, $ node ./examples/index.js ``` + ## Notes -If provided an unsorted input `array`, the function is `O( N log(N) )`, where `N` is the `array` length. If the `array` is already sorted in __ascending__ order, the function is `O(N)` as the function still has to perform a linear search to remove all non-numeric elements of the input array. +If provided an unsorted input `array`, the function is `O( N log(N) )`, where `N` is the `array` length. If the `array` is already sorted in __ascending__ order, the function is `O(N)` as the function needs to make a single linear pass to remove non-numeric values from the `array`. + + ## Tests diff --git a/examples/index.js b/examples/index.js index dd0cedf..f9215e1 100644 --- a/examples/index.js +++ b/examples/index.js @@ -3,13 +3,11 @@ var nanmedian = require( './../lib' ); var data = new Array( 1001 ); - for ( var i = 0; i < data.length; i++ ) { - if( i % 2 === 0 ){ - data[ i ] = Math.round( Math.random() * 100 ); - } else { - data[ i ] = null; - } + if ( i % 2 === 0 ) { + data[ i ] = null; + } else { + data[ i ] = Math.round( Math.random() * 100 ); + } } - console.log( nanmedian( data ) ); diff --git a/lib/index.js b/lib/index.js index 7dea142..e6a15f7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,9 +30,11 @@ // MODULES // -var isArray = require( 'validate.io-array' ); -var isBoolean = require( 'validate.io-boolean' ); -var isNumber = require( 'validate.io-number' ); +var isArray = require( 'validate.io-array' ), + isObject = require( 'validate.io-object' ), + isBoolean = require( 'validate.io-boolean' ), + isNumber = require( 'validate.io-number' ); + // FUNCTIONS // @@ -49,46 +51,69 @@ function ascending( a, b ) { return a - b; } // end FUNCTION ascending() + // MEDIAN // /** -* FUNCTION: nanmedian( arr[, sorted] ) -* Computes the median of a numeric array ignoring non-numeric values. +* FUNCTION: nanmedian( arr[, options] ) +* Computes the median of an array ignoring non-numeric values. * -* @param {Array} arr - numeric array -* @param {Boolean} [sorted] - boolean flag indicating if the array is sorted in ascending order -* @returns {Number} median value +* @param {Array} arr - input array +* @param {Object} [options] - function options +* @param {Boolean} [options.sorted] - boolean flag indicating if the array is sorted in ascending order +* @param {Function} [options.accessor] - accessor function for accessing array values +* @returns {Number|null} median value or null */ -function nanmedian( arr, sorted ) { - if ( !isArray( arr ) ) { - throw new TypeError( 'nanmedian()::invalid input argument. Must provide an array.' ); - } - if ( arguments.length > 1 && !isBoolean(sorted) ) { - throw new TypeError( 'nanmedian()::invalid input argument. Second argument must be a boolean.' ); +function nanmedian( arr, options ) { + var sorted, + clbk, + len, + id, + d, + x; + if ( !isArray( arr ) ) { + throw new TypeError( 'nanmedian()::invalid input argument. Must provide an array. Value: `' + arr + '`.' ); + } + if ( arguments.length > 1 ) { + if ( !isObject( options ) ) { + throw new TypeError( 'nanmedian()::invalid input argument. Options must be an object. Value: `' + options + '`.' ); + } + if ( options.hasOwnProperty( 'sorted' ) ) { + sorted = options.sorted; + if ( !isBoolean( sorted ) ) { + throw new TypeError( 'nanmedian()::invalid option. Sorted flag must be a boolean. Option: `' + sorted + '`.' ); + } + } + if ( options.hasOwnProperty( 'accessor' ) ) { + clbk = options.accessor; + if ( typeof clbk !== 'function' ) { + throw new TypeError( 'nanmedian()::invalid option. Accessor must be a function. Option: `' + clbk + '`.' ); + } + } + } + d = []; + for ( var i = 0; i < arr.length; i++ ) { + x = ( clbk ) ? clbk( arr[ i ] ) : arr[ i ]; + if ( isNumber( x ) ) { + d.push( x ); + } + } + len = d.length; + if ( !len ) { + return null; } - - var red = [], len, id; - for(var i = 0; i < arr.length; i++){ - if( isNumber(arr[i]) === true ){ - red.push( arr[i] ); - } - } - - len = red.length; if ( !sorted ) { - red.sort( ascending ); + d.sort( ascending ); } - - // Get the middle index: + // Get the middle index: id = Math.floor( len / 2 ); if ( len % 2 ) { // The number of elements is not evenly divisible by two, hence we have a middle index: - return red[ id ]; + return d[ id ]; } - // Even number of elements, so must take the mean of the two middle values: - return ( red[ id-1 ] + red[ id ] ) / 2.0; - + // Even number of elements, so must take the mean of the two middle values: + return ( d[ id-1 ] + d[ id ] ) / 2.0; } // end FUNCTION nanmedian() diff --git a/package.json b/package.json index 63aa548..5b646bc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ { "name": "Philipp Burckhardt", "email": "pburckhardt@outlook.com" + }, + { + "name": "Athan Reines", + "email": "kgryte@gmail.com" } ], "scripts": { @@ -25,7 +29,16 @@ "keywords": [ "compute.io", "compute", - "computation" + "computation", + "statistics", + "stats", + "mathematics", + "math", + "central tendency", + "median", + "array", + "avg", + "average" ], "bugs": { "url": "https://github.com/compute-io/nanmedian/issues" @@ -33,11 +46,12 @@ "dependencies": { "validate.io-array": "^1.0.3", "validate.io-boolean": "^1.0.4", - "validate.io-number": "^1.0.3" + "validate.io-number": "^1.0.3", + "validate.io-object": "^1.0.3" }, "devDependencies": { - "chai": "1.x.x", - "mocha": "1.x.x", + "chai": "2.x.x", + "mocha": "2.x.x", "coveralls": "^2.11.1", "istanbul": "^0.3.0", "jshint": "2.x.x", diff --git a/test/test.js b/test/test.js index e859cf9..13dc3e8 100644 --- a/test/test.js +++ b/test/test.js @@ -26,15 +26,15 @@ describe( 'compute-nanmedian', function tests() { it( 'should throw an error if provided a non-array', function test() { var values = [ - '5', - 5, - true, - undefined, - null, - NaN, - function(){}, - {} - ]; + '5', + 5, + true, + undefined, + null, + NaN, + function(){}, + {} + ]; for ( var i = 0; i < values.length; i++ ) { expect( badValue( values[i] ) ).to.throw( TypeError ); @@ -46,7 +46,29 @@ describe( 'compute-nanmedian', function tests() { } }); - it( 'should throw an error if provided a non-boolean sorted flag', function test() { + it( 'should throw an error if options is not an object', function test() { + var values = [ + '5', + 5, + true, + undefined, + null, + NaN, + function(){}, + [] + ]; + + for ( var i = 0; i < values.length; i++ ) { + expect( badValue( values[i] ) ).to.throw( TypeError ); + } + function badValue( value ) { + return function() { + nanmedian( [], value ); + }; + } + }); + + it( 'should throw an error if provided a non-boolean sorted option', function test() { var values = [ '5', 5, @@ -64,12 +86,35 @@ describe( 'compute-nanmedian', function tests() { function badValue( value ) { return function() { - nanmedian( [], value ); + nanmedian( [], {'sorted': value }); }; } }); - it( 'should compute the median for numeric array', function test() { + it( 'should throw an error if provided an accessor option which is not a function', function test() { + var values = [ + '5', + 5, + [], + undefined, + null, + NaN, + true, + {} + ]; + + for ( var i = 0; i < values.length; i++ ) { + expect( badValue( values[i] ) ).to.throw( TypeError ); + } + + function badValue( value ) { + return function() { + nanmedian( [], {'accessor': value }); + }; + } + }); + + it( 'should compute the median', function test() { var data, expected; data = [ 2, 4, 5, 3, 8, 2 ]; @@ -86,10 +131,10 @@ describe( 'compute-nanmedian', function tests() { data = [ 2, 2, 3, 4, 5, 8, 9 ]; expected = 4; - assert.strictEqual( nanmedian( data, true ), expected ); + assert.strictEqual( nanmedian( data, {'sorted': true}), expected ); }); - it( 'should compute the median for array ignoring non-numeric elements', function test() { + it( 'should compute the median ignoring non-numeric elements', function test() { var data, expected; data = [ 2, 4, null, 5, 3, 8, null, 2 ]; @@ -106,7 +151,70 @@ describe( 'compute-nanmedian', function tests() { data = [ 2, 2, 3, 4, 5, 8, 9, null, null ]; expected = 4; - assert.strictEqual( nanmedian( data, true ), expected ); + assert.strictEqual( nanmedian( data, {'sorted': true}), expected ); + }); + + it( 'should compute the median using an accessor function', function test() { + var data, expected, actual; + + data = [ + [1,2], + [2,4], + [3,null], + [4,5], + [5,3], + [6,8], + [7,null], + [8,2] + ]; + expected = 3.5; + + actual = nanmedian( data, { + 'accessor': getValue + }); + assert.strictEqual( actual, expected ); + + // Sorted: + data = [ + [1,2], + [2,null], + [3,2], + [4,3], + [5,null], + [6,5], + [7,8], + [8,9] + ]; + expected = 4; + + actual = nanmedian( data, { + 'sorted': true, + 'accessor': getValue + }); + assert.strictEqual( actual, expected ); + + function getValue( d ) { + return d[ 1 ]; + } + }); + + it( 'should return null if provided either an empty array or an array not containing any numeric values', function test() { + var data, expected; + + data = [ + null, + NaN, + true, + '', + [], + {} + ]; + + expected = null; + + assert.strictEqual( nanmedian( data ), expected ); + + assert.strictEqual( nanmedian( [] ), expected ); }); });