Source: ut.js

const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
  'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

const ALPHANUMERIC = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
  'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
  'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
  'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

const ESCAPE_REG_EXP = /[-/\\^$*+?.()|[\]{}]/g;
const HEXADECIMAL = /^[0-9a-f]+$/i;
const MEMOIZE_MAX_SIZE = 500;

// Date

/**
 * Date object to mysql date string.
 * @param {Date} [date=new Date()] The date object.
 * @return {String} The mysql string.
 */
function dateToMysql(date = new Date()) {
  const yyyy = date.getFullYear();
  const mm = date.getMonth() + 1;
  const dd = date.getDate();
  const hh = date.getHours();
  const min = date.getMinutes();
  const ss = date.getSeconds();

  return `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}`;
}
module.exports.dateToMysql = dateToMysql;

/**
 * Date object to readable date string.
 * @param {Date} [date=new Date()] The date object.
 * @return {String} The date string.
 */
function dateToString(date = new Date()) {
  const dd = numberToString(date.getDate());
  const mm = MONTHS[date.getMonth()];
  const yyyy = numberToString(date.getFullYear());
  const hh = numberToString(date.getHours());
  const min = numberToString(date.getMinutes());
  const ss = numberToString(date.getSeconds());

  return `${paddingLeft(dd, '0', 2)}-${mm}-${yyyy.substring(2)} ${
    paddingLeft(hh, '0', 2)}:${paddingLeft(min, '0', 2)}:${paddingLeft(ss, '0', 2)}`;
}
module.exports.dateToString = dateToString;

/**
 * Get the actual date as a readable string.
 * @return {String} A readable date string.
 */
function now() {
  const date = new Date();
  const string = dateToString(date);
  const millis = numberToString(date.getMilliseconds());
  return `${string}.${paddingLeft(millis, '0', 3)}`;
}
module.exports.now = now;

/**
 * Clone a Date object.
 * @param {Date} date The original Date object.
 * @return {Date} The cloned Date.
 */
function cloneDate(date) {
  return new Date(date.getTime());
}
module.exports.cloneDate = cloneDate;

// Array

/**
 * Split an array into chunks.
 * @param {Array} array The array.
 * @param {Number} chunkSize The chunk size.
 * @return {Array} An array of chunks.
 */
function arrayChunk(array, chunkSize) {
  const size = array.length;
  const tempArray = new Array(Math.ceil(size / chunkSize));

  for (let i = 0, j = 0; j < size; j += chunkSize, i++) {
    tempArray[i] = copyArray(array, j, j + chunkSize);
  }

  return tempArray;
}
module.exports.arrayChunk = arrayChunk;

/**
 * Recursive quicksort using Hoare partitioning with random pivot and cut off to insertion sort.
 * @param {Array} array The array to sort.
 * @param {Function} [comparator=_numericComparator] An optional comparator, it will be called
 *   with two values and must return 1 if the first is greater than the second, 0 if they are
 *   equals or -1 if the second is greater than the first one.
 * @param {Number} [left=0] The left index.
 * @param {Number} [right=array.length-1] the right index.
 */
function sort(array, comparator, left, right) {
  if (isNumber(comparator)) {
    right = left;
    left = comparator;
    comparator = undefined;
  }

  left = left || 0;
  right = right || array.length - 1;
  comparator = comparator || _numericComparator;

  _quickSort(array, comparator, left, right);
}
module.exports.sort = sort;

function _quickSort(array, comparator, left, right) {
  if (right - left < 27) {
    _insertionSort(array, comparator, left, right);
    return;
  }

  let leftIndex = left;
  let rightIndex = right;
  const pivot = array[randomNumber(left, right + 1)];

  while (leftIndex <= rightIndex) {
    while (comparator(array[leftIndex], pivot) < 0) {
      leftIndex++;
    }

    while (comparator(array[rightIndex], pivot) > 0) {
      rightIndex--;
    }

    if (leftIndex <= rightIndex) {
      swap(array, leftIndex, rightIndex);
      leftIndex++;
      rightIndex--;
    }
  }

  if (left < rightIndex) {
    _quickSort(array, comparator, left, rightIndex);
  }

  if (right > leftIndex) {
    _quickSort(array, comparator, leftIndex, right);
  }
}

function _insertionSort(array, comparator, left, right) {
  for (let i = left; i <= right; i++) {
    for (let j = i; j > left && comparator(array[j], array[j - 1]) < 0; j--) {
      swap(array, j, j - 1);
    }
  }
}

function _numericComparator(number1, number2) {
  return number1 - number2;
}

/**
 * Swap the two values in an array.
 * @param {Array} array The array.
 * @param {Number} from From index.
 * @param {Number} to To index.
 */
