lively.lang_array.js

/* global GLOB */
/**
 * Methods to make working with arrays more convenient and collection-like.
 * @module lively.lang/array
 */

import { equals as objectEquals } from './object.js';
import { Null as NullFunction, delay } from './function.js';
import Group from './Group.js';

/**
 * Creates an array containing elements from `begin` until `end` with `step`-sized steps.
 * @param {number} begin - First element
 * @param {number} end  - Last element
 * @param {number} step - step size
 * @returns {number[]}
 */
function range (begin, end, step) {
  step = step || 0;
  const result = [];
  if (begin <= end) {
    if (step <= 0) step = -step || 1;
    for (let i = begin; i <= end; i += step) result.push(i);
  } else {
    if (step >= 0) step = -step || -1;
    for (let i = begin; i >= end; i += step) result.push(i);
  }
  return result;
}

/**
 * Returns an array filled with `obj` for `n` times.
 * @param {number} n - Length of the array to create
 * @param {Object} obj - Object with which the array is to be filled
 * @returns {any[]}
 */
function withN (n, obj) {
  const result = new Array(n);
  while (n > 0) result[--n] = obj;
  return result;
}

/**
 * Creates an array with the result of `generator` called `n` times.
 *
 * `arr.genN(3, num.random) => [46,77,95]`
 * @param {number} n
 * @param {function} generator
 * @returns {any[]}
*/
function genN (n, generator) {
  const result = new Array(n);
  while (n > 0) result[--n] = generator(n);
  return result;
}

/**
 * Returns the element equal to the given search value or undefined.
 *
 * If defined, a converter function will be applied to compare an
 * array element with the search value
 *
 * If `returnClosestElement` is `true`, the element closest to the search value will be returned,
 * even if it is not equal.
 *
 * If `false`, only an exact match will be returned, otherwise undefined will be returned.
 * @param {any[]} array - The array in which to search
 * @param {Object} searchValue - The value to search for in `array`
 * @param {function} converter - The function used to compare array elements with `searchValue`
 * @param {Boolean} returnClosestElement - Whether only exact matches should be returned
 * @returns {Object}
 * @see {@link https://en.wikipedia.org/wiki/Binary_search_algorithm}
 */
function binarySearchFor (array, searchValue, converter, returnClosestElement = false) {
  if (!array || !Array.isArray(array)) return;

  let leftLimit = 0;
  let rightLimit = array.length - 1;

  while (leftLimit <= rightLimit) {
    const pivot = leftLimit + Math.floor((rightLimit - leftLimit) / 2);

    const compareValue = !converter ? array[pivot] : converter(array[pivot]);

    if (compareValue === searchValue) {
      return array[pivot];
    } else {
      if (compareValue > searchValue) {
        rightLimit = pivot - 1;
      } else {
        leftLimit = pivot + 1;
      }
    }
  }

  if (returnClosestElement) {
    // left and right limit point to two elements
    //   of which one must be the elemnt closest to the search value
    if (!array[leftLimit]) return array[rightLimit];
    if (!array[rightLimit]) return array[leftLimit];
    const compareValueL = !converter ? array[leftLimit] : converter(array[leftLimit]);
    const compareValueR = !converter ? array[rightLimit] : converter(array[rightLimit]);
    const diffOfElementL = Math.abs(compareValueL - searchValue);
    const diffOfElementR = Math.abs(compareValueR - searchValue);
    return diffOfElementL - diffOfElementR < 0 ? array[leftLimit] : array[rightLimit];
  }

  return undefined;
}

/**
 * Find the first occurence for which `iterator` returns a truthy value and
 * return *this* value, i.e. unlike `find` the iterator result and not the
 * element of the list is returned.
 * @param {any[]} arr
 * @param {function} iterator
 * @returns {Object}
 */
function findAndGet (arr, iterator) {
  let result;
  arr.find(function (ea, i) { return result = iterator(ea, i); });
  return result;
}

/**
 * Returns only the Objects in `arr` that have `key` as property.
 * @param {any[]} arr
 * @param {string} key
 * @returns {any[]}
 */
function filterByKey (arr, key) {
  return arr.filter(function (ea) { return !!ea[key]; });
}

