import Immutable from 'immutable'
// TODO: Do not import entire lodash
import lodash from 'lodash'
const { Map, List, Set, OrderedSet, Repeat, Seq, Iterable } = Immutable
const identity = (x) => x
const not = (fun) => {
return (...args) => !fun(...args)
}
const truthy = (x) => {
if (x) {
return true
}
}
const splitPath = (path) => {
return lodash.filter(path.split(/[\[\]\.]+/))
}
const MAX_LIST_LENGTH = 4294967295
const MAX_LIST_INDEX = MAX_LIST_LENGTH - 1
/**
* Creates an iterable of elements split into groups the length of `size`.
* If `iterable` can't be split evenly, the final chunk will be the remaining
* elements.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to process.
* @param {number} [size=1] The length of each chunk
* @returns {Iterable} Returns the new iterable of chunks.
* @example
*
* let list = ['a', 'b', 'c', 'd']
* _.chunk(list, 2)
* // => [['a', 'b'], ['c', 'd']]
*
* _.chunk(list, 3)
* // => [['a', 'b', 'c'], ['d']]
*/
function chunk(iterable, size=1) {
let current = 0
if (_.isEmpty(iterable)) {
return iterable
}
let result = List()
while (current < iterable.size) {
result = result.push(iterable.slice(current, current + size))
current += size
}
return result
}
/**
* Creates an iterable with all falsey values removed. The values `false`, `null`,
* `0`, `""`, `undefined`, and `NaN` are falsey.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to compact.
* @param {Iterable} Returns the new iterable of filtered values.
* @example
*
* _.compact([0, 1, false, 2, '', 3])
* // => [1, 2, 3]
*/
function compact(iterable) {
return iterable.filter(truthy)
}
/**
* Creates an Iterable of the same type concatenating `iterable`
* with any additional iterables and/or values.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to concatenate.
* @param {...*} [values] The values to concatenate.
* @param {Iterable} Returns the new concatenated sequence.
* @example
*
* _.concat(['a'], 2, [3], [[4]])
* // => ['a', 2, 3, [4]]
*/
function concat(...args) {
let [first, ...other] = args
return first.concat(...other)
}
/**
* Creates an Iterable of `iterable` values not included in the other given iterables
* The order of result values is determined by the order they occur
* in the first iterable.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to inspect.
* @param {...Iterable} [values] The values to exclude.
* @returns {Iterable} Returns the iterable of filtered values.
* @see _.without, _.xor
* @example
*
* _.difference([2, 1], [2, 3])
* // => [1]
*/
function difference(iterable, values) {
const valueSet = Set(values)
return iterable.filterNot((x) => valueSet.has(x))
}
/**
* This method is like `_.difference` except that it accepts `iteratee` which
* is invoked for each element of `iterable` and `values` to generate the criterion
* by which they're compared. Result values are chosen from the first iterable.
* The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to inspect.
* @param {...Iterable} [values] The values to exclude.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Iterable} Returns the new iterable of filtered values.
* @example
*
* _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor)
* // => [1.2]
*/
function differenceBy(iterable, values, iteratee) {
if (!iterable) return iterable
const valueSet = Set(values.map(iteratee))
return iterable.filterNot((x) => valueSet.has(iteratee(x)))
}
/**
* Fills elements of `iterable` with `value` from `start` up to, but not
* including, `end`.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to fill.
* @param {*} value The value to fill `iterable` with.
* @param {number} [start=0] The start position.
* @param {number} [end=iterable.size] The end position.
* @returns {Iterable} Returns `iterable`.
* @example
*
* let list = [1, 2, 3]
*
* _.fill(list, 'a')
* // => ['a', 'a', 'a']
*
* _.fill([4, 6, 8, 10], '*', 1, 3)
* // => [4, '*', '*', 10]
*/
function fill(iterable, value, start=0, end=iterable.size) {
let num = end - start
return iterable.splice(start, num, ...Repeat(value, num))
}
/**
* Returns a sequence of unique values that are included in all given iterables
* The order of result values is determined by the order they occur in the first iterable.
*
* @static
* @memberOf _
* @category Iterable
* @param {...Iterable} [iterables] The iterables to inspect.
* @returns {Iterable} Returns the new iterable of intersecting values.
* @example
*
* _.intersection([2, 1], [2, 3])
* // => Seq [2]
*/
function intersection(...iterables) {
if (_.isEmpty(iterables)) return OrderedSet()
let result = OrderedSet(iterables[0])
for (let iterable of iterables.slice(1)) {
result = result.intersect(iterable)
}
return result.toSeq()
}
/**
* Removes elements from `iterable` corresponding to `indexes` and returns an
* iterable of removed elements.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to modify.
* @param {...(number|number[])} [indexes] The indexes of elements to remove.
* @returns {Iterable} Returns the new iterable of removed elements.
* @example
*
* let iterable = ['a', 'b', 'c', 'd']
* _.pullAt(iterable, [1, 3])
* // => ['b', 'd']
*/
function pullAt(iterable, indexes) {
return indexes.map((x) => iterable.get(x))
}
function baseSortedIndexBy(iterable, value, iteratee, retHighest) {
value = iteratee(value)
let setLow
let low = 0
let high = iterable ? iterable.size : 0
const valIsNaN = value !== value
const valIsNull = value === null
const valIsUndefined = value === undefined
while (low < high) {
const mid = Math.floor((low + high) / 2)
const computed = iteratee(iterable.get(mid))
const othIsDefined = computed !== undefined
const othIsNull = computed === null
const othIsReflexive = computed === computed
if (valIsNaN) {
setLow = retHighest || othIsReflexive
} else if (valIsUndefined) {
setLow = othIsReflexive && (retHighest || othIsDefined)
} else if (valIsNull) {
setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull)
} else if (othIsNull) {
setLow = false
} else {
setLow = retHighest ? (computed <= value) : (computed < value)
}
if (setLow) {
low = mid + 1
} else {
high = mid
}
}
return Math.min(high, MAX_LIST_INDEX)
}
/**
* Determines the lowest index at which `value`
* should be inserted into `iterable` in order to maintain its sort order.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The sorted iterable to inspect.
* @param {*} value The value to evaluate.
* @returns {number} Returns the index at which `value` should be inserted
* into `iterable`.
* @example
*
* _.sortedIndex([30, 50], 40)
* // => 1
*/
function sortedIndex(iterable, value) {
// TODO: Use binary search for numbers
return baseSortedIndexBy(iterable, value, identity)
}
/**
* This method is like `_.sortedIndex` except that it accepts `iteratee`
* which is invoked for `value` and each element of `iterable` to compute their
* sort ranking. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The sorted iterable to inspect.
* @param {*} value The value to evaluate.
* @param {Function} [iteratee=_.identity]
* The iteratee invoked per element.
* @returns {number} Returns the index at which `value` should be inserted
* into `iterable`.
* @example
*
* let maps = [{ 'x': 4 }, { 'x': 5 }]
*
* _.sortedIndexBy(maps, { 'x': 4 }, (o) => o.x)
* // => 0
*
* // The `_.property` iteratee shorthand.
* _.sortedIndexBy(maps, { 'x': 4 }, 'x')
* // => 0
*/
function sortedIndexBy(iterable, value, iteratee=identity) {
return baseSortedIndexBy(iterable, value, iteratee)
}
/**
* Creates an iterable of unique values, in order, from all given iterables.
*
* @static
* @memberOf _
* @category Iterable
* @param {...Iterable} [iterables] The iterables to inspect.
* @returns {Seq} Returns the new iterable of combined values.
* @example
*
* _.union([2], [1, 2])
* // => Seq [2, 1]
*/
function union(...iterables) {
return OrderedSet(Seq(iterables).flatten(true)).toSeq()
}
/**
* Creates a duplicate-free sequence of the iterable, in which only the first
* occurrence of each element is kept.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to inspect.
* @returns {Seq} Returns the new duplicate free sequence.
* @example
*
* _.uniq([2, 1, 2])
* // => Seq [2, 1]
*/
function uniq(iterable) {
return OrderedSet(iterable).toSeq()
}
/**
* Creates a sequence of unique values that is the
* [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)
* of the given iterables. The order of result values is determined by the order
* they occur in the iterables.
*
* @static
* @memberOf _
* @category Iterable
* @param {...Iterable} [iterables] The iterables to inspect.
* @returns {Seq} Returns the sequence of filtered values.
* @see _.difference
* @example
*
* _.xor([2, 1], [2, 3])
* // => [1, 3]
*/
function xor(...iterables) {
// TODO: Return original type
let [first, ...others] = iterables
return Set().union(...iterables).subtract(Set(first).intersect(...others)).toSeq()
}
/**
* Creates a Map composed of keys generated from the results of running
* each element of `iterable` through `iteratee`. The corresponding value of
* each key is the number of times the key was returned by `iteratee`. The
* iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @category iterable
* @param {Function} [iteratee=_.identity]
* The iteratee to transform keys.
* @example
*
* _.countBy([6.1, 4.2, 6.3], Math.floor)
* // => { '4': 1, '6': 2 }
*/
function countBy(iterable, iteratee=identity) {
let result = Map()
for (let value of iterable) {
result = result.update(iteratee(value), 0, (x) => x + 1)
}
return result
}
/**
* Creates a map composed of keys generated from the results of running
* each element of `iterable` through `iteratee`. The order of grouped values
* is determined by the order they occur in `iterable`. The corresponding
* value of each key is a List of elements responsible for generating the
* key. The iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @category iterable
* @param {Iterable} iterable The iterable to iterate over.
* @param {Function} [iteratee=_.identity]
* The iteratee to transform keys.
* @returns {Map} Returns the composed aggregate map.
* @example
*
* _.groupBy([6.1, 4.2, 6.3], Math.floor)
* // => { '4': [4.2], '6': [6.1, 6.3] }
*/
function groupBy(iterable, iteratee=identity) {
// TODO: How does looped reassignment like this behave in terms of memory usage?
let result = Map()
for (let entry of iterable.entrySeq()) {
let [key, value] = entry
const keyed = iteratee(value)
const collector = result.get(keyed, List()).push(key)
result = result.set(keyed, collector)
}
return result
}
/**
* Creates a Map composed of keys generated from the results of running
* each element of `iterable` through `iteratee`. The corresponding value of
* each key is the last element responsible for generating the key. The
* iteratee is invoked with one argument: (value).
*
* @static
* @memberOf _
* @category iterable
* @param {Iterable} iterable The iterable to iterate over.
* @param {Function} [iteratee=_.identity]
* The iteratee to transform keys.
* @returns {Map} Returns the composed aggregate map.
* @example
*
* let keys = [
* { 'dir': 'left', 'code': 97 },
* { 'dir': 'right', 'code': 100 }
* ]
*
* _.keyBy(keys, (o) => String.fromCharCode(o.code))
* // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }
*
* _.keyBy(keys, 'dir')
* // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }
*/
function keyBy(iterable, iteratee=identity) {
let result = Map()
for (let value of iterable) {
result = result.set(iteratee(value), value)
}
return result
}
/**
* Creates a list of elements split into two groups, the first of which
* contains elements `predicate` returns truthy for, the second of which
* contains elements `predicate` returns falsey for. The predicate is
* invoked with one argument: (value).
*
* @static
* @memberOf _
* @category iterable
* @param {Iterable} iterable The iterable to iterate over.
* @param {Function} [predicate=_.identity] The function invoked per iteration.
* @returns {List} Returns the list of grouped elements.
* @example
*
* let users = [
* { 'user': 'barney', 'age': 36, 'active': false },
* { 'user': 'fred', 'age': 40, 'active': true },
* { 'user': 'pebbles', 'age': 1, 'active': false }
* ]
*
* _.partition(users, (o) => o.active)
* // => objects for [['fred'], ['barney', 'pebbles']]
*
*/
function partition(iterable, predicate=identity) {
let truths = List(), falsehoods = List()
for (let value of iterable) {
if (predicate(value)) {
truths = truths.push(value)
} else {
falsehoods = falsehoods.push(value)
}
}
return List.of(truths, falsehoods)
}
/**
* Gets a random element from `iterable`.
*
* @static
* @memberOf _
* @category iterable
* @param {Iterable} iterable The iterable to sample.
* @returns {*} Returns the random element.
* @example
*
* _.sample([1, 2, 3, 4])
* // => 2
*/
function sample(iterable) {
let index = lodash.random(0, iterable.size - 1)
return iterable.get(index)
}
/**
* Gets `n` random elements at unique keys from `iterable` up to the
* size of `iterable`.
*
* @static
* @memberOf _
* @category iterable
* @param {Iterable} iterable The iterable to sample.
* @param {number} [n=1] The number of elements to sample.
* @returns {List} Returns the random elements.
* @example
*
* _.sampleSize([1, 2, 3], 2)
* // => List [3, 1]
*
* _.sampleSize([1, 2, 3], 4)
* // => List [2, 3, 1]
*/
function sampleSize(iterable, n=1) {
let index = -1
let result = List(iterable)
const length = result.size
const lastIndex = length - 1
while (++index < n) {
const rand = lodash.random(index, lastIndex)
const value = result.get(rand)
result = result.set(rand, result.get(index))
result = result.set(index, value)
}
return result.slice(0, Math.min(length, n))
}
/**
* Creates an iterable of shuffled values, using a version of the
* [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle).
*
* @static
* @memberOf _
* @category iterable
* @param {Iterable} iterable The iterable to shuffle.
* @returns {Iterable} Returns the new shuffled iterable.
* @example
*
* _.shuffle([1, 2, 3, 4])
* // => [4, 1, 3, 2]
*/
function shuffle(iterable) {
let indexes = lodash.shuffle(lodash.range(iterable.size))
return _.pullAt(iterable, List(indexes))
}
function size(iterable) {
return Iterable.isIterable(iterable) ? iterable.size : lodash.size(iterable)
}
/**
* Creates an array of values corresponding to `paths` of `iterable`.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to iterate over.
* @param {...(string|string[])} [paths] The property paths of elements to pick.
* @returns {Iterable} Returns the picked values.
* @example
*
* let iterable = { 'a': [{ 'b': { 'c': 3 } }, 4] }
*
* _.at(iterable, ['a[0].b.c', 'a[1]'])
* // => [3, 4]
*/
function at(map, paths) {
return paths.map((path) => {
return map.getIn(splitPath(path))
})
}
/**
* Creates new iterable with all properties of source iterables that resolve
* to `undefined`. Source iterables are applied from left to right.
* Once a key is set, additional values of the same key are ignored.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The destination iterable.
* @param {...Iterable} [sources] The source iterables.
* @returns {Iterable} Returns `iterable`.
* @see _.defaultsDeep
* @example
*
* _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 })
* // => { 'a': 1, 'b': 2 }
*/
function defaults(map, ...sources) {
return map.mergeWith((prev, next) => {
return prev === undefined ? next : prev
}, ...sources)
}
/**
* This method is like `_.defaults` except that it recursively assigns
* default properties.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The destination iterable.
* @param {...Iterable} [sources] The source iterables.
* @returns {Iterable} Returns `iterable`.
* @see _.defaults
* @example
*
* _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } })
* // => { 'a': { 'b': 2, 'c': 3 } }
*/
function defaultsDeep(map, ...sources) {
return map.mergeDeepWith((prev, next) => {
return prev === undefined ? next : prev
}, ...sources)
}
/**
* Creates an iterable composed of the inverted keys and values of `iterable`.
* If `iterable` contains duplicate values, subsequent values overwrite
* property assignments of previous values.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The iterable to invert.
* @returns {Iterable} Returns the new inverted iterable.
* @example
*
* let iterable = { 'a': 1, 'b': 2, 'c': 1 }
*
* _.invert(iterable)
* // => { '1': 'c', '2': 'b' }
*/
function invert(map) {
return map.mapEntries((entry) => lodash.reverse(entry))
}
function baseOmitBy(map, predicate, omit=true) {
let keyPredicate = (entry) => predicate(entry[0])
let entries = map.entrySeq()
let filtera = omit ? entries.filterNot(keyPredicate) : entries.filter(keyPredicate)
return filtera.fromEntrySeq()
}
/**
* The opposite of `_.pick` this method creates a new iterable omitting the
* supplied keys
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The source iterable.
* @param {...(string|string[])} [props] The property identifiers to omit.
* @returns {Iterable} Returns the new iterable.
* @example
*
* let map = { 'a': 1, 'b': '2', 'c': 3 }
*
* _.omit(map, ['a', 'c'])
* // => { 'b': '2' }
*/
function omit(map, props) {
props = Set(props)
return _.omitBy(map, (key) => {
return props.has(key)
})
}
/**
* The opposite of `_.pick` this method creates a new iterable omitting the
* supplied keys `predicate` doesn't return truthy for. The predicate is invoked with two
* arguments: (value, key).
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The source iterable.
* @param {Function} [predicate=_.identity] The function invoked per property.
* @returns {Iterable} Returns the new iterable.
* @example
*
* let iterable = { 'a': 1, 'b': '2', 'c': 3 }
*
* _.omitBy(iterable, _.isNumber)
* // => { 'b': '2' }
*/
function omitBy(map, predicate=identity) {
return baseOmitBy(map, predicate, true)
}
/**
* Creates an iterable composed of the picked `iterable` properties.
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The source iterable.
* @param {...(string|string[])} [props] The property identifiers to pick.
* @returns {Iterable} Returns the new iterable.
* @example
*
* let iterable = { 'a': 1, 'b': '2', 'c': 3 }
*
* _.pick(iterable, ['a', 'c'])
* // => { 'a': 1, 'c': 3 }
*/
function pick(map, props) {
props = Set(props)
return _.pickBy(map, (key) => {
return props.has(key)
})
}
/**
* Creates an iterable composed of the `iterable` properties `predicate` returns
* truthy for. The predicate is invoked with two arguments: (value, key).
*
* @static
* @memberOf _
* @category Iterable
* @param {Iterable} iterable The source iterable.
* @param {Function} [predicate=_.identity] The function invoked per property.
* @returns {Iterable} Returns the new iterable.
* @example
*
* let iterable = { 'a': 1, 'b': '2', 'c': 3 }
*
* _.pickBy(iterable, _.isNumber)
* // => { 'a': 1, 'c': 3 }
*/
function pickBy(map, predicate=identity) {
return baseOmitBy(map, predicate, false)
}
function isEmpty(iterable) {
return !iterable || iterable.length === 0 || iterable.size === 0
}
function noop() {}
/**
* The object containing all `immutable-lodash` functions.
* @example
*
* // Important note for examples:
*
* // Rather than specifying every example data structure like so:
*
* let map = List([Map({ 'a': 1 }), Map({ 'b': 2 }), Map({ 'c': 3 }) ])
*
* // Immutable Lists and Maps are implied:
*
* let map = [ { 'a': 1 }, { 'b': 2 }, { 'c': 3 } ]
*
* // This is to make the it easier for you to see what every function is actually doing.
*
* // As a rule, `immutable-lodash` functions try to return the type of object that was passed in, or,
* // where it makes more sense, a lazy `Seq`. The docs specify a "Returns:" type for every function,
* // which you can see below.
*
* @namespace
*/
const _ = {}
_.chunk = chunk
_.compact = compact
_.concat = concat
_.difference = difference
_.differenceBy = differenceBy
_.fill = fill
_.intersection = intersection
_.pullAt = pullAt
_.sortedIndex = sortedIndex
_.sortedIndexBy = sortedIndexBy
_.union = union
_.uniq = uniq
_.xor = xor
_.countBy = countBy
_.groupBy = groupBy
_.keyBy = keyBy
_.partition = partition
_.sample = sample
_.sampleSize = sampleSize
_.shuffle = shuffle
_.size = size
_.at = at
_.defaults = defaults
_.defaultsDeep = defaultsDeep
_.invert = invert
_.omit = omit
_.omitBy = omitBy
_.pick = pick
_.pickBy = pickBy
_.isEmpty = isEmpty
_.noop = noop
const iterableFunctions = lodash.pull(
lodash.functions(_),
'some', 'size', 'isEmpty', 'noop'
)
// Guard against empty-ish first arguments
lodash.forEach(iterableFunctions, (fun) => {
const func = _[fun]
_[fun] = (first, ...args) => {
if (_.isEmpty(first)) {
return first
}
return func(first, ...args)
}
})
export default _