import _ from 'lodash';
/**
* @namespace number
* @description the JS native Number class
*/
export default {
native: {
/**
* check if something is a number
* @example <caption>eg. usage</caption>
* var n = 1;
*
* console.log(Number.isNumber(n)); // true
*
* console.log(Number.isNumber(2)); // true
*
* console.log(Number.isNumber('')); // false
*
* console.log(Number.isNumber(null)); // false
* @memberOf number
* @method isNumber
* @instance
* @return {boolean}
*/
isNumber(n) {
return Number.prototype.isNumber.call(n);
},
/**
* checks if a number is between a range
* @example <caption>eg. usage</caption>
* console.log((5).between(1, 10)); // true
*
* console.log((5).between(1, 4)); // false
* @memberOf number
* @method between
* @instance
* @param {number} n - the number
* @param {number} [from=Number.MIN_VALUE] - the from number
* @param {number} [to=Number.MAX_VALUE] - the to number
* @return {*|boolean}
*/
isBetween(n, from = Number.MIN_VALUE, to = Number.MAX_VALUE) {
if (!Number.isNumber(n)) {
return false;
}
return Number.prototype.isBetween.call(n, from, to);
},
/**
* parse a number value, returns null if parsing failes
* @example <caption>eg. usage</caption>
* console.log(Number.parse("1")); // 1
*
* console.log(Number.parse("1,25")); // 1.25
*
* console.log(Number.parse({})); // null
* @memberOf number
* @method parse
* @instance
* @param {*} n - the value to be parsed
* @return {number|null}
*/
parse(n) {
return _.parseInt(n);
},
/**
* repeats a function n times
* @example <caption>eg. usage</caption>
* (5).times(function(i) {
* console.log(i);
* });
*
* // logs 1, 2, 3, 4, 5
* @example <caption>or</caption>
* (5).times(function(i) {
* console.log(i);
* }, true);
*
* // logs 5, 4, 3, 2, 1
* @memberOf number
* @method times
* @instance
* @param {number} n - the number of times
* @param {function} iteratee - the iteratee function to invoke<br>
* the iteratee will be invoked passing the index as i<br>
* so the iteratee has to be something like this<br>
* <pre>
* function(i) {}
* </pre>
* @param {number} iteratee.i - the index
* @param {boolean} [reverse=false] - true if you want to do a times reverse cycle
*/
times(n, iteratee, reverse = false) {
if (Number.isNumber(n) && Number.isInteger(n)) {
return Number.prototype.times.call(n, iteratee, reverse);
}
return n;
},
/**
* randomizes a number
* @example <caption>eg. usage</caption>
* console.log(Number.random(1, 5)); // a number between 1 and 5
*
* console.log(Number.random(1, 5, true)); // a number between 1.0 and 5.0
*
* console.log(Number.random()); // a number between Number.MIN_VALUE and Number.MAX_VALUE
* @memberOf number
* @method random
* @instance
* @param {number} [lower=Number.MIN_VALUE] - the lower number
* @param {number} [upper=Number.MAX_VALUE] - the upper number
* @param {boolean} [floating=false] - ask to return a floating number value
* @return {number}
*/
random(lower = Number.MIN_VALUE, upper = Number.MAX_VALUE, floating = false) {
return _.random(lower, upper, floating);
},
/**
* converts a Romans Number String in a Decimal Number
* @example <caption>eg. usage</caption>
* console.log(Number.fromRoman('MCMLXXX')); // 1980
* @memberOf number
* @method fromRoman
* @instance
* @param {string} s - the roman number string
* @return {number}
*/
fromRoman(s) {
let str = s;
let result = null;
// the result is now a number, not a string
const decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
const roman = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
decimal.length.times((i) => {
while (str.indexOf(roman[i]) === 0) {
if (!result) {
result = 0;
}
result += decimal[i];
str = str.replace(roman[i], '');
}
});
return result;
},
/**
* converts a number in a Roman Number String
* @example <caption>eg. usage</caption>
* console.log((1980).toRoman()); // 'MCMLXXX'
* @memberOf number
* @method toRoman
* @instance
* @param {number} n - the number
* @return {string}
*/
toRoman(n) {
return Number.prototype.toRoman.call(n);
},
/**
* converts a number of bytes in a human readable file size string
* @example <caption>eg. usage</caption>
* console.log((1024).toFileSize()); // 1kb
* @memberOf number
* @method toFileSize
* @instance
* @param {number} n - the number
* @param {number} precision - the precision number
* @return {*}
*/
toFileSize(n, precision = 0) {
return Number.prototype.toFileSize.call(n, precision);
},
/**
* absolutes a number
* @example <caption>eg. usage</caption>
* console.log((1).toAbsolute()); // 1
*
* console.log((1.56).toAbsolute()); // 1.56
*
* console.log((-1.56).toAbsolute()); // 1.56
* @memberOf number
* @method toAbsolute
* @instance
* @param {number} n - the number
* @return {number}
*/
toAbsolute(n) {
return Number.prototype.toAbsolute.call(n);
},
/**
* converts a number to a integer/float string with symbol (currency, measure unit) support
* @example <caption>eg. usage</caption>
* console.log((1).toSymbolString()); // 1,00
*
* console.log((1).toSymbolString({decimals: 3}); // 1,000
*
* console.log((123456.789).toSymbolString({decimals: 2}); // 123,456.79
* console.log((123456.789).toSymbolString({sections: 4, decimals: 2}); // 12,3456.79
*
* console.log((123456.789).toSymbolString({symbol: 'kWh'}); // 12.3456,79 kWh
* console.log((123456.789).toSymbolString({symbol: '€', symbolNumberSeparator: ''}); // 123.456,79€
*
* console.log((123456.789).toSymbolString({sectionsDelimiter: '.', decimalsDelimiter: ','}); // 123.456,789
* @memberOf number
* @method toSymbolString
* @instance
* @param {number} n - the number
* @param {object} [options=false] - options to be used as parameters for conversion<br>
* @param {number} [options.sections=3] sections - section to divide the integer part of number in
* @param {string} [options.sectionsDelimiter=','] sectionsDelimiter - delimiter used to separe integer sections
* @param {number} [options.decimals=2] decimals - desired number of decimals
* @param {string} [options.decimalsDelimiter='.'] decimalsDelimiter - delimiter used to separe decimals from integer part of number
* @param {string} [options.symbol=''] symbol - currency symbol or measure unit to use (eg. '€')
* @param {boolean} [options.symbolAppend=true] symbolAppend - if true the symbol will be appended to the number
* @param {string} [options.symbolNumberSeparator=' '] symbolNumberSeparator - the separator that will be used to divide symbol from the number
* @return {string}
*/
toSymbolString(n, {
sections = 3,
sectionsDelimiter = ',',
decimals = 2,
decimalsDelimiter = '.',
symbol = '',
symbolAppend = true,
symbolNumberSeparator = ' ',
} = false) {
if (Number.isNumber(n)) {
return Number.prototype.toSymbolString.call(n, {
sections,
sectionsDelimiter,
decimals,
decimalsDelimiter,
symbol,
symbolAppend,
symbolNumberSeparator,
});
}
return n;
},
/**
* formats a number to a currency string
* @example <caption>eg. usage</caption>
* console.log((1).toCurrency()); // 1,00 €
*
* console.log((1).toCurrency({decimals: 3}); // 1,000 €
*
* console.log((123456.789).toCurrency({decimals: 2}); // 123.456,79 €
*
* console.log((123456.789).toCurrency({sections: 4, decimals: 3}); // 12.3456,789 €
*
* console.log((123456.789).toCurrency({sections: 4, sectionsDelimiter: ',', decimals: 3, decimalsDelimiter: '.'}); // 12,3456.789 €
* @memberOf number
* @method toCurrency
* @instance
* @param {number} n - the number
* @param {object} [options=false] - options to be used as parameters for conversion<br>
* @param {number} [options.sections=3] sections - section to divide the integer part of number in
* @param {string} [options.sectionsDelimiter='.'] sectionsDelimiter - delimiter used to separe integer sections
* @param {number} [options.decimals=2] decimals - desired number of decimals
* @param {string} [options.decimalsDelimiter=','] decimalsDelimiter - delimiter used to separe decimals from integer part of number
* @param {string} [options.symbol=''] symbol - currency symbol or measure unit to use (eg. '€')
* @param {boolean} [options.symbolAppend=true] symbolAppend - if true the symbol will be appended to the number
* @param {string} [options.symbolNumberSeparator=' '] symbolNumberSeparator - the separator that will be used to divide symbol from the number
* @return {string}
*/
toCurrency(n, {
sections = 3,
sectionsDelimiter = '.',
decimals = 2,
decimalsDelimiter = ',',
symbol = '€',
symbolAppend = true,
symbolNumberSeparator = '',
} = false) {
if (Number.isNumber(n)) {
return Number.prototype.toCurrency.call(n, {
sections,
sectionsDelimiter,
decimals,
decimalsDelimiter,
symbol,
symbolAppend,
symbolNumberSeparator,
});
}
return n;
},
/**
* floors a value
* @example <caption>eg. usage</caption>
* console.log((5.076).floor()); // 4
*
* console.log((5.076).floor(2)); // 5.07
*
* console.log((5070).floor(-2)); // 5000
* @memberOf number
* @method floor
* @instance
* @param {number} n - the number
* @param {number} [precision=0] - the precision number
* @return {number}
*/
floor(n, precision = 0) {
return Number.prototype.floor.call(n, precision);
},
/**
* rounds a value
* @example <caption>eg. usage</caption>
* console.log((5.007).round()); // 5
*
* console.log((5.007).round(2)); // 5.01
*
* console.log((5070).round(-2)); // 5100
* @memberOf number
* @method round
* @instance
* @param {number} n - the number
* @param {number} [precision=0] - the precision number
* @return {number}
*/
round(n, precision = 0) {
if (Number.isNumber(n)) {
return Number.prototype.round.call(n, precision);
}
return n;
},
/**
* Keeps a value `v` between `min` and `max`.
*
* @class clip
* @constructor
* @param {Number} v The value to be bounded.
* @param {Number} min The minimum bound for the value.
* @param {Number} max The maximum bound for value.
* @returns {Number} The bounded value.
*/
/**
* crops a value between specified bounds
* @example <caption>eg. usage</caption>
* console.log(Number.crop(5, 1, 10)); // 5
*
* console.log(Number.crop(5, 2, 4)); // 4
*
* console.log(Number.crop(5, 2)); // 5
*
* console.log(Number.crop(5, 6)); // 6
*
* console.log(Number.crop('5')); // '5'
*
* console.log((5).crop(1, 10)); // 5
*
* console.log((5).crop(2, 4)); // 4
*
* console.log((5).crop(2)); // 5
*
* console.log((5).crop(6)); // 6
* @memberOf number
* @method round
* @instance
* @param {number} n - the number
* @param {number} [precision=0] - the precision number
* @return {number}
*/
crop(n, min, max) {
if (Number.isNumber(n)) {
return Number.prototype.crop.call(n, min, max);
}
return n;
},
/**
* Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end. A step of -1 is used if a negative start is specified without an end or step. If end is not specified, it's set to start with start then set to 0.
* @example <caption>eg. usage</caption>
* console.log(Array.range(4));
* // [0, 1, 2, 3]
*
* console.log(Array.range(-4));
* // [0, -1, -2, -3]
*
* console.log(Array.range(1, 5));
* // [1, 2, 3, 4]
*
* console.log(Array.range(0, 20, 5));
* // [0, 5, 10, 15]
*
* console.log(Array.range(0, -4, -1));
* // [0, -1, -2, -3]
*
* console.log(Array.range(1, 4, 0));
* // [1, 1, 1]
*
* console.log(Array.range(0);
* // []
* @example <caption>eg. usage (reverse)</caption>
* console.log(Array.rangeRight(4));
* // [3, 2, 1, 0]
*
* console.log(Array.rangeRight(-4));
* // [-3, -2, -1, 0]
*
* console.log(Array.rangeRight(1, 5));
* // [4, 3, 2, 1]
*
* console.log(Array.rangeRight(0, 20, 5));
* // [15, 10, 5, 0]
*
* console.log(Array.rangeRight(0, -4, -1));
* // [-3, -2, -1, 0]
*
* console.log(Array.rangeRight(1, 4, 0));
* // [1, 1, 1]
*
* console.log(Array.rangeRight(0));
* // []
* @memberOf number
* @method range
* @instance
* @param {number} [start=0] - the start of the range
* @param {number} end - the end of the range
* @param {boolean} reverse - true, if ou want a reverse range
* @param {number} [step=1] - the value to increment or decrement by
* @return {array}
*/
range(start, end = null, reverse = false, step = 1) {
if (Number.isNumber(start)) {
return Number.prototype.range.call(start, end, reverse, step);
}
return start;
},
/**
* wraps an angle value (in degrees) between 0 and 359.
*
* @memberOf number
* @method degreeWrap
* @instance
* @param {Number} angle The angle in degrees.
* @returns {Number} The wrapped value.
*/
degreeWrap(a) {
if (Number.isNumber(a)) {
return Number.prototype.degreeWrap.call(a);
}
return a;
},
/**
* Returns the minimum distance from angle `a1` to `a2` (both in degrees). The
* result is kept between 0 and 359.
*
* @memberOf number
* @method degreeDiff
* @instance
* @param {Number} a1 The initial angle in degrees.
* @param {Number} a2 The final angle in degrees.
* @returns {Number} The angle distance value.
*/
degreeDiff(a1, a2) {
if (Number.isNumber(a1) && Number.isNumber(a2)) {
return Number.prototype.degreeDiff.call(a1, a2);
}
return 0;
},
/**
* Returns the direction that represents the minimum distance from angle `a1`
* to `a2` (in degrees). The result is `-1`, `1`, or `0` if equal.
*
* @memberOf number
* @method degreeDir
* @instance
* @param {Number} a1 The initial angle in degrees.
* @param {Number} a2 The final angle in degrees.
* @returns {Integer} A direction -1, 1 or 0.
*/
degreeDir(a1, a2) {
if (Number.isNumber(a1) && Number.isNumber(a2)) {
return Number.prototype.degreeDir.call(a1, a2);
}
return 0;
},
},
prototype: {
isNumber() {
return _.isNumber(this);
},
isBetween(from = Number.MIN_VALUE, to = Number.MAX_VALUE) {
return from <= this && this <= to;
},
times(iteratee, reverse = false) {
return (!!reverse) ? _.timesReverse(this, iteratee) : _.times(this, iteratee);
},
toRoman() {
let num = this;
let result = '';
const decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
const roman = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
_.times(decimal.length, (i) => {
while (num % decimal[i] < num) {
result += roman[i];
num -= decimal[i];
}
});
return result;
},
toFileSize(precision = 0) {
let fileSizeString = '0 B';
if (!!this) {
const sizes = ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'];
const i = Math.floor(Math.log(this) / Math.log(1024));
fileSizeString = parseFloat((this / Math.pow(1024, i)).toFixed(precision)) + sizes[i];
}
return fileSizeString;
},
toAbsolute() {
return Math.abs(this);
},
toSymbolString({
sections = 3,
sectionsDelimiter = ',',
decimals = 2,
decimalsDelimiter = '.',
symbol = '',
symbolAppend = true,
symbolNumberSeparator = ' ',
} = false) {
const prepend = (!symbolAppend && !!symbol ? symbol + symbolNumberSeparator : '');
const append = (!!symbolAppend && !!symbol ? symbolNumberSeparator + symbol : '');
const re = '\\d(?=(\\d{' + (sections || 3) + '})+' + (decimals > 0 ? '\\D' : '$') + ')';
return prepend + this.toFixed(decimals).replace('.', decimalsDelimiter).replace(new RegExp(re, 'g'), '$&' + sectionsDelimiter) + append;
},
toCurrency({
sections = 3,
sectionsDelimiter = '.',
decimals = 2,
decimalsDelimiter = ',',
symbol = '€',
symbolAppend = true,
symbolNumberSeparator = '',
} = false) {
return this.toSymbolString({
sections,
sectionsDelimiter,
decimals,
decimalsDelimiter,
symbol,
symbolAppend,
symbolNumberSeparator,
});
},
floor(precision = 0) {
return _.floor(this, precision);
},
round(precision = 0) {
return _.round(this, precision);
},
crop(min = Number.MIN_VALUE, max = Number.MAX_VALUE) {
return Math.max(Math.min(this, max), min);
},
range(end = null, reverse = false, step = 1) {
const rangeStart = Number.isNumber(end) ? this : 0;
const rangeEnd = Number.isNumber(end) ? end : this;
const method = reverse ? 'rangeRight' : 'range';
return _[method](rangeStart, rangeEnd, step);
},
degreeWrap(min = 0, max = 360) {
return ((this < min) ? max : 0) + this % max;
},
degreeDiff(a, min = 0, max = 360) {
const ang1 = Number.degreeWrap(this, min, max);
const ang2 = Number.degreeWrap(a, min, max);
let diff = ang2 - ang1;
if (diff < min) {
diff += max;
}
if (diff > max / 2) {
diff = max - diff;
}
return diff;
},
degreeDir(a, min = 0, max = 360) {
const ang1 = Number.degreeWrap(this, min, max);
const ang2 = Number.degreeWrap(a, min, max);
if (ang1 === ang2) {
return 0;
}
const diff = Number.degreeDiff(ang1, ang2, min, max);
if (diff > max / 2) {
return -1;
}
return 1;
},
},
};