/**
 * Returns an array that contains all elements of `arr` that contain/satisfy `test`.
 * `grep` stringifies all elements in `arr`.
 * @param {any[]} arr
 * @param {String|RegEx} test
 * @returns {any[]}
 */
function grep (arr, test) {
  if (typeof test === 'string') test = new RegExp(test, 'i');
  return arr.filter(e => String(e).match(test));
}

/**
 * Return an array containing all elements or `array` for which `mask` contains a truthy value at the same index.
 * @param {any[]} array - The array which should be subsetted.
 * @param {Boolean[]} mask - Array used for masking `array`.
 * @returns {any[]}
 */
function mask (array, mask) {
  return array.filter(function (_, i) { return !!mask[i]; });
}

/**
 * Returns an array of all elements of `array` for which `func` is falsy.
 * @param {Obejct[]} array - The array which should be subsetted.
 * @param {function} func - Function that is used for testing.
 * @param {Object} context - Acts as `this` when calling `func`.
 * @returns {any[]}
 */
function reject (array, func, context) {
  function iterator (val, i) { return !func.call(context, val, i); }
  return array.filter(iterator);
}

/**
 * Returns an array of all elements of `array` that do not have `key` as property.
 * @param {any[]} array
 * @param {string} key
 * @returns {any[]}
 */
function rejectByKey (array, key) {
  return array.filter(function (ea) { return !ea[key]; });
}

/**
 * Returns a copy of `array` without `elem`.
 * @param {any[]} array
 * @param {Object} elem
 * @returns {any[]}
 */
function without (array, elem) {
  return array.filter(val => val !== elem);
}

/**
 * Returns a copy of `array` without all elements in `otherArr`.
 * @param {any[]} array
 * @param {any[]} otherArr
 * @returns {any[]}
 */
function withoutAll (array, otherArr) {
  return array.filter(val => otherArr.indexOf(val) === -1);
}

/**
 * Returns `array` without duplicates.
 * @param {any[]} array
 * @param {boolean} sorted - Wether `array` is sorted. Used for optimizations.
 * @returns {any[]}
 */
function uniq (array, sorted) {
  if (!array.length) return array;

  const result = [array[0]];
  if (sorted) {
    for (let i = 1; i < array.length; i++) {
      const val = array[i];
      if (val !== result[result.length - 1]) { result.push(val); }
    }
  } else {
    for (let i = 1; i < array.length; i++) {
      const val = array[i];
      if (result.indexOf(val) === -1) { result.push(val); }
    }
  }
  return result;
}

/**
 * Like `arr.uniq` but with custom equality `comparator(a,b)`.
 * @param {any[]} array
 * @param {function} comparator - Function used to determine if a equals b
 * @param {Object} context - Used as `this` when calling `comparator`.
 * @returns {any[]}
 */
function uniqBy (array, comparator, context) {
  const result = array.slice();
  for (let i = result.length; i--;) {
    const item = array[i];
    for (let j = i + 1; j < result.length; j++) {
      if (comparator.call(context, item, result[j])) { result.splice(j--, 1); }
    }
  }
  return result;
}

/**
 * Like `arr.uniq` but with equality based on `array[index].key`.
 * @param {any[]} array
 * @param {string} key
 * @returns {any[]}
 */
function uniqByKey (array, key) {
  const seen = {}; const result = [];
  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    if (!seen[item[key]]) {
      seen[item[key]] = true;
      result.push(item);
    }
  }
  return result;
}

/**
 * Returns a copy of `array` with falsy values removed.
 * @param {any[]} array
 * @returns {any[]}
 */
function compact (array) {
  return array.filter(Boolean);
}

/**
 * Returns `array` with falsy values removed.
 * @param {any[]} array
 * @returns {any[]}
 */
function mutableCompact (array) {
  // fix gaps that were created with 'delete'
  let i = 0; let j = 0; const len = array.length;
  while (i < len) {
    if (array.hasOwnProperty(i)) array[j++] = array[i];
    i++;
  }
  while (j++ < len) array.pop();
  return array;
}

/**
 * Takes any number of lists as arguments. Combines them elment-wise.
 * ```
 * arr.zip([1,2,3], ["a", "b", "c"], ["A", "B"])
 * => [[1,"a","A"],[2,"b","B"],[3,"c",undefined]]
 * ```
 * @param arguments - Any number of lists
 * @returns {any[]}
 */
