/**
* @class Ext.Number
*
* A collection of useful static methods to deal with numbers
* @singleton
*/
Ext.Number = (new function() { // jshint ignore:line
// @define Ext.lang.Number
// @define Ext.Number
// @require Ext
var ExtNumber = this,
isToFixedBroken = (0.9).toFixed() !== '1',
math = Math,
ClipDefault = {
count: false,
inclusive: false,
wrap: true
};
// polyfill
Number.MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -(math.pow(2, 53) - 1);
Number.MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || math.pow(2, 53) - 1;
Ext.apply(ExtNumber, {
MIN_SAFE_INTEGER: Number.MIN_SAFE_INTEGER,
MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER,
MAX_32BIT_INTEGER: math.pow(2, 31) - 1,
// No good way to allow "9." w/o allowing "." alone but we use isNaN to reject that
floatRe: /^[-+]?(?:\d+|\d*\.\d*)(?:[Ee][+-]?\d+)?$/,
intRe: /^[-+]?\d+(?:[Ee]\+?\d+)?$/,
Clip: {
DEFAULT: ClipDefault,
COUNT: Ext.applyIf({
count: true
}, ClipDefault),
INCLUSIVE: Ext.applyIf({
inclusive: true
}, ClipDefault),
NOWRAP: Ext.applyIf({
wrap: false
}, ClipDefault)
},
/**
* Strictly parses the given value and returns the value as a number or `null` if
* the value is not a number or contains non-numeric pieces.
* @param {String} value
* @return {Number}
* @since 6.5.1
*/
parseFloat: function(value) {
if (value === undefined) {
value = null;
}
if (value !== null && typeof value !== 'number') {
value = String(value);
value = ExtNumber.floatRe.test(value) ? +value : null;
if (isNaN(value)) {
value = null;
}
}
return value;
},
/**
* Strictly parses the given value and returns the value as a number or `null` if
* the value is not an integer number or contains non-integer pieces.
* @param {String} value
* @return {Number}
* @since 6.5.1
*/
parseInt: function(value) {
if (value === undefined) {
value = null;
}
if (typeof value === 'number') {
value = Math.floor(value);
}
else if (value !== null) {
value = String(value);
value = ExtNumber.intRe.test(value) ? +value : null;
}
return value;
},
binarySearch: function(array, value, begin, end) {
var middle, midVal;
if (begin === undefined) {
begin = 0;
}
if (end === undefined) {
end = array.length;
}
--end;
while (begin <= end) {
middle = (begin + end) >>> 1; // unsigned right shift = Math.floor(x/2)
midVal = array[middle];
if (value === midVal) {
return middle;
}
if (midVal < value) {
begin = middle + 1;
}
else {
end = middle - 1;
}
}
return begin;
},
bisectTuples: function(array, value, index, begin, end) {
var middle, midVal;
if (begin === undefined) {
begin = 0;
}
if (end === undefined) {
end = array.length;
}
--end;
while (begin <= end) {
middle = (begin + end) >>> 1; // unsigned right shift = Math.floor(x/2)
midVal = array[middle][index];
if (value === midVal) {
return middle;
}
if (midVal < value) {
begin = middle + 1;
}
else {
end = middle - 1;
}
}
return begin;
},
/**
* Coerces a given index into a valid index given a `length`.
*
* Negative indexes are interpreted starting at the end of the collection. That is,
* a value of -1 indicates the last item, or equivalent to `length - 1`.
*
* When handling methods that take "begin" and "end" arguments like most array or
* string methods, this method can be used like so:
*
* function foo (array, begin, end) {
* var range = Ext.Number.clipIndices(array.length, [begin, end]);
*
* begin = range[0];
* end = range[1];
*
* // 0 <= begin <= end <= array.length
*
* var length = end - begin;
* }
*
* For example:
*
* +---+---+---+---+---+---+---+---+
* | | | | | | | | | length = 8
* +---+---+---+---+---+---+---+---+
* 0 1 2 3 4 5 6 7
* -8 -7 -6 -5 -4 -3 -2 -1
*
* console.log(Ext.Number.clipIndices(8, [3, 10])); // logs "[3, 8]"
* console.log(Ext.Number.clipIndices(8, [-5])); // logs "[3, 8]"
* console.log(Ext.Number.clipIndices(8, []));
* console.log(Ext.Number.clipIndices(8, []));
*
* @param {Number} length
* @param {Number[]} indices
* @param {Object} [options] An object with different option flags.
* @param {Boolean} [options.count=false] The second number in `indices` is the
* count not and an index.
* @param {Boolean} [options.inclusive=false] The second number in `indices` is
* "inclusive" meaning that the item should be considered in the range. Normally,
* the second number is considered the first item outside the range or as an
* "exclusive" bound.
* @param {Boolean} [options.wrap=true] Wraps negative numbers backwards from the
* end of the array. Passing `false` simply clips negative index values at 0.
* @return {Number[]} The normalized `[begin, end]` array where `end` is now
* exclusive such that `length = end - begin`. Both values are between 0 and the
* given `length` and `end` will not be less-than `begin`.
*/
clipIndices: function(length, indices, options) {
var defaultValue = 0, // default value for "begin"
wrap, begin, end, i;
options = options || ClipDefault;
wrap = options.wrap;
indices = indices || [];
for (i = 0; i < 2; ++i) {
// names are off on first pass but used this way so things make sense
// following the loop..
begin = end; // pick up and keep the result from the first loop
end = indices[i];
if (end == null) {
end = defaultValue;
}
else if (i && options.count) {
end += begin; // this is the length not "end" so convert to "end"
end = (end > length) ? length : end;
}
else {
if (wrap) {
end = (end < 0) ? (length + end) : end;
}
if (i && options.inclusive) {
++end;
}
end = (end < 0) ? 0 : ((end > length) ? length : end);
}
defaultValue = length; // default value for "end"
}
// after loop:
// 0 <= begin <= length (calculated from indices[0])
// 0 <= end <= length (calculated from indices[1])
indices[0] = begin;
indices[1] = (end < begin) ? begin : end;
return indices;
},
/**
* Checks whether or not the passed number is within a desired range. If the number is
* already within the range it is returned, otherwise the min or max value is returned
* depending on which side of the range is exceeded. Note that this method returns the
* constrained value but does not change the current number.
* @param {Number} number The number to check
* @param {Number} min The minimum number in the range
* @param {Number} max The maximum number in the range
* @return {Number} The constrained value if outside the range, otherwise the current value
*/
constrain: function(number, min, max) {
var x = parseFloat(number);
// (x < Nan) || (x < undefined) == false
// same for (x > NaN) || (x > undefined)
// sadly this is not true of null - (1 > null)
if (min === null) {
min = number;
}
if (max === null) {
max = number;
}
// Watch out for NaN in Chrome 18
// V8bug: http://code.google.com/p/v8/issues/detail?id=2056
// Operators are faster than Math.min/max. See http://jsperf.com/number-constrain
return (x < min) ? min : ((x > max) ? max : x);
},
/**
* Snaps the passed number between stopping points based upon a passed increment value.
*
* The difference between this and {@link #snapInRange} is that {@link #snapInRange} uses
* the minValue when calculating snap points:
*
* // Returns 56 - snap points are zero based
* r = Ext.Number.snap(56, 2, 55, 65);
*
* // Returns 57 - snap points are based from minValue
* r = Ext.Number.snapInRange(56, 2, 55, 65);
*
* @param {Number} value The unsnapped value.
* @param {Number} increment The increment by which the value must move.
* @param {Number} minValue The minimum value to which the returned value must be
* constrained. Overrides the increment.
* @param {Number} maxValue The maximum value to which the returned value must be
* constrained. Overrides the increment.
* @return {Number} The value of the nearest snap target.
*/
snap: function(value, increment, minValue, maxValue) {
var m;
// If no value passed, or minValue was passed and value is less than minValue
// (anything < undefined is false)
// Then use the minValue (or zero if the value was undefined)
if (value === undefined || value < minValue) {
return minValue || 0;
}
if (increment) {
m = value % increment;
if (m !== 0) {
value -= m;
if (m * 2 >= increment) {
value += increment;
}
else if (m * 2 < -increment) {
value -= increment;
}
}
}
return ExtNumber.constrain(value, minValue, maxValue);
},
/**
* Snaps the passed number between stopping points based upon a passed increment value.
*
* The difference between this and {@link #snap} is that {@link #snap} does not use
* the minValue when calculating snap points:
*
* // Returns 56 - snap points are zero based
* r = Ext.Number.snap(56, 2, 55, 65);
*
* // Returns 57 - snap points are based from minValue
* r = Ext.Number.snapInRange(56, 2, 55, 65);
*
* @param {Number} value The unsnapped value.
* @param {Number} increment The increment by which the value must move.
* @param {Number} [minValue=0] The minimum value to which the returned value must be
* constrained.
* @param {Number} [maxValue=Infinity] The maximum value to which the returned value
* must be constrained.
* @return {Number} The value of the nearest snap target.
*/
snapInRange: function(value, increment, minValue, maxValue) {
var tween;
// default minValue to zero
minValue = (minValue || 0);
// If value is undefined, or less than minValue, use minValue
if (value === undefined || value < minValue) {
return minValue;
}
// Calculate how many snap points from the minValue the passed value is.
if (increment && (tween = ((value - minValue) % increment))) {
value -= tween;
tween *= 2;
if (tween >= increment) {
value += increment;
}
}
// If constraining within a maximum, ensure the maximum is on a snap point
if (maxValue !== undefined) {
if (value > (maxValue = ExtNumber.snapInRange(maxValue, increment, minValue))) {
value = maxValue;
}
}
return value;
},
/**
* Round a number to the nearest interval.
* @param {Number} value The value to round.
* @param {Number} interval The interval to round to.
* @return {Number} The rounded value.
*
* @since 6.2.0
*/
roundToNearest: function(value, interval) {
interval = interval || 1;
return interval * math.round(value / interval);
},
/**
* Rounds a number to the specified precision.
* @param value
* @param precision
* @return {number}
*/
roundToPrecision: function(value, precision) {
var factor = math.pow(10, precision || 1);
return math.round(value * factor) / factor;
},
/**
* Truncates a number to the specified precision,
* without rounding.
* @param value
* @param precision
* @return {number}
* @since 6.5.1
*/
truncateToPrecision: function(value, precision) {
var factor = math.pow(10, precision || 1);
return parseInt(value * factor, 10) / factor;
},
/**
* Returns the sign of the given number. See also MDN for Math.sign documentation
* for the standard method this method emulates.
* @param {Number} x The number.
* @return {Number} The sign of the number `x`, indicating whether the number is
* positive (1), negative (-1) or zero (0).
* @method sign
*/
sign: math.sign || function(x) {
x = +x; // force to a Number
if (x === 0 || isNaN(x)) {
return x;
}
return (x > 0) ? 1 : -1;
},
/**
* Returns the base 10 logarithm of a number.
* This will use Math.log10, if available (ES6).
* @param {Number} x The number.
* @return {Number} Base 10 logarithm of the number 'x'.
* @method log10
*/
log10: math.log10 || function(x) {
return math.log(x) * math.LOG10E;
},
/**
* Determines if two numbers `n1` and `n2` are equal within a given
* margin of precision `epsilon`.
* @param {Number} n1 First number.
* @param {Number} n2 Second number.
* @param {Number} epsilon Margin of precision.
* @return {Boolean} `true`, if numbers are equal. `false` otherwise.
*/
isEqual: function(n1, n2, epsilon) {
//<debug>
/* eslint-disable-next-line max-len */
if (!(typeof n1 === 'number' && typeof n2 === 'number' && typeof epsilon === 'number')) {
Ext.raise("All parameters should be valid numbers.");
}
//</debug>
return math.abs(n1 - n2) < epsilon;
},
/**
* Determines if the value passed is a number and also finite.
* This a Polyfill version of Number.isFinite(),differently than
* the isFinite() function, this method doesn't convert the parameter to a number.
* @param {Number} value Number to be tested.
* @return {Boolean} `true`, if the parameter is a number and finite, `false` otherwise.
* @since 6.2
*/
isFinite: Number.isFinite || function(value) {
return typeof value === 'number' && isFinite(value);
},
isInteger: Number.isInteger || function(value) {
// Add zero get a valid result in a special case where the value is a number string.
// E.g. '10' + 0 is '100'.
return ~~(value + 0) === value;
},
/**
* @method
* Formats a number using fixed-point notation
* @param {Number} value The number to format
* @param {Number} precision The number of digits to show after the decimal point
*/
toFixed: isToFixedBroken
? function(value, precision) {
var pow;
precision = precision || 0;
pow = math.pow(10, precision);
return (math.round(value * pow) / pow).toFixed(precision);
}
: function(value, precision) {
return value.toFixed(precision);
},
/**
* Validate that a value is numeric and convert it to a number if necessary.
* Returns the specified default value if it is not.
*
* Ext.Number.from('1.23', 1); // returns 1.23
* Ext.Number.from('abc', 1); // returns 1
*
* @param {Object} value
* @param {Number} defaultValue The value to return if the original value is non-numeric
* @return {Number} value, if numeric, defaultValue otherwise
*/
from: function(value, defaultValue) {
if (isFinite(value)) {
value = parseFloat(value);
}
return !isNaN(value) ? value : defaultValue;
},
/**
* Returns a random integer between the specified range (inclusive)
* @param {Number} from Lowest value to return.
* @param {Number} to Highest value to return.
* @return {Number} A random integer within the specified range.
*/
randomInt: function(from, to) {
return math.floor(math.random() * (to - from + 1) + from);
},
/**
* Corrects floating point numbers that overflow to a non-precise
* value because of their floating nature, for example `0.1 + 0.2`
* @param {Number} n The number
* @return {Number} The correctly rounded number
*/
correctFloat: function(n) {
// This is to correct the type of errors where 2 floats end with
// a long string of decimals, eg 0.1 + 0.2. When they overflow in this
// manner, they usually go to 15-16 decimals, so we cut it off at 14.
return parseFloat(n.toPrecision(14));
}
});
/**
* @deprecated 4.0.0 Please use {@link Ext.Number#from} instead.
* @member Ext
* @method num
* @inheritdoc Ext.Number#from
*/
Ext.num = function() {
return ExtNumber.from.apply(this, arguments);
};
}());