function swap(array, from, to) {
  const aux = array[from];
  array[from] = array[to];
  array[to] = aux;
}
module.exports.swap = swap;

/**
 * Add all the elements in source at the end of dest.
 * @param {Array} dest The destiny array.
 * @param {Array} source The source array.
 */
function concatArrays(dest, source) {
  const destLength = dest.length;
  dest.length += source.length;

  for (let i = 0; i < source.length; i++) {
    dest[destLength + i] = source[i];
  }
}
module.exports.concatArrays = concatArrays;

/**
 * Shallow copy of an array.
 * @param {Array} array The array to copy.
 * @param {Number} [start=0] The start inclusive index.
 * @param {Number} [end=array.length] The end exclusive index.
 * @return {Array} The copied array.
 */
function copyArray(array, start = 0, end = array.length) {
  if (end > array.length) {
    end = array.length;
  }

  const copyLength = end - start;

  if (copyLength === 1) {
    return [array[start]];
  }

  if (copyLength < 50) {
    const copy = new Array(copyLength);
    for (let i = 0; i < copyLength; i++) {
      copy[i] = array[i + start];
    }

    return copy;
  }

  return array.slice(start, end);
}
module.exports.copyArray = copyArray;

/**
 * Empty the content of an array.
 * @param {Array} array The array to clear.
 */
function clearArray(array) {
  array.length = 0;
}
module.exports.clearArray = clearArray;

function _defaultDataGenerator() {
  return randomNumber(1, 100);
}

/**
 * Return a random array of generated elements by dataGenerator.
 * @param {Number} length The length.
 * @param {Function} [dataGenerator=_defaultDataGenerator] The data generator.
 * @return {Array} The array.
 */
function randomArray(length, dataGenerator = _defaultDataGenerator) {
  const array = new Array(length);

  for (let i = 0; i < length; i++) {
    array[i] = dataGenerator(i);
  }

  return array;
}
module.exports.randomArray = randomArray;

/**
 * Intersect two sorted arrays.
 * @param {Array} array1 The first array.
 * @param {Array} array2 The second array.
 * @return {Array} The interected array.
 * @param {Function} [comparator=_numericComparator] An optional comparator, it will be called
 *   with two values and must return 1 if the first is greater than the second, 0 if they are
 *   equals or -1 if the second is greater than the first one.
 */
function intersectSorted(array1, array2, comparator = _numericComparator) {
  let i1 = 0;
  let i2 = 0;
  const result = [];
  let previous = Infinity;

  while (i1 < array1.length && i2 < array2.length) {
    if (comparator(array1[i1], array2[i2]) < 0) {
      i1++;
    } else if (comparator(array1[i1], array2[i2]) > 0) {
      i2++;
    } else {
      if (array1[i1] !== previous) {
        previous = array1[i1];
        result.push(previous);
      }

      i1++;
      i2++;
    }
  }

  return result;
}
module.exports.intersectSorted = intersectSorted;

/**
 * About 1.5x faster than the two-arg version of Array#splice(). This
 * algorithm was taken from the core of Node.js.
 * @param {Array} array The array.
 * @param {Number} index The element to remove.
 */
function spliceOne(array, index) {
  if (index === 0) {
    array.shift();
    return;
  }

  for (; index + 1 < array.length; index++) {
    array[index] = array[index + 1];
  }

  array.pop();
}
module.exports.spliceOne = spliceOne;

/**
 * Inserts a value into a sorted array using an iterative binary search to find
 * the insertion index. 'rejectDuplicates' defines the behaviour when the value
 * that will be inserted is already in the array.
 * @param {*} value The value to insert.
 * @param {Array} array The array.
 * @param {Function} [comparator=_numericComparator] An optional comparator, it will be called
 *   with two values and must return 1 if the first is greater than the second, 0 if they are
 *   equals or -1 if the second is greater than the first one.
 * @param {Boolean} [rejectDuplicates=false] Specify if duplicated values will be rejected.
 */
function binaryInsert(value, array, comparator, rejectDuplicates) {
  if (isBoolean(comparator)) {
    rejectDuplicates = comparator;
    comparator = undefined;
  }

  rejectDuplicates = rejectDuplicates || false;
  comparator = comparator || _numericComparator;

  let left = 0;
  let right = array.length - 1;

  while (left <= right) {
    const middle = (left + right) >>> 1;
    const computed = array[middle];
    const cmpValue = comparator(computed, value);

    if (cmpValue > 0) {
      right = middle - 1;
      continue;
    }

    left = middle + 1;
    if (cmpValue === 0) {
      if (rejectDuplicates) {
        return;
      }
      break;
    }
  }

  array.splice(left, 0, value);
}
module.exports.binaryInsert = binaryInsert;