function zip (/* arr, arr2, arr3 */) {
  const args = Array.from(arguments);
  const array = args.shift();
  const iterator = typeof last(args) === 'function' // eslint-disable-line no-use-before-define
    ? args.pop()
    : function (x) { return x; };
  const collections = [array].concat(args).map(function (ea) { return Array.from(ea); });
  return array.map(function (value, index) {
    return iterator(pluck(collections, index), index); // eslint-disable-line no-use-before-define
  });
}
/**

/**
 * Returns a new array that contains an element of `arra` and `delim` alternating.
 * @param {any[]} array
 * @param {any} delim
 * @returns {any[]}
 */
function interpose (array, delim) {
  return array.reduce(function (xs, x) {
    if (xs.length > 0) xs.push(delim);
    xs.push(x); return xs;
  }, []);
}

/**
 * Calls `method` on each element in `array`, passing all arguments.
 * Often a handy way to avoid verbose `map` calls.
 * @param {any[]} array
 * @param {function} method - The method to invoke on all elements of `array`
 * @param {any} arg1
 * @param {any} arg2
 * @param {any} arg3
 * @param {any} arg4
 * @param {any} arg5
 * @param {any} arg6
 */
function invoke (array, method, arg1, arg2, arg3, arg4, arg5, arg6) {
  return array.map(function (ea) {
    return ea[method](arg1, arg2, arg3, arg4, arg5, arg6);
  });
}

/**
 * Returns `property` or undefined from each element of array. For quick `map`s and similar to `invoke`.
 * @param {any[]} array
 * @param {string} property - The property to return
 */
function pluck (array, property) {
  return array.map(ea => ea[property]);
}

/**
 * Returns true if each element in `array` is equal (`==`) to its
 * corresponding element in `otherArray`.
 * @param {any[]} array
 * @param {any[]} otherArray
 * @returns {boolean}
 */
function equals (array, otherArray) {
  const len = array.length;
  if (!otherArray || len !== otherArray.length) return false;
  for (let i = 0; i < len; i++) {
    if (array[i] && otherArray[i] && array[i].equals && otherArray[i].equals) {
      if (!array[i].equals(otherArray[i])) {
        return false;
      } else {
        continue;
      }
    }
    if (array[i] !== otherArray[i]) return false;
  }
  return true;
}

/**
 * Returns true if each element in `array` is structurally equal
 * (`lang.obj.equals`) to its corresponding element in `otherArray`.
 * @param {any[]} array
 * @param {any[]} otherArray
 * @param {any[]} m - Gets populated with memorization when this function is called as part of `obj.equal`
 * @returns {boolean}
 */
function deepEquals (array, otherArray, m) {
  const len = array.length;
  if (!otherArray || len !== otherArray.length) return false;
  for (let i = 0; i < len; i++) {
    if (!objectEquals(array[i], otherArray[i], m)) return false;
  }
  return true;
}

/**
 * Returns a boolean indicating whether or not `array` is sorted.
 * @param {any[]} array
 * @param {boolean} descending - indicating if `array` should be checked for descending or ascending order
 * @returns {boolean} wether `array` is sorted or not
 */
function isSorted (array, descending) {
  if (descending) {
    for (let i = 1; i < array.length; i++) { if (array[i - 1] < array[i]) return false; }
  } else {
    for (let i = 1; i < array.length; i++) { if (array[i - 1] > array[i]) return false; }
  }
  return true;
}

/**
 * Sorts `array` according to the elements value of `iterator`.
 * @param {any[]} array - the array to sort
 * @param {function} iterator - the function to use to sort `array`
 * @param {Object} context - Used as `this` when calling `iterator`
 */
function sortBy (array, iterator, context) {
  return pluck(
    array.map(function (value, index) {
      return { value: value, criteria: iterator.call(context, value, index) };
    }).sort(function (left, right) {
      const a = left.criteria; const b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }), 'value');
}

/**
 * Sorts array by the values of a given key.
 * @param {any[]} array - the array to sort
 * @param {string} key - the key to sort by
 */
function sortByKey (array, key) {
  return sortBy(array, ea => ea[key]);
}

function reversed (array) { return array.slice().reverse(); }

