Last active
September 9, 2020 03:15
-
-
Save dfkaye/f78f82906c3847bcbf98e9343c376c7a to your computer and use it in GitHub Desktop.
safe-math-avg.js ~ safer numeric average computation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Update 17 August 2020: | |
* Now part of safe math blog post at https://dfkaye.com/posts/2020/08/17/safer-math-operations-in-javascript-using-tdd/ | |
* Test suite at https://dfkaye.com/demos/safe-math-test-suite/ | |
*/ | |
// 14 August 2020 | |
// Returns the average of a list of functionally numeric values. | |
// Params may be a single array of values, or any number of value params. | |
// Assumes booleans and numeric strings are functionally numeric, but ignores other non-numerics. | |
// Includes safer math helper against 0.1 + 0.2 (0.30000000000000004) from gist at | |
// https://gist.github.com/dfkaye/c2210ceb0f813dda498d22776f98d48a | |
function avg() { | |
var args = Array.isArray(arguments[0]) | |
? arguments[0] | |
: arguments; | |
var size = 0; | |
return [].slice.call(args).reduce(function(sum, next) { | |
// Expand values to { left, right, by }. | |
var e = expand(+sum, +next); | |
return +e.right === +e.right | |
// If next is a number, add the values and increment the set size. | |
? (size += 1, (e.left + e.right) / e.by) | |
// Else just return current sum. | |
: sum | |
}, size) / size; | |
} | |
/* | |
* eExpand() is a helper carved from safer math ops gist at | |
* https://gist.github.com/dfkaye/c2210ceb0f813dda498d22776f98d48a | |
*/ | |
function expand(a, b) { | |
// remove formatting commas | |
var reCommas = /[\,]/g | |
var left = (a).toString().replace(reCommas, ''); | |
var right = (b).toString().replace(reCommas, ''); | |
// expand to integer values based on largest mantissa length | |
var reDecimal = /[\.]/; | |
var ml = reDecimal.test(left) && left.split('.')[1].length; | |
var mr = reDecimal.test(right) && right.split('.')[1].length; | |
var pow = ml > mr ? ml : mr; | |
var by = Math.pow(10, pow); | |
// left & right number pair, plus the expansion factor. | |
// The multiplication operator, *, coerces non-numerics to their equivalent, | |
// e.g., {} => NaN, true => 1, [4] => '4' => 4 | |
return { left: left * by, right: right * by, by }; | |
} | |
/* Test it out... */ | |
console.log( avg(1, 2, 3, 4) ); | |
// 2.5 | |
console.log( avg([1, 2, , 3, 4]) ); | |
// 2.5 | |
console.log( avg(1, 2, 3, 'bonk', 4) ); | |
// 2.5 | |
console.log( avg([1, 2, 'bonk' , 3, 4]) ); | |
// 2.5 | |
console.log( avg(1, 2, '3.0', 'bonk', 4) ); | |
// 2.5 | |
console.log( avg([1, 2, 'bonk' , '3.0', 4]) ); | |
// 2.5 | |
// false evaluates to 0 | |
console.log( avg(1, 2, 3, false, 4) ); | |
// 2.5 | |
// true evaluates to 1 | |
console.log( avg([1, true, 2, 3, 4]) ); | |
// 2.5 | |
/* 0.1 + 0.2 */ | |
console.log( avg(0.1, 0.2) ); | |
// 0.15 | |
console.log( avg(0.01, 0.02) ); | |
// 0.015 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment