'use strict'; var isNumeric = require('fast-isnumeric'); var loggers = require('./loggers'); var identity = require('./identity'); var BADNUM = require('../constants/numerical').BADNUM; // don't trust floating point equality - fraction of bin size to call // "on the line" and ensure that they go the right way specified by // linelow var roundingError = 1e-9; /** * findBin - find the bin for val - note that it can return outside the * bin range any pos. or neg. integer for linear bins, or -1 or * bins.length-1 for explicit. * bins is either an object {start,size,end} or an array length #bins+1 * bins can be either increasing or decreasing but must be monotonic * for linear bins, we can just calculate. For listed bins, run a binary * search linelow (truthy) says the bin boundary should be attributed to * the lower bin rather than the default upper bin */ exports.findBin = function(val, bins, linelow) { if(isNumeric(bins.start)) { return linelow ? Math.ceil((val - bins.start) / bins.size - roundingError) - 1 : Math.floor((val - bins.start) / bins.size + roundingError); } else { var n1 = 0; var n2 = bins.length; var c = 0; var binSize = (n2 > 1) ? (bins[n2 - 1] - bins[0]) / (n2 - 1) : 1; var n, test; if(binSize >= 0) { test = linelow ? lessThan : lessOrEqual; } else { test = linelow ? greaterOrEqual : greaterThan; } val += binSize * roundingError * (linelow ? -1 : 1) * (binSize >= 0 ? 1 : -1); // c is just to avoid infinite loops if there's an error while(n1 < n2 && c++ < 100) { n = Math.floor((n1 + n2) / 2); if(test(bins[n], val)) n1 = n + 1; else n2 = n; } if(c > 90) loggers.log('Long binary search...'); return n1 - 1; } }; function lessThan(a, b) { return a < b; } function lessOrEqual(a, b) { return a <= b; } function greaterThan(a, b) { return a > b; } function greaterOrEqual(a, b) { return a >= b; } exports.sorterAsc = function(a, b) { return a - b; }; exports.sorterDes = function(a, b) { return b - a; }; /** * find distinct values in an array, lumping together ones that appear to * just be off by a rounding error * return the distinct values and the minimum difference between any two */ exports.distinctVals = function(valsIn) { var vals = valsIn.slice(); // otherwise we sort the original array... vals.sort(exports.sorterAsc); // undefined listed in the end - also works on IE11 var last; for(last = vals.length - 1; last > -1; last--) { if(vals[last] !== BADNUM) break; } var minDiff = (vals[last] - vals[0]) || 1; var errDiff = minDiff / (last || 1) / 10000; var newVals = []; var preV; for(var i = 0; i <= last; i++) { var v = vals[i]; // make sure values aren't just off by a rounding error var diff = v - preV; if(preV === undefined) { newVals.push(v); preV = v; } else if(diff > errDiff) { minDiff = Math.min(minDiff, diff); newVals.push(v); preV = v; } } return {vals: newVals, minDiff: minDiff}; }; /** * return the smallest element from (sorted) array arrayIn that's bigger than val, * or (reverse) the largest element smaller than val * used to find the best tick given the minimum (non-rounded) tick * particularly useful for date/time where things are not powers of 10 * binary search is probably overkill here... */ exports.roundUp = function(val, arrayIn, reverse) { var low = 0; var high = arrayIn.length - 1; var mid; var c = 0; var dlow = reverse ? 0 : 1; var dhigh = reverse ? 1 : 0; var rounded = reverse ? Math.ceil : Math.floor; // c is just to avoid infinite loops if there's an error while(low < high && c++ < 100) { mid = rounded((low + high) / 2); if(arrayIn[mid] <= val) low = mid + dlow; else high = mid - dhigh; } return arrayIn[low]; }; /** * Tweak to Array.sort(sortFn) that improves performance for pre-sorted arrays * * Note that newer browsers (such as Chrome v70+) are starting to pick up * on pre-sorted arrays which may render the following optimization unnecessary * in the future. * * Motivation: sometimes we need to sort arrays but the input is likely to * already be sorted. Browsers don't seem to pick up on pre-sorted arrays, * and in fact Chrome is actually *slower* sorting pre-sorted arrays than purely * random arrays. FF is at least faster if the array is pre-sorted, but still * not as fast as it could be. * Here's how this plays out sorting a length-1e6 array: * * Calls to Sort FN | Chrome bare | FF bare | Chrome tweak | FF tweak * | v68.0 Mac | v61.0 Mac| | * ------------------+---------------+-----------+----------------+------------ * ordered | 30.4e6 | 10.1e6 | 1e6 | 1e6 * reversed | 29.4e6 | 9.9e6 | 1e6 + reverse | 1e6 + reverse * random | ~21e6 | ~18.7e6 | ~21e6 | ~18.7e6 * * So this is a substantial win for pre-sorted (ordered or exactly reversed) * arrays. Including this wrapper on an unsorted array adds a penalty that will * in general be only a few calls to the sort function. The only case this * penalty will be significant is if the array is mostly sorted but there are * a few unsorted items near the end, but the penalty is still at most N calls * out of (for N=1e6) ~20N total calls * * @param {Array} array: the array, to be sorted in place * @param {function} sortFn: As in Array.sort, function(a, b) that puts * item a before item b if the return is negative, a after b if positive, * and no change if zero. * @return {Array}: the original array, sorted in place. */ exports.sort = function(array, sortFn) { var notOrdered = 0; var notReversed = 0; for(var i = 1; i < array.length; i++) { var pairOrder = sortFn(array[i], array[i - 1]); if(pairOrder < 0) notOrdered = 1; else if(pairOrder > 0) notReversed = 1; if(notOrdered && notReversed) return array.sort(sortFn); } return notReversed ? array : array.reverse(); }; /** * find index in array 'arr' that minimizes 'fn' * * @param {array} arr : array where to search * @param {fn (optional)} fn : function to minimize, * if not given, fn is the identity function * @return {integer} */ exports.findIndexOfMin = function(arr, fn) { fn = fn || identity; var min = Infinity; var ind; for(var i = 0; i < arr.length; i++) { var v = fn(arr[i]); if(v < min) { min = v; ind = i; } } return ind; };