/**
 * Returns the matches of `re` for the elements in `arr`. Might include null items if `re` did not match.
 * @param {any[]} arr
 * @param {*} re - The refular expression to use
 * @param {*} stringifier - Used to stringify the elements of `arr`. Defaults to `String`.
 */
function reMatches (arr, re, stringifier) {
  // result might include null items if re did not match (usful for masking)
  // Example:
  //   var morphs = $world.withAllSubmorphsDo(function(x) { return x; ;
  //   morphs.mask(morphs.reMatches(/code/i))
  stringifier = stringifier || String;
  return arr.map(ea => stringifier(ea).match(re));
}

/**
 * Returns the first element of an array.
 */
function first (array) { return array[0]; }

/**
 * Returns the last element of an array.
 */
function last (array) { return array[array.length - 1]; }

/**
 * Rerturns the intersection of `array1` and `array2` according to set semantic.
 * @param {any[]} array1
 * @param {any[]} array2
 */
function intersect (array1, array2) {
  return uniq(array1).filter(item => array2.indexOf(item) > -1);
}

/**
 * Rerturns the union of `array1` and `array2` according to set semantic.
 * @param {any[]} array1
 * @param {any[]} array2
 */
function union (array1, array2) {
  const result = array1.slice();
  for (let i = 0; i < array2.length; i++) {
    const item = array2[i];
    if (result.indexOf(item) === -1) result.push(item);
  }
  return result;
}

/**
 * Inserts `item` at `index`. Mutating.
 * @param {any[]} array
 * @param {any} item
 * @param {number} index
 */
function pushAt (array, item, index) {
  array.splice(index, 0, item);
}

/**
 * Remove the element of `array` at `index`. Mutating.
 * @param {any[]} array
 * @param {number} index
 */
function removeAt (array, index) {
  array.splice(index, 1);
}

/**
 * Removes the first occurrence of `item` in `array`. Mutating.
 * @param {any[]} array
 * @param {any} item
 * @returns {any} item
 */
function remove (array, item) {
  const index = array.indexOf(item);
  if (index >= 0) removeAt(array, index);
  return item;
}

/**
 * Appends all `items` to `array`. Mutating.
 * @param {any[]} array
 * @param {any[]} items
 * @returns {any[]} array
 */
function pushAll (array, items) {
  array.push.apply(array, items);
  return array;
}

/**
 * Inserts all `items` at `idx`. Mutating.
 * @param {any[]} array
 * @param {anu[]} items
 * @param {number} idx
 */
function pushAllAt (array, items, idx) {
  array.splice.apply(array, [idx, 0].concat(items));
}

/**
 * Only appends `item` if its not already in `array`. Mutating.
 * @param {any[]} array
 * @param {any} item
 */
function pushIfNotIncluded (array, item) {
  if (!array.includes(item)) array.push(item);
}

/**
 * Replace the element `array[index]` with `item`. Mutating.
 * @param {any[]} array
 * @param {any} item
 * @param {number} index
 */
function replaceAt (array, item, index) {
  array.splice(index, 1, item);
}

/**
 * Removes all items. Mutating.
 * @param {any[]} array
 */
function clear (array) {
  array.length = 0; return array;
}

/**
 * Returns wether all elements in `list1` are in `list2`.
 * @param {any[]} list1
 * @param {any[]} list2
 * @returns {boolean}
 */
function isSubset (list1, list2) {
  for (let i = 0; i < list1.length; i++) {
    if (!list2.includes(list1[i])) { return false; }
  }
  return true;
}

/**
 * Iterates over array but instead of consecutively calling `iterator`,
 * `iterator` gets passed in the invocation for the next iteration step
 * as a function as first parameter. This allows to wait arbitrarily
 * between operation steps, great for managing dependencies between tasks.
 * ```
 * arr.doAndContinue([1,2,3,4], function(next, n) {
 *   alert("At " + n);
 *   setTimeout(next, 100);
 * }, function() { alert("Done"); })
 * ```
 * If the elements are functions you can leave out the iterator:
 * ```
 * arr.doAndContinue([
 *   function(next) { alert("At " + 1); next(); },
 *   function(next) { alert("At " + 2); next(); }
 * ], null, function() { alert("Done"); });
 ```
 * @param {any[]} array - The array to iterate over
 * @param {function} iterator - The function that is called for each element of `array`
 * @param {function} endFunc - A function called once after iterating over `array`
 * @param {Object} context - Bound to `this` in `iterator`
 */