/**
 * Find a value into a sorted array using an iterative binary search.
 * @param {*} value The value to search.
 * @param {Array} array The array.
 * @param {Function} [comparator=_numericComparator] An optional comparator, it will be called
 *   with two values and must return 1 if the first is greater than the second, 0 if they are
 *   equals or -1 if the second is greater than the first one.
 * @param {Number} [left=0] The left index.
 * @param {Number} [right=array.length-1] The right index.
 * @return {Number} The index if the value was found or -1.
 */
function binarySearch(value, array, comparator, left, right) {
  if (isNumber(comparator)) {
    right = left;
    left = comparator;
    comparator = undefined;
  }

  left = left || 0;
  right = right || array.length - 1;
  comparator = comparator || _numericComparator;

  while (left <= right) {
    const middle = (left + right) >>> 1;
    const computed = array[middle];
    const cmpValue = comparator(computed, value);

    if (cmpValue > 0) {
      right = middle - 1;
      continue;
    }

    left = middle + 1;
    if (cmpValue === 0) {
      return middle;
    }
  }

  return -1;
}
module.exports.binarySearch = binarySearch;

/**
 * Returns a random value within the provided array.
 * @param {Array} array The array.
 * @param {Number} [start=0] The start inclusive index.
 * @param {Number} [end=array.length] The end exclusive index.
 * @return {*} A random item.
 */
function randomArrayItem(array, start = 0, end = array.length) {
  if (end > array.length) {
    end = array.length;
  }

  return array[randomNumber(start, end)];
}
module.exports.randomArrayItem = randomArrayItem;

// Arguments

/**
 * Convert arguments to array.
 * @param {arguments} args The arguments object.
 * @return {Array} The array.
 */
function argumentsToArray(args) {
  return copyArray(args);
}
module.exports.argumentsToArray = argumentsToArray;

// String

/**
 * Return a random alphanumeric string.
 * @param {Number} size The size
 * @param {Boolean} [caseInsensitive=false] If true, only lower case letters will be returned.
 * @return {String} The random string.
 */
function randomString(size, caseInsensitive = false) {
  let string = '';
  const limit = caseInsensitive ? 36 : 62;

  for (let i = 0; i < size; i++) {
    string += ALPHANUMERIC[randomNumber(0, limit)];
  }

  return string;
}
module.exports.randomString = randomString;

/**
 * Convert a string to a number.
 * @param {String} string The string.
 * @return {Number} The number.
 */
function stringToNumber(string) {
  return string * 1;
}
module.exports.stringToNumber = stringToNumber;

/**
 * Add a left padding to string.
 * @param {String} string The string.
 * @param {String} pad The pad.
 * @param {Number} length The length final length.
 * @return {String} The padded string.
 */
function paddingLeft(string, pad, length) {
  return repeat(pad, length - string.length) + string;
}
module.exports.paddingLeft = paddingLeft;

/**
 * Add a right padding to string.
 * @param {String} string The string.
 * @param {String} pad The pad.
 * @param {Number} length The length final length.
 * @return {String} The padded string.
 */
function paddingRight(string, pad, length) {
  return string + repeat(pad, length - string.length);
}
module.exports.paddingRight = paddingRight;

/**
 * Add a left and right padding to string.
 * @param {String} string The string.
 * @param {String} pad The pad.
 * @param {Number} length The length final length.
 * @return {String} The padded string.
 */
function paddingBoth(string, pad, length) {
  const right = Math.ceil((length - string.length) / 2);
  const left = length - (right + string.length);
  return repeat(pad, left) + string + repeat(pad, right);
}
module.exports.paddingBoth = paddingBoth;

/**
 * Repeat a string N times.
 * @param {String} string The string to repeat.
 * @param {Number} times The times to repeat.
 * @return {String} The repeated string.
 */
function repeat(string, times) {
  const length = times * string.length;
  const n1 = Math.floor(logN(2, string.length));
  const n2 = Math.ceil(logN(2, length));

  for (let i = n1; i < n2; i++) {
    string += string;
  }

  return string.substring(0, length);
}
module.exports.repeat = repeat;

/**
 * Replace all ocurrences in string.
 * @param {String} string The string.
 * @param {String} substr The substring to be replaced.
 * @param {String} newSubstr The String that replaces the substr param.
 * @param {Boolean} [ignoreCase=false] If ignore case or not.
 * @return {String} The final string.
 */
function replaceAll(string, substr, newSubstr, ignoreCase = false) {
  const flags = ignoreCase ? 'gi' : 'g';
  return string.replace(new RegExp(escapeRegExp(substr), flags), newSubstr);
}
module.exports.replaceAll = replaceAll;

/**
 * Check if a string starts by a given prefix.
 * @param {String} string The string.
 * @param {String} prefix The prefix.
 * @return {boolean} If the string starts by prefix of not.
 */
