Extensions/number.js

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: '&euro;', symbolNumberSeparator: ''}); // 123.456,79&euro;
     *
     * 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. '&euro;')
     * @param {boolean} [options.symbolAppend=true] symbolAppend - if true the symbol will be appended to the number
     * @param {string} [options.symbolNumberSeparator='&nbsp;'] 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 = '&nbsp;',
    } = 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. '&euro;')
     * @param {boolean} [options.symbolAppend=true] symbolAppend - if true the symbol will be appended to the number
     * @param {string} [options.symbolNumberSeparator='&nbsp;'] symbolNumberSeparator - the separator that will be used to divide symbol from the number
     * @return {string}
     */
    toCurrency(n, {
      sections = 3,
      sectionsDelimiter = '.',
      decimals = 2,
      decimalsDelimiter = ',',
      symbol = '&euro;',
      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 = '&nbsp;',
    } = 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 = '&euro;',
      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;
    },
  },
};