function doAndContinue (array, iterator, endFunc, context) {
  endFunc = endFunc || NullFunction;
  context = context;
  iterator = iterator || function (next, ea, idx) { ea.call(context, next, idx); };
  return array.reduceRight(function (nextFunc, ea, idx) {
    return function () { iterator.call(context, nextFunc, ea, idx); };
  }, endFunc)();
}

/**
 * Calls `iterator` for every element in `array` and waits between iterator
 * calls `waitSecs`. Eventually `endFunc` is called. When passing a number n
 * as `optSynchronChunks`, only every nth iteration is delayed.
 * @param {any[]} array - The array to iterate
 * @param {function} iterator
 * @param {number} waitSecs - The number of seconds to wait between invocations of `iterator`
 * @param {function} endFunc - A function called once after iterating over `array`
 * @param {Object} context - Bound to `this` in `iterator`
 * @param {number} optSynchronChunks - Only wait after each `n`th element
 */
function nestedDelay (array, iterator, waitSecs, endFunc, context, optSynchronChunks) {
  endFunc = endFunc || function () {};
  return array.clone().reverse().reduce(function (nextFunc, ea, idx) {
    return function () {
      iterator.call(context, ea, idx);
      // only really delay every n'th call optionally
      if (optSynchronChunks && (idx % optSynchronChunks !== 0)) {
        nextFunc();
      } else {
        nextFunc.delay(waitSecs);
      }
    };
  }, endFunc)();
}

function forEachShowingProgress (/* array, progressBar, iterator, labelFunc, whenDoneFunc, context or spec */) {
  const args = Array.from(arguments);
  const array = args.shift();
  const steps = array.length;
  let progressBar; let iterator; let labelFunc; let whenDoneFunc; let context;
  let progressBarAdded = false;

  // init args
  if (args.length === 1) {
    progressBar = args[0].progressBar;
    iterator = args[0].iterator;
    labelFunc = args[0].labelFunction;
    whenDoneFunc = args[0].whenDone;
    context = args[0].context;
  } else {
    progressBar = args[0];
    iterator = args[1];
    labelFunc = args[2];
    whenDoneFunc = args[3];
    context = args[4];
  }
  if (!context) context = typeof window !== 'undefined' ? window : GLOB;
  if (!labelFunc) labelFunc = function (x) { return x; };

  // init progressbar
  if (!progressBar) {
    progressBarAdded = true;
    progressBar = $world
      ? $world.showLoadingIndicatorFor($world)
      : {
          value: null,
          label: null,
          remove: function () {}
        };
  }
  progressBar.progress = 0;

  // nest functions so that the iterator calls the next after a delay
  (array.reduceRight(function (nextFunc, item, idx) {
    return function () {
      try {
        progressBar.progress = (idx / steps);
        if (labelFunc) progressBar.label = (labelFunc.call(context, item, idx));
        iterator.call(context, item, idx);
      } catch (e) {
        console.error(
          'Error in forEachShowingProgress at %s (%s)\n%s\n%s',
          idx, item, e, e.stack);
      }
      delay(nextFunc, 0);
    };
  }, function () {
    progressBar.progress = 1;
    if (progressBarAdded) (function () { progressBar.remove(); }).delay(0);
    if (whenDoneFunc) whenDoneFunc.call(context);
  }))();

  return array;
}

/**
 * Swap the element at `array[index1]` with the one at `array[index2]`. Mutating.
 * @param {any[]} array
 * @param {number} index1
 * @param {number} index2
 */
function swap (array, index1, index2) {
  if (index1 < 0) index1 = array.length + index1;
  if (index2 < 0) index2 = array.length + index2;
  const temp = array[index1];
  array[index1] = array[index2];
  array[index2] = temp;
  return array;
}

/**
 * Shift the elements in `array` to the left `times` times.
 * @param {any[]} array
 * @param {number} times
 * @returns {any[]} - A copy of `array` rotated `times` times
 */
function rotate (array, times) {
  times = times || 1;
  return array.slice(times).concat(array.slice(0, times));
}