function startsWith(string, prefix) {
  return string.slice(0, prefix.length) === prefix;
}
module.exports.startsWith = startsWith;

/**
 * Check if a string ends by a given suffix.
 * @param {String} string The string.
 * @param {String} suffix The suffix.
 * @return {boolean} If the string ends by suffix of not.
 */
function endsWith(string, suffix) {
  const { length } = suffix;
  return length === 0 || string.slice(-length) === suffix;
}
module.exports.endsWith = endsWith;

/**
 * Escapes a regex expression string.
 * @param {String} string The string to be escaped.
 * @return {String} The escaped string.
 */
function escapeRegExp(string) {
  return string.replace(ESCAPE_REG_EXP, '\\$&');
}
module.exports.escapeRegExp = escapeRegExp;

/**
 * If is a string value representing a date. The string should be in a format
 * recognized by the Date.parse().
 * @param {String} string The string.
 * @return {Boolean} If is a valid date string or not.
 */
function isDateString(string) {
  const date = new Date(string);
  return !isNaN(date.getTime());
}
module.exports.isDateString = isDateString;

/**
 * Check whether a string represent a hexadecimal string or not.
 * @param {String} string The string.
 * @return {Boolean} If is a valid hexadecimal string or not.
 */
function isHexString(string) {
  return HEXADECIMAL.test(string);
}
module.exports.isHexString = isHexString;

/**
 * Split a string into chunks.
 * @param {String} string The string.
 * @param {Number} chunkSize The chunk size.
 * @return {Array} An array of chunks.
 */
function stringChunk(string, chunkSize) {
  const size = string.length;
  const tempArray = new Array(Math.ceil(size / chunkSize));

  for (let i = 0, j = 0; j < size; j += chunkSize, i++) {
    tempArray[i] = string.substring(j, j + chunkSize);
  }

  return tempArray;
}
module.exports.stringChunk = stringChunk;

/**
 * Splits an object path into an array of tokens.
 * @param {String} path the object path.
 * @return {Array} The path tokens.
 * @function
 */
const splitPath = _memoize((path) => {
  const arr = [];
  let first = 0;
  let last = 0;
  for (; last < path.length; last++) {
    if (path[last] === '[' || path[last] === '.') {
      if (first < last) {
        arr.push(path.substring(first, last));
      }

      first = last + 1;
    } else if (path[last] === ']') {
      arr.push(path.substring(first, last));
      first = last + 1;
    }
  }

  if (first < last) {
    arr.push(path.substring(first, last));
  }

  return arr;
});
module.exports.splitPath = splitPath;

function _memoize(fn, maxSize = MEMOIZE_MAX_SIZE) {
  function memoize(...args) {
    if (memoize.cache[args[0]] !== undefined) return memoize.cache[args[0]];
    const result = fn(...args);
    if (memoize.size === maxSize) {
      memoize.cache = {};
      memoize.size = 0;
    }
    memoize.cache[args[0]] = result;
    memoize.size++;
    return result;
  }

  memoize.cache = {};
  memoize.size = 0;
  return memoize;
}

// Number

/**
 * Convert a number to string.
 * @param {Number} number The number.
 * @return {String} The string.
 */
function numberToString(number) {
  return `${number}`;
}
module.exports.numberToString = numberToString;

/**
 * Get a random number.
 * @param {Number} min The inclusive min value.
 * @param {Number} max The exclusive max value.
 * @return {Number} The random number.
 */
function randomNumber(min, max) {
  return Math.floor(Math.random() * (max - min) + min);
}
module.exports.randomNumber = randomNumber;

/**
 * Get the middle value.
 * @param {Number} a The first number.
 * @param {Number} b The second number.
 * @param {Number} c The third number.
 * @return {Number} The middle number.
 */
function getMiddleNumber(a, b, c) {
  if ((a > b && b > c) || (c > b && b > a)) return b;
  if ((b > a && a > c) || (c > a && a > b)) return a;
  return c;
}
module.exports.getMiddleNumber = getMiddleNumber;

/**
 * Get the number of digits in a number. See
 * <a href="http://stackoverflow.com/questions/14879691/get-number-of-digits-with-javascript/
 * 28203456#28203456">link</a>.
 * @param {Number} integer The integer.
 * @param {Number} [base=10] The base of the number.
 * @return {Number} The number of digits.
 */
function numDigits(integer, base = 10) {
  return Math.max(Math.floor(logN(base, Math.abs(integer))), 0) + 1;
}
module.exports.numDigits = numDigits;

/**
 * Check if a number is an integer or not.
 * @param {Number} number The number to check.
 * @return {Boolean} If the number is an integer.
 */
function isInteger(number) {
  return number % 1 === 0;
}
module.exports.isInteger = isInteger;