/**
 * Applies `iterator` to each element in `array` and puts the return value into a collection
 * associated to its stringified representation.
 * ```
 * // Example: Groups characters by how often they occur in a string
 * var chars = arr.from("Hello World");
 * arr.groupBy(arr.uniq(chars), function(c) {
 * return arr.count(chars, c); })
 * => {
 *   "1": ["H","e"," ","W","r","d"],
 *   "2": ["o"],
 *   "3": ["l"]
 * }
 * ```
 * @see lively.lang/Group
 * @param {any[]} array
 * @param {function} iterator
 * @param {Object} context - Bound to `this` when calling `iterator`
 */
function groupBy (array, iterator, context) {
  return Group.fromArray(array, iterator, context);
}

/**
 * @see lively.lang/array~groupBy
 * @param {any[]} array
 * @param {string} key
 */
function groupByKey (array, key) {
  return groupBy(array, ea => ea[key]);
}

/**
 * Partition `array` according to a condition specified in `iterator`.
 * Creates two partitions (condition is either `true` or `false`).
 * @param {any[]} array
 * @param {function} iterator
 * @param {Object} context - bound to `this` when calling `iterator`
 */
function partition (array, iterator, context) {
  iterator = iterator || function (x) { return x; };
  const trues = []; const falses = [];
  array.forEach(function (value, index) {
    (iterator.call(context, value, index) ? trues : falses).push(value);
  });
  return [trues, falses];
}

/**
 * Takes elements and fits them into subarrays (= batches) so that for
 * each batch `constrainedFunc` returns `true`. Note that `contrainedFunc`
 * should at least produce 1-length batches, otherwise an error is raised.
 * @param {any[]} array
 * @param {function} constrainedFunc
 * @param {Object} context - bound to `this` when calling `constrainedFunc`
 */
function batchify (array, constrainedFunc, context) {
  return findBatches([], array); // eslint-disable-line no-use-before-define

  function extractBatch (batch, sizes) {
    // Array -> Array -> Array[Array,Array]
    // case 1: no sizes to distribute, we are done
    if (!sizes.length) return [batch, []];
    const first = sizes[0]; const rest = sizes.slice(1);
    // if batch is empty we have to take at least one
    // if batch and first still fits, add first
    const candidate = batch.concat([first]);
    if (constrainedFunc.call(context, candidate)) return extractBatch(candidate, rest);
    // otherwise leave first out for now
    const batchAndSizes = extractBatch(batch, rest);
    return [batchAndSizes[0], [first].concat(batchAndSizes[1])];
  }

  function findBatches (batches, sizes) {
    if (!sizes.length) return batches;
    const extracted = extractBatch([], sizes);
    if (!extracted[0].length) {
      throw new Error('Batchify constrained does not ensure consumption ' +
              'of at least one item per batch!');
    }
    return findBatches(batches.concat([extracted[0]]), extracted[1]);
  }
}

/**
 * Decomposes `array` into sub-arrays with at most `tupleLength` elements.
 * @param {any[]} array
 * @param {number} tupleLength - description
 */
function toTuples (array, tupleLength) {
  // Creates sub-arrays with length `tupleLength`
  // Example:
  // arr.toTuples(["H","e","l","l","o"," ","W","o","r","l","d"], 4)
  // // => [["H","e","l","l"],["o"," ","W","o"],["r","l","d"]]
  tupleLength = tupleLength || 1;
  return range(0, Math.ceil(array.length / tupleLength) - 1).map(function (n) {
    return array.slice(n * tupleLength, n * tupleLength + tupleLength);
  }, array);
}

/**
 * Returns the number of permutations of the elements in `array`.
 * @function
 * @name permutations
 * @param {any[]} array
 * @returns number
 */
const permutations = (function () {
  function computePermutations (restArray, values) {
    return !restArray.length
      ? [values]
      : restArray.flatMap(function (ea, i) {
        return computePermutations(
          restArray.slice(0, i).concat(restArray.slice(i + 1)),
          values.concat([ea]));
      });
  }
  return function (array) { return computePermutations(array, []); };
})();

/**
 * Can be used to recursively create all combinations of elements in `n` arrays.
 * Given a "listOfListsOfValues" in the form of an array of arrays and
 * `pickIndices` list with the size of the number of arrays which indicates what
 * values to pick from each of the arrays. Returns a list with two lists:
 * 1. values picked from each of the arrays, 2. the next pickIndices or null if at end (no more combinations possible).
 * Needs to be called recursively to enumerate alls combinations.
 * ```
 * // Example:
 * var searchSpace = [["a", "b", "c"], [1,2]];
 * arr.combinationsPick(searchSpace, [0,1]);
 * // => [["a",2], [1,0]]
 * arr.combinationsPick(searchSpace, [1,0]);
 * // => [["b",1], [1,1]]
 * ```
 * @param {any[][]} listOfListsOfValues
 * @param {number[]} pickIndices
 */
function combinationsPick (listOfListsOfValues, pickIndices) {
  const values = listOfListsOfValues.map(function (subspace, i) {
    return subspace[pickIndices[i]];
  });
  let nextState = pickIndices.slice();
  for (let i = listOfListsOfValues.length; i--; i >= 0) {
    const subspace = listOfListsOfValues[i]; const nextIndex = nextState[i] + 1;
    if (subspace[nextIndex]) { nextState[i] = nextIndex; break; } else if (i === 0) { nextState = undefined; break; } else { nextState[i] = 0; }
  }
  return [values, nextState];
}

/**
 * Given a "listOfListsOfValues" in the form of an array of arrays,
 * retrieve all the combinations by picking one item from each array.
 * This basically creates a search tree, traverses it and gathers all node
 * values whenever a leaf node is reached.
 * @param {any[][]} listOfListsOfValues
 * @returns {any[][]}
 */
function combinations (listOfListsOfValues) {
  const size = listOfListsOfValues.reduce(function (prod, space) { return prod * space.length; }, 1);
  let searchState = listOfListsOfValues.map(function (_) { return 0; });
  const results = new Array(size);
  for (let i = 0; i < size; i++) {
    const result = combinationsPick(listOfListsOfValues, searchState);
    results[i] = result[0];
    searchState = result[1];
  }
  return results;
}

/**
 * @function
 * @name take
 * @param {any[]} arr
 * @param {number} n
 * @returns {any[]} The first `n` elements of `arr`.
 */
function take (arr, n) { return arr.slice(0, n); }

/**
 * @function
 * @name drop
 * @param {any[]} arr
 * @param {number} n
 * @returns {any[]} `arr` without the first `n` elements.
 */
function drop (arr, n) { return arr.slice(n); }

/**
 * Return elements from `arr` in a list until `fun` is falsy while iterating over `arr`.
 * @param {any[]} arr
 * @param {function} fun
 * @param {Object} context - bound to `this` when calling `fun`
 */
function takeWhile (arr, fun, context) {
  let i = 0;
  for (; i < arr.length; i++) { if (!fun.call(context, arr[i], i)) break; }
  return arr.slice(0, i);
}

/**
 * Return elements from `arr` in a list starting from `fun` being falsy while iterating over `arr`.
 * @param {any[]} arr
 * @param {function} fun
 * @param {Object} context - bound to `this` when calling `fun`
 */
function dropWhile (arr, fun, context) {
  let i = 0;
  for (; i < arr.length; i++) { if (!fun.call(context, arr[i], i)) break; }
  return arr.slice(i);
}

/**
 * Randomizes the order of elements in `array`. Non-mutating.
 * @param {any[]} array
 */
function shuffle (array) {
  const unusedIndexes = range(0, array.length - 1);
  const shuffled = Array(array.length);
  for (let i = 0; i < array.length; i++) {
    const shuffledIndex = unusedIndexes.splice(
      Math.round(Math.random() * (unusedIndexes.length - 1)), 1);
    shuffled[shuffledIndex] = array[i];
  }
  return shuffled;
}

/**
 * Return the element of `array` which has the highest value according to `iterator`.
 * @param {any[]} array
 * @param {function} iterator
 * @param {Object} context - bound to `this` when calling `iterators`
 */
function max (array, iterator, context) {
  iterator = iterator || function (x) { return x; };
  let result;
  array.reduce(function (max, ea, i) {
    const val = iterator.call(context, ea, i);
    if (typeof val !== 'number' || val <= max) return max;
    result = ea; return val;
  }, -Infinity);
  return result;
}