/**
 * Checks if a number is NaN. Taken from <a href="http://jacksondunstan.com/articles/983">link</a>.
 * @param {number} number The number to ckeck.
 * @return {Boolean} If the number is NaN.
 */
function isNaN(number) {
  // eslint-disable-next-line no-self-compare
  return number !== number;
}
module.exports.isNaN = isNaN;

/**
 * Checks if a number is NaN, Infinity or -Infinity.
 * Taken from <a href="http://jacksondunstan.com/articles/983">link</a>.
 * @param {Number} number The number to ckeck.
 * @return {Boolean} If the number is NaN, Infinity or -Infinity.
 */
function isNaNOrInfinity(number) {
  return (number * 0) !== 0;
}
module.exports.isNaNOrInfinity = isNaNOrInfinity;

/**
 * Truncates the number. This method is as fast as "number | 0" but it's
 * able to handle correctly numbers greater than 2^31 or lower than -2^31.
 * @param {Number} number The number to be truncated.
 * @return {Number} The truncated number.
 */
function truncateNumber(number) {
  return number - (number % 1);
}
module.exports.truncateNumber = truncateNumber;

// Object

/**
 * Merge the source object into dest. This function only works for object,
 * arrays and primitive data types, references will be copied.
 * @param {Object|Array} dest The destiny object or array.
 * @param {Object|Array} source The source object or array.
 */
function mergeObjects(dest, source) {
  if (isPlainObject(source)) {
    for (const i in source) {
      if (!Object.prototype.hasOwnProperty.call(source, i)) continue;
      _mergeObjects(dest, source, i);
    }
  } else if (isArray(source)) {
    for (let i = 0; i < source.length; i++) {
      _mergeObjects(dest, source, i);
    }
  }
}
module.exports.mergeObjects = mergeObjects;

function _mergeObjects(dest, source, i) {
  if (isPlainObject(source[i])) {
    if (!isPlainObject(dest[i])) {
      dest[i] = {};
    }

    mergeObjects(dest[i], source[i]);
  } else if (isArray(source[i])) {
    if (!isArray(dest[i])) {
      dest[i] = new Array(source[i].length);
    }

    mergeObjects(dest[i], source[i]);
  } else {
    dest[i] = source[i];
  }
}

/**
 * Update an object or array using a given path string.
 * @param {Object|Array} dest The object or array to update.
 * @param {*} value The value to place in path.
 * @param {String|Array} path The path where to place the new value.
 */
function updateObject(dest, value, path) {
  const keys = isArray(path) ? path : splitPath(path);
  const parentPath = keys.slice(0, keys.length - 1);

  const parent = parentPath.length ? get(dest, parentPath) : dest;
  if (isObject(parent)) {
    const key = keys[keys.length - 1];
    parent[key] = value;
  }
}
module.exports.updateObject = updateObject;

function _defaultKeyGenerator() {
  return randomString(6);
}

function _defaultValueGenerator() {
  return randomNumber(1, 1000000);
}

/**
 * Get a random object.
 * @param {Number[]|Number} lengths Number of items per level.
 * @param {Function} [keyGenerator=_defaultKeyGenerator] The key generator.
 * @param {Function} [valueGenerator=_defaultValueGenerator] The value generator.
 * @return {Object} The random object.
 */
function randomObject(lengths, keyGenerator = _defaultKeyGenerator,
  valueGenerator = _defaultValueGenerator) {
  lengths = isNumber(lengths) ? [lengths] : lengths;

  const object = {};
  _randomObject(lengths, keyGenerator, valueGenerator, object, 1);
  return object;
}
module.exports.randomObject = randomObject;

function _randomObject(lengths, keyGenerator, valueGenerator, object, actualDepth) {
  const maxDepth = lengths.length;

  if (actualDepth > maxDepth) {
    return;
  }

  for (let i = 0; i < lengths[actualDepth - 1]; i++) {
    const key = keyGenerator();
    object[key] = actualDepth === maxDepth ? valueGenerator() : {};
    _randomObject(lengths, keyGenerator, valueGenerator, object[key], actualDepth + 1);
  }
}

/**
 * Divide an object into chunks by keys number.
 * @param {Object} object The object.
 * @param {Number} chunkSize The max key number per chunk.
 * @return {Object[]} An array of chunks objects.
 */
function objectChunk(object, chunkSize) {
  const chunks = [];
  let index = 0;
  let counter = 0;

  for (const key in object) {
    if (!Object.prototype.hasOwnProperty.call(object, key)) continue;
    if (chunks[index] === undefined) {
      chunks[index] = {};
    }

    chunks[index][key] = object[key];

    if (++counter % chunkSize === 0) {
      index++;
    }
  }

  return chunks;
}
module.exports.objectChunk = objectChunk;