/**
 * Return the element of `array` which has the smallest value according to `iterator`.
 * @param {any[]} array
 * @param {function} iterator
 * @param {Object} context - bound to `this` when calling `iterators`
 */
function min (array, iterator, context) {
  iterator = iterator || (x => x);
  return max(array, (ea, i) => -iterator.call(context, ea, i));
}

/**
 * Return the sum of the elements of `array`.
 * @param {number[]} array
 */
function sum (array) {
  let sum = 0;
  for (let i = 0; i < array.length; i++) { sum += array[i]; }
  return sum;
}

/**
 * Returns the number of times `item` occurs in `array`.
 * @param {any[]} array
 * @param {any} item
 */
function count (array, item) {
  return array.reduce(function (count, ea) {
    return ea === item ? count + 1 : count;
  }, 0);
}

/**
 * When called without `binspec`, returns `data` partitioned into lists containing two elements each.
 * `binspec` can be an array of `n` numbers. If given, `histogram` will create `n-1` bins, using the provided values in `binspec` as threshholds.
 * The `n-1` bins will be returned as a list. T
 * @param {number[]} data - The data to be partitioned
 * @param {number[]} binSpec - The threshholds used to partition the data. `binspec` should be sorted ascending.
 */
function histogram (data, binSpec) {
  if (typeof binSpec === 'undefined' || typeof binSpec === 'number') {
    const binNumber = binSpec || (function sturge () {
      return Math.ceil(Math.log(data.length) / Math.log(2) + 1);
    })(data);
    const binSize = Math.ceil(Math.round(data.length / binNumber));
    return range(0, binNumber - 1).map(function (i) {
      return data.slice(i * binSize, (i + 1) * binSize);
    });
  } else if (binSpec instanceof Array) {
    // bins specifies n threshold values that will create n-1 bins.
    // Each data value d is placed inside a bin i if:
    // threshold[i] >= d && threshold[i+1] < d
    const thresholds = binSpec;
    return data.reduce(function (bins, d) {
      if (d < thresholds[1]) { bins[0].push(d); return bins; }
      for (let i = 1; i < thresholds.length; i++) {
        if (d >= thresholds[i] && (!thresholds[i + 1] || d <= thresholds[i + 1])) {
          bins[i].push(d); return bins;
        }
      }
      throw new Error(`Histogram creation: Cannot group data ${d} into thresholds ${thresholds}`);
    }, range(1, thresholds.length).map(function () { return []; }));
  }
}

/**
 * Returns a shallow copy of `array`.
 * @param {any[]} array
 */
function clone (array) {
  return [].concat(array);
}

/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Enable chaining (lively.lang) for methods on Array.prototype.
Do not use these directly, use the methods provided by Array.prototype instead.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */

function filter (array, iterator, context) {
  return array.filter(iterator, context);
}

function find (array, iterator, context) {
  return array.find(iterator, context);
}

function map (array, iterator, context) {
  return array.map(iterator, context);
}

function flat (array, optDepth) {
  return array.flat(optDepth);
}

function flatMap (array, iterator, context) {
  return array.flatMap(iterator, context);
}

function slice (array, start, end) {
  return array.slice(start, end);
}

export {
  range,
  withN,
  genN,
  binarySearchFor,
  findAndGet,
  filterByKey,
  grep,
  mask,
  reject,
  rejectByKey,
  without,
  withoutAll,
  uniq,
  uniqBy,
  uniqByKey,
  compact,
  mutableCompact,
  zip,
  interpose,
  invoke,
  pluck,
  equals,
  deepEquals,
  isSorted,
  sortBy,
  sortByKey,
  reversed,
  reMatches,
  first,
  last,
  intersect,
  union,
  pushAt,
  removeAt,
  remove,
  pushAll,
  pushAllAt,
  pushIfNotIncluded,
  replaceAt,
  clear,
  isSubset,
  doAndContinue,
  nestedDelay,
  swap,
  rotate,
  groupBy,
  groupByKey,
  partition,
  batchify,
  toTuples,
  permutations,
  combinationsPick,
  combinations,
  take,
  drop,
  takeWhile,
  dropWhile,
  shuffle,
  max,
  min,
  sum,
  count,
  histogram,
  clone,
  flat,
  filter,
  find,
  map,
  flatMap,
  slice,
  forEachShowingProgress
};