/**
 * Deep copy of object or array.
 * @param {Object|Array} object The object or array.
 * @return {Object|Array} The cloned object.
 */
function cloneObject(original) {
  const clone = isArray(original) ? [] : {};
  mergeObjects(clone, original);
  return clone;
}
module.exports.cloneObject = cloneObject;

/**
 * Get the value using a path in an object.
 * @param {Object|Array} object The object or array.
 * @param {String|Array} path The path.
 * @param {*} [def] Value to return if no value is found in path.
 * @return {*} The found value in path.
 */
function get(obj, path, def) {
  const keys = isArray(path) ? path : splitPath(path);
  let value = keys.length ? obj : undefined;
  for (let i = 0; i < keys.length && value !== undefined; i++) {
    value = value !== null ? value[keys[i]] : undefined;
  }

  return value !== undefined ? value : def;
}
module.exports.get = get;

/**
 * Performs a deep comparison between two values to determine if they are equivalent. Plain
 * objects and arrays will be recursively iterated and primitive values and references
 * will be compared using the identity operator (===). Even though it's still a bit slower than
 * JSON.stringify(), this method works well with unsorted objects.
 * @param {Object|Array} value The first value.
 * @param {Object|Array} other The other value to compare against.
 * @return {Boolean} If the objects are equal or not.
 */
function equals(value, other) {
  if (value === other || (isNaN(value) && isNaN(other))) {
    return true;
  }

  if (!isObject(other)) {
    return false;
  }

  if (isPlainObject(value)) {
    for (const key in value) {
      if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
      if (!equals(value[key], other[key])) {
        return false;
      }
    }

    for (const key in other) {
      if (!Object.prototype.hasOwnProperty.call(other, key)) continue;
      if (value[key] === undefined
          && other[key] !== undefined) {
        return false;
      }
    }

    return true;
  } if (isArray(value)) {
    if (value.length !== other.length) {
      return false;
    }

    for (let i = 0; i < value.length; i++) {
      if (!equals(value[i], other[i])) {
        return false;
      }
    }

    return true;
  }

  return false;
}
module.exports.equals = equals;

/**
 * Group an array of objects using the values of a list of keys.
 * Usage:
 * <pre>
 *   var array = [{lang:'spanish', age: 2}, {lang:'spanish', age:5}, {lang:'english', age:25}]
 *   ut.groupBy(array, 'lang', function(obj) { return obj.age; })
 *   return -> { spanish: [ 2, 5 ], english: [ 25 ] }
 * </pre>
 * @param {Object[]} data An array of objects.
 * @param {String|String[]} keys The key or keys to group by.
 * @param {Function} [iteratee] A function to modify the final grouped objects.
 * @return {Object} The grouped object.
 */
function groupBy(array, keys, iteratee) {
  keys = isString(keys) ? [keys] : keys;

  const result = {};
  const lastKeyIndex = keys.length - 1;

  for (let i = 0; i < array.length; i++) {
    const obj = array[i];

    const pointer = obj;
    let resultPointer = result;

    for (let j = 0; j < keys.length; j++) {
      const key = keys[j];
      const keyValue = pointer[key];

      if (keyValue === undefined) {
        break;
      }

      if (resultPointer[keyValue] === undefined) {
        resultPointer[keyValue] = j < lastKeyIndex ? {} : [];
      }

      if (j === lastKeyIndex) {
        resultPointer[keyValue].push(iteratee ? iteratee(obj) : obj);
      }

      resultPointer = resultPointer[keyValue];
    }
  }

  return result;
}
module.exports.groupBy = groupBy;

/**
 * Counts and returns the length of the given object.
 * @param {Object} object The object.
 * @return {Number} The length of the object.
 */
function objectLength(object) {
  let length = 0;
  // eslint-disable-next-line no-unused-vars
  for (const i in object) {
    if (!Object.prototype.hasOwnProperty.call(object, i)) continue;
    length++;
  }

  return length;
}
module.exports.objectLength = objectLength;

/**
 * Empty the content of an object. It uses "delete" so the object will be converted into a
 * hash table mode (slow properties).
 * @see {@link toFastProperties}
 * @param {Object} object The plain object to clear.
 */
function clearObject(object) {
  for (const key in object) {
    if (!Object.prototype.hasOwnProperty.call(object, key)) continue;
    delete object[key];
  }
}
module.exports.clearObject = clearObject;

/**
 * Converts a deoptimized object (dictionary mode) into an optimized object (fast mode).
 * Objects are deoptimized when you use them like a hash table like deleting properties. You
 * can check it using the native function "%HasFastProperties(object)" running nodejs with
 * the flag "--allow-natives-syntax". This code was taken from the module "bluebird".
 * @param {Object} object The object to optimize.
 * @return {Object} Reference to the same object.
 */
function toFastProperties(object) {
  function FakeConstructor() {}

  FakeConstructor.prototype = object;

  // petkaantonov: v8 slack tracking lasts for 8 constructions
  let l = 8;
  while (l--) {
    // eslint-disable-next-line no-new
    new FakeConstructor();
  }

  return object;

  // Prevent the function from being optimized through dead code elimination
  // or further optimizations. This code is never reached but even using eval
  // in unreachable code causes v8 to not optimize functions.

  // eslint-disable-next-line no-eval, no-unreachable
  eval(object);
}
module.exports.toFastProperties = toFastProperties;

// Boolean

/**
 * Returns a random boolean.
 * @return {Boolean} The random boolean.
 */
function randomBoolean() {
  return Math.random() < 0.5;
}
module.exports.randomBoolean = randomBoolean;

// Type

/**
 * If value has a numeric value or not. It can be a Number or a String.
 * @param {*} value The value.
 * @return {Boolean} If has a numeric value or not.
 */
function isNumeric(value) {
  return !isNaNOrInfinity(parseFloat(value));
}
module.exports.isNumeric = isNumeric;

/**
 * If is a Number or not.
 * @param {*} value The value.
 * @return {Boolean} If is a Number or not.
 */
function isNumber(value) {
  return typeof value === 'number'
      || (isObject(value) && value.constructor === Number);
}
module.exports.isNumber = isNumber;

/**
 * If is a String or not.
 * @param {*} value The value.
 * @return {Boolean} If is a String or not.
 */
function isString(value) {
  return typeof value === 'string'
      || (isObject(value) && value.constructor === String);
}
module.exports.isString = isString;

/**
 * If is an Array or not.
 * @param {*} value The value.
 * @return {Boolean} If is an Array or not.
 * @function
 */
const { isArray } = Array;
module.exports.isArray = isArray;

/**
 * If is an Object or not.
 * @param {*} value The value.
 * @return {Boolean} If is an Object or not.
 */
function isObject(value) {
  return typeof value === 'object' && value !== null;
}
module.exports.isObject = isObject;

/**
 * If is a plain object (not an array) or not.
 * @param {*} value The value.
 * @return {Boolean} If is an Object and not an Array.
 */
function isPlainObject(value) {
  if (isObject(value)) {
    const proto = Object.getPrototypeOf(value);
    return proto === Object.prototype || proto === null;
  }

  return false;
}
module.exports.isPlainObject = isPlainObject;
/**
 * If is a Boolean or not.
 * @param {*} value The value.
 * @return {Boolean} If is a Boolean or not.
 */
function isBoolean(value) {
  return typeof value === 'boolean'
      || (isObject(value) && value.constructor === Boolean);
}
module.exports.isBoolean = isBoolean;

/**
 * If is Function or not.
 * @param {*} value The value.
 * @return {Boolean} If is a Function or not.
 */
function isFunction(value) {
  return typeof value === 'function';
}
module.exports.isFunction = isFunction;

/**
 * If is a RegExp or not.
 * @param {*} value The value.
 * @return {Boolean} If is a RegExp or not.
 */
function isRegExp(value) {
  return value instanceof RegExp;
}
module.exports.isRegExp = isRegExp;

/**
 * If is a Date or not.
 * @param {*} value The value.
 * @return {Boolean} If is a Date or not.
 */
function isDate(value) {
  return value instanceof Date;
}
module.exports.isDate = isDate;

/**
 * If is a Number or not. NaN, Infinity or -Infinity aren't considered valid numbers.
 * @param {*} value The value.
 * @return {Boolean} If is a Number or not.
 */
function isValidNumber(value) {
  return isNumber(value) && !isNaNOrInfinity(value);
}
module.exports.isValidNumber = isValidNumber;

// Math

/**
 * Calculate the log using a given base and value.
 * @param {Number} base The base.
 * @param {Number} value The value.
 * @return {Number} The log result.
 */
function logN(base, value) {
  const i = base === 2 ? Math.LN2
    : base === 10 ? Math.LN10 : Math.log(base);
  return Math.log(value) / i;
}
module.exports.logN = logN;

// Miscellaneous

/**
 * Execute a function N times and print the execution time.
 * @param {Function} fn The function to execute.
 * @param {Number} [times=1] How many times to execute.
 * @param {String} [label='Default label']  A label to be used in the log string.
 */
function test(fn, times = 1, label = 'Default label') {
  // eslint-disable-next-line no-console
  console.time(label);

  for (let i = 0; i < times; i++) {
    fn(i);
  }

  // eslint-disable-next-line no-console
  console.timeEnd(label);
}
module.exports.test = test;

/**
 * Check if a value is inside of a given range.
 * @param {Number|String|Array|Object} val The value.
 * @param {Number} [min=-Infinity] Min inclusive value.
 * @param {Number} [max=Infinity] Max inclusive value.
 * @return {Boolean} If the value is inside of the given range or not.
 */
function inRange(val, min = -Infinity, max = Infinity) {
  if (isNumber(val)) {
    return val >= min && val <= max;
  } if (isString(val)) {
    return val.length >= min && val.length <= max;
  } if (isArray(val)) {
    return val.length >= min && val.length <= max;
  } if (isPlainObject(val)) {
    const length = objectLength(val);
    return length >= min && length <= max;
  }

  return false;
}
module.exports.inRange = inRange;

/**
 * Fast error builder, it doesn't have a real stacktrace but is x10 faster than
 * new Error().
 * @param {String} [message=''] The error message.
 * @param {Function} [constructor=Error] Optional constructor for custom errors.
 * @return {Error} An Error instance.
 */
function error(message, constructor) {
  if (isFunction(message)) {
    constructor = message;
    message = undefined;
  }

  message = message || '';
  constructor = constructor || Error;

  const object = {
    name: constructor.name,
    message,
    stack: `${constructor.name}: ${message}`,
  };
  Object.setPrototypeOf(object, constructor.prototype);
  return object;
}
module.exports.error = error;

// Logging

/**
 * A simple logger.
 * @namespace logger
 */
const logger = {

  /**
   * The log level debug.
   * @type {Number}
   * @memberOf logger
   */
  DEBUG: 1,

  /**
   * The log level info.
   * @type {Number}
   * @memberOf logger
   */
  INFO: 2,

  /**
   * The log level warn.
   * @type {Number}
   * @memberOf logger
   */
  WARN: 3,

  /**
   * The log level error.
   * @type {Number}
   * @memberOf logger
   */
  ERROR: 4,

  /**
   * Disable all logs.
   * @type {Number}
   * @memberOf  logger
   */
  NONE: Number.MAX_VALUE,

  _logLevel: 1,
  _usingDate: true,
  _prettify: false,

  /**
   * Set the log level.
   * @param {Number} logLevel The new log level.
   * @memberOf logger
   */
  setLogLevel: function setLogLevel(logLevel) {
    this._logLevel = logLevel;
  },

  /**
   * If date will appear in the log string or not.
   * @param {Boolean} usingDate If using date or not.
   * @memberOf logger
   */
  setUsingDate: function setUsingDate(usingDate) {
    this._usingDate = usingDate;
  },

  /**
   * If plain objects should be printed prettified or not.
   * @param {Boolean} prettify If prettify plain objects or not.
   * @memberOf logger
   */
  setPrettify: function setPrettify(prettify) {
    this._prettify = prettify;
  },

  /**
   * Print a debug log.
   * @param {...*} args The arguments
   * @memberOf logger
   */
  debug: function debug(...args) {
    if (this._checkLogLevel(1)) {
      process.stdout.write(this._createHeader('[DEBUG] ') + this._createbody(args));
    }
  },

  /**
   * Print a info log.
   * @param {...*} args The arguments
   * @memberOf logger
   */
  info: function info(...args) {
    if (this._checkLogLevel(2)) {
      process.stdout.write(this._createHeader('[INFO] ') + this._createbody(args));
    }
  },

  /**
   * Print a warn log.
   * @param {...*} args The arguments
   * @memberOf logger
   */
  warn: function warn(...args) {
    if (this._checkLogLevel(3)) {
      process.stdout.write(this._createHeader('[WARN] ') + this._createbody(args));
    }
  },

  /**
   * Print a error log.
   * @param {...*} args The arguments
   * @memberOf logger
   */
  error: function loggerError(...args) {
    if (this._checkLogLevel(4)) {
      process.stdout.write(this._createHeader('[ERROR] ') + this._createbody(args));
    }
  },

  _createHeader: function _createHeader(label) {
    if (this._usingDate) {
      return `${now()} ${label}`;
    }

    return label;
  },

  _createbody: function _createbody(args) {
    if (args.length > 0) {
      let data = '';
      const { length } = args;

      for (let i = 0; i < length; i++) {
        const arg = args[i];

        if (isObject(arg)) {
          if (arg instanceof Error) {
            data += `Error: ${arg.message}`;
          } else if (this._prettify && (isArray(arg) || isPlainObject(arg))) {
            data += JSON.stringify(arg, null, 2);
          } else {
            data += JSON.stringify(arg);
          }
        } else {
          data += arg;
        }

        if (i < length - 1) {
          data += ' ';
        }
      }

      return `${data}\n`;
    }

    return '\n';
  },

  _checkLogLevel: function _checkLogLevel(methodLogLevel) {
    return this._logLevel <= methodLogLevel;
  },
};
module.exports.logger = logger;