core/collections.js

/**
 * @module core/collections
 * @memberOf core
 */

const T = require("./types");

if (!global._X_LOOP_BREAK_) {
    global._X_LOOP_BREAK_ = Symbol("BREAK_LOOP");
    global._X_ANY_ = Symbol("ANY");
    global._X_ALL_ = Symbol("ALL");
}
/**
 * Break from functional loops: forEach, filter, ...
 * @type {symbol}
 */
const BREAK = global._X_LOOP_BREAK_;
/**
 * Match Any: Used as value in predicate object
 * @type {symbol}
 */
const ANY = global._X_ANY_;
const ALL = global._X_ALL_;
const UNSAFE_PROPS = ['__proto__', 'constructor', '__defineGetter__', '__defineSetter__', 'prototype'];

/**
 * Get item by index from multiple source types
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} s - source
 * @param {Number|String} i - item index
 * @returns {any|null|undefined}
 */
function item(s, i) {
    if (!T.isVal(s)) return undefined;
    if (T.isObj(s)) return s[i]
    if (T.isStr(s)) return s[i]
    if (T.isArr(s)) return s[i]
    else return s.item(i)
}

/**
 * Source contains value or key:value
 *
 * @param {Array|Object|String} src
 * @param {any} value
 * @param {String|undefined} [key]
 * @returns {boolean}
 */
function contains(src, value, key) {
    if (!T.isVal(src)) return false;
    if (!T.isArr(src) && T.isObj(src)) return src[key] === value;
    return src.indexOf(value) >= 0;
}

/**
 * Add item(s) to source
 *
 * @param {Array|{add:Function}} src - source
 * @param {any} v - values
 */
function add(src, ...v) {
    if (T.isArr(src)) {
        forEach(v, (vi) => src.push(vi))
    } else if (T.isMutableList(src)) {
        src.add(v);
    }
}

/**
 * Remove item(s) from source
 *
 * @param {Array|String} src
 * @param {...any} it
 * @returns {boolean}
 */
function remove(src, ...it) {
    it = flatMap(it);
    let any = false;
    forEach(it, (item) => {
        let idx = src.indexOf(item);
        if (idx >= 0) {
            any = true
            src.splice(idx, 1);
        }
    });

    return any;
}

/**
 * Toggles items in source
 *
 * @param {Array} src
 * @param {...any} c
 */
function toggle(src, ...c) {
    if (c.length === 0) return;
    c = flatMap(c);
    let idx = undefined;
    if (c.length === 1) {
        if (!remove(src, c)) {
            add(src, c[0])
        }
    } else {
        c.push(c[0]);
        let any = false;
        forEach(src, (cl, i) => {
            idx = c.indexOf(cl);
            if (idx >= 0 && c.length > (idx + 1)) {
                any = true;
                src[i] = c[idx + 1]
            }
        });
        if (!any) {
            src.push(c[0])
        }
    }
}

function objMatchOne(o, match) {
    let m = Object.keys(match);
    for (let k of m) {
        // if (!T.isObj(o[k])) continue;
        if (match[k] === ANY && o.hasOwnProperty(k)) return true;
        if (match[k] === o[k]) return true;
    }
    return false
}

function objMatchAll(o, match) {
    let m = Object.keys(match);
    for (let k of m) {
        // if (!T.isObj(o[k])) return false;
        if (match[k] === ANY) continue;
        if (match[k] !== o[k]) return false
    }
    return true
}

function predicate(f, def = () => true, inc = true) {
    if (T.isUnd(f)) return def;
    if (T.isFun(f)) return f;
    else if (f instanceof RegExp) return (v) => !T.isObj(v) ? f.test(v.toString()) : false;
    else if (T.isObj(f)) {
        if (Object.keys(f).length === 0) return def;
        return inc ? (v) => objMatchOne(v, f) : (v) => objMatchAll(v, f);
    } else return (v) => v === f;
}

function funOrKey(f) {
    if (T.isUnd(f)) return (v) => v;
    if (T.isFun(f)) return f;
    if (T.isStr(f)) {
        const key = f;
        f = (v) => v[key];
    }
    throw Error(`Predicate ${f} cannot be of type ${typeof f}`)
}

/**
 * Create empty instance of given source
 *
 * @param {any} src - source object
 * @param {any} def - default value for primitives
 * @returns {any} empty instance of src
 */
function emptyOf(src, def = {}) {
    if (T.isStr(src)) return "";
    if (T.isList(src)) return [];
    if (T.isObj(src)) {
        if (T.isEl(src)) {
            if (src.nodeType === 3 || src.nodeType === 8) {
                return document.createTextNode(src.textContent);
            } else {
                return document.createElement(src.tagName);
            }
        }
        if (src.__proto__)
            return Object.create(src.__proto__);
        return {};
    }
    return def
}

/**
 * Type-agnostic concat
 *
 * @param {Array|String|Object} target
 * @param {Array|String|Object} source
 * @returns {Array|String|Object}
 */
function concat(target, source) {
    if (T.isStr(target)) {
        return target.concat(source);
    }
    if (T.isArr(target)) {
        return target.concat(source);
    }

    for (let k of Object.keys(source)) {
        if (!target[k])
            target[k] = source[k];
    }
    return target
}

/**
 * Polyfill-like for Object.values (uses native method if available)
 *
 * @param {Object} obj
 * @returns {Array|undefined} - array of values
 */
function objectValues(obj) {
    if (Object.values) {
        return Object.values(obj);
    } else {
        return map(obj, (v)=>v)
    }
}

/**
 * Index for loop from "init" to "finish"(included) by "step"
 *
 * @param {Function} func
 * @param {Number} init
 * @param {Number} finish
 * @param {Number} [step]
 */
function forN(func, init=0, finish=0, step=init<finish?1:-1) {
    if (step>0) {
        for (; init<=finish; init+=step) {
            func(init)
        }
    }
    for (; init>=finish; init+=step) {
        func(init)
    }
}

/**
 * forEach(left) with limited range
 *
 * @param {Array|Object|NodeList|HTMLCollection} src
 * @param {Function} func
 * @param {Number} [start]
 * @param {Number?} [end]
 * @returns {*|number}
 */
function forEachRange(src, func, start = 0, end) {
    if (!T.isArr(src) || !T.isStr(src)) {
        let keys = Object.keys(src);
        end = end || keys.length - 1
        for (let i = start; i <= end; i++) {
            let r = func(src[keys[i]], keys[i], i, src);
            if (r === BREAK) return i;
        }
        return end;
    }
    end = end || src.length;
    for (let i = start; i < end; i++) {
        let r = func(item(src, i), i, i, src);
        if (r === BREAK) return i;
    }
    return end
}

/**
 * Type-agnostic forEach loop
 *
 * @param src
 * @param func
 * @returns {number|*}
 */
function forEach(src, func) {
    if (!T.isVal(src)) return -1;
    if (!T.isArr(src) || !T.isStr(src) || !T.isList(src)) {
        let i = 0;
        let keys = Object.keys(src);
        const len = keys.length;
        for (; i < len; i++) {
            // let r = ;
            const k = keys[i], v = src[k];
            if (func(v, k, i, src) === BREAK) return i;
        }
        return i;
    }
    const len = src.length;
    if (!T.isArr(src)) {
        for (let i = 0; i < len; i++) {
            const v = src[i];
            let r = func(v, i, i, src);
            if (r === BREAK) return i;
        }
    } else {
        for (let i = 0; i < len; i++) {
            const v = item(src, i);
            let r = func(v, i, i, src);
            if (r === BREAK) return i;
        }
    }
    return src.length
}

/**
 * Type-agnostic forEachRight loop
 *
 * @param src
 * @param func
 * @param range
 * @returns {number|*}
 */
function forEachRight(src, func, range = []) {
    if (!T.isArr(src) || !T.isStr(src)) {
        let i = 0;
        let keys = Object.keys(src);
        for (let i = keys.length - 1; i >= 0; i--) {
            if (i < range[1]) continue;
            if (i >= range[0]) return i;
            let r = func(src[keys[i]], keys[i], i, src);
            if (r === BREAK) return i;
        }
        return i;
    }
    for (let i = src.length - 1; i >= 0; i--) {
        let r = func(item(src, i), i, i, src);
        if (r === BREAK) return i;
    }
    return src.length
}

/**
 * Extended version of native indexOf method, using {@link forEach}
 * @see forEach
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Object|Array} pred - predicate function/{key:value}/[keys]
 * @returns {number}
 */
function firstIndex(src, pred) {
    pred = predicate(pred, () => true);
    let r = -1;
    forEach(src, function (v, k, i) {
        r = pred(v, k, i, src);
        if (r === true) {
            r = i;
            return BREAK;
        }
    })
    return r;
}

/**
 * Extended version of native first method, using {@link firstIndex}
 * @see firstIndex
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Object|Array} pred - predicate function/{key:value}/[keys]
 * @returns {*}
 */
function first(src, pred) {
    return item(src, firstIndex(src, pred))
}

/**
 * Returns true if first item matches predicate
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Object|Array} pred - predicate function/{key:value}/[keys] pred
 * @returns {boolean|*}
 */
function startsWith(src, pred) {
    if (T.isStr(src) && T.isStr(pred)) {
        return src.indexOf(pred) === 0
    }
    pred = predicate(pred, () => true);
    return pred(first(src))
}

/**
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Array|Object} pred
 * @returns {number}
 */
function lastIndex(src, pred) {
    pred = predicate(pred, () => true);
    let r = -1;
    forEachRight(src, function (v, k, i) {
        r = pred(v, k, i);
        if (r === true) {
            r = i;
            return BREAK;
        }
    })
    return r;
}

/**
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Array|Object} pred
 * @returns {*}
 */
function last(src, pred) {
    return src[lastIndex(src, pred)]
}

/**
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Array|Object} pred
 * @returns {boolean|*}
 */
function endsWith(src, pred) {
    if (T.isStr(src) && T.isStr(pred)) {
        return src.indexOf(pred) === src.length - pred.length
    }
    pred = predicate(pred, () => true);
    return pred(last(src))
}

/**
 * Reverse array-like src
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @returns {Array|Object|String|NodeList|HTMLCollection} - reversed src
 */
function reverse(src) {
    if (T.isArr(src)) return src.reverse();
    let rev = "";
    forEachRight(src, function (it) {
        rev += it;
    });
    return rev;
}

/**
 * A more versatile alternative to native "some" method
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Array|Object} pred
 * @returns {boolean}
 */
function any(src, pred) {
    // if (!func){
    //     if (T.isArr(src) || T.isStr(src)) return src.length>0;
    //     return Object.keys(src).length>0;
    // }
    let fn = predicate(pred);
    let r = false;
    forEach(src, function (v, k, i, src) {
        r = fn(v, k, i, src);
        if (r === true) return BREAK;
    });
    return r;
}

/**
 * A more versatile alternative to native "every" method
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src
 * @param {Function|Array|Object} pred
 * @returns {boolean}
 */
function all(src, pred) {
    pred = predicate(pred, () => true);
    let r = true;
    forEach(src, function (v, k, i, src) {
        r = pred(v, k, i, src);
        if (r === false) return BREAK;
    })
    return r;
}

function filterStr(src, pred, right = false, omit = false) {
    pred = predicate(pred, () => true);
    let res = "";
    let loop = right ? forEachRight : forEach;
    loop(src, function (v, k, i) {
        if (!pred || pred(v, k, i, src) === omit) res += v;
    });
    return res;
}

function filterObj(src, pred, right = false, omit = false) {
    if (T.isArr(pred)) {
        let a = Object.assign({}, pred)
        if (omit) pred = (v, k) => !any(a, k);
        else pred = (v, k) => any(a, k);
    } else
        pred = predicate(pred, () => true);
    let res = {};
    // let loop = right ? ForEachRight : ForEach;
    const keys = Object.keys(src);
    const len = keys.length;

    if (!right) {
        for (let i = 0; i < len; i++) {
            const k = keys[i]
            const v = src[k]
            if (pred(v, k, i, src) !== omit) res[k] = v;
        }
    } else {
        for (let i = len-1; i >=0; i--) {
            const k = keys[i]
            const v = src[k]
            if (pred(v, k, i, src) !== omit) res[k] = v;
        }
    }

    // loop(src, function (v, k, i) {
    //     if (pred(v, k, i, src) !== omit) res[k] = v;
    // });
    return res;
}

function filterArr(src, pred, right = false, omit = false) {
    if (T.isArr(pred)) {
        let a = Object.assign([], pred)
        if (omit) pred = (v, k, i) => !any(a, i);
        else pred = (v, k, i) => any(a, i);
    } else
        pred = predicate(pred, () => true);
    let res = [];
    const len = src.length;
    if (!right) {
        for (let i = 0; i < len; i++) {
            const v = src[i]
            if (pred(v, i, i, src) !== omit) {
                res.push(v);
            }
        }
    } else {
        for (let i = len - 1; i >= 0; i--) {
            const v = src[i]
            if (!pred(v, i, i, src) !== omit) {
                res.push(v);
            }
        }
    }
    return res;
}

/**
 * A more versatile alternative to native "filter" method
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src - source
 * @param {Function|Array|Object} pred - predicate function/{key:value}/[keys]
 * @param {boolean} [right] - reverse loop
 * @returns {Array|Object|String}
 */
function filter(src, pred, right = false) {
    if (T.isStr(src)) return filterStr(src, pred, right);
    if (T.isArr(src) || T.isList(src)) return filterArr(src, pred, right);
    return filterObj(src, pred, right);
}

/**
 * Opposite of filter, returns items not matching predicate
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src - source
 * @param {Function|Array|Object} pred - predicate function/{key:value}/[keys]
 * @param {boolean} [right] - reverse loop
 * @returns {Array|Object|String}
 */
function omit(src, pred, right = false) {
    if (T.isStr(src)) return filterStr(src, pred, right, true);
    if (T.isArr(src) || T.isList(src)) return filterArr(src, pred, right, true);
    return filterObj(src, pred, right, true);
}

/**
 * {@link filter} in reverse order (right)
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src - source
 * @param {Function|Array|Object} pred - predicate function/{key:value}/[keys]
 * @returns {Array|Object|String}
 */
function filterRight(src, pred) {
    return filter(src, pred, true);
}

/**
 * Find max index
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} list
 * @param {Function|String} pred - predicate function/key
 * @return {number}
 */
function maxIndex(list, pred) {
    pred = funOrKey(pred);
    let mx;
    let index = -1;
    forEach(list, function (i, ix) {
        let x = pred(i, ix);
        if (!mx) {
            mx = x;
            index = ix;
        } else if (x >= mx) {
            mx = x;
            index = ix;
        }
    });
    return index;
}

/**
 * Find max
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} list
 * @param {Function|String} pred - predicate function/key
 * @return {number}
 */
function max(list, pred) {
    return list[maxIndex(list, pred)];
}

/**
 * Find min index
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} list
 * @param {Function|String} pred - predicate function/key
 * @return {number}
 */
function minIndex(list, pred) {
    pred = funOrKey(pred);
    let mn;
    let index = -1;
    forEach(list, function (i, ix) {
        let x = pred(i, ix);
        if (!mn) {
            mn = x;
            index = ix;
        } else if (x <= mn) {
            mn = x;
            index = ix;
        }
    });
    return index;
}

/**
 * Find min
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} list
 * @param {Function|String} pred - predicate function/key
 * @return {number}
 */
function min(list, pred) {
    return list[minIndex(list, pred)];
}

function mapArr(src, func, right = false) {
    let res = [];
    // let loop = right ? ForEachRight : ForEach;
    const len = src.length;
    if (!right) {
        for (let i=0; i<len; i++) {
            let r = func(src[i], i, src);
            if (r === BREAK) break;
            if (!T.isUnd(r))
                res.push(r);
        }
    } else {
        for (let i=len; i>=0; i--) {
            let r = func(src[i], i, src);
            if (r === BREAK) break;
            if (!T.isUnd(r))
                res.push(r);
        }
    }
    // loop(src, function (a, i) {
    //     let r = func(a, i, src);
    //     if (!T.isUnd(r))
    //         res.push(r);
    // });
    return res;
}

function mapObj(src, func, right = false) {
    let res = {};
    // let loop = right ? ForEachRight : ForEach;
    const keys = Object.keys(src);
    const len = src.length;
    if (!right) {
        for (let i=0; i<len; i++) {
            const k = keys[i];
            const v = src[k];
            let r = func(v, k, i, src);
            if (r === BREAK) break;
            if (!T.isUnd(r))
                res[k] = r
        }
    } else {
        for (let i=len; i>=0; i--) {

            const k = keys[i];
            const v = src[k];
            let r = func(v, k, i, src);
            if (r === BREAK) break;
            if (!T.isUnd(r))
                res[k] = r
        }
    }
    // loop(src, function (v, k, i) {
    //     let r = func(v, k, i, src);
    //     if (!T.isUnd(r))
    //         res[k] = r
    // });
    return res;
}

/**
 * A more versatile alternative to native "map" method
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src - source
 * @param {Function|Array|Object} transform - transformer function
 * @param {boolean} [right] - reverse loop
 * @returns {Array|Object|String}
 */
function map(src, transform, right = false) {
    transform = predicate(transform, (v) => v);
    if (T.isArr(src)) return mapArr(src, transform, right);
    else if (T.isObj(src)) return mapObj(src, transform, right);
}

/**
 * A more versatile alternative to native "flatMap" method, flattens first level items
 *
 * @param {Array|Object|String|NodeList|HTMLCollection} src - source
 * @param {Function|Array|Object} [transform] - transformer function
 * @returns {Array|Object|String}
 */
function flatMap(src, transform) {
    let res;
    if (T.isStr(src)) res = ""
    else if (T.isArr(src)) res = []
    else res = {};
    forEach(src, function (a, i) {
        let f;
        if (!!transform) {
            f = transform(a, i, src);
        } else {
            if (!T.isArr(res) && T.isObj(res)) {
                f = {};
                f[i] = a;
            } else {
                f = a
            }
        }
        res = concat(res, f);
    });
    return res;
}

/**
 * A more versatile alternative to native "reduce" method
 *
 * @param {any} src - source
 * @param {Function|Array|Object} func - reducer function
 * @param {any} [acc] - accumulator
 * @returns {any}
 */
function reduce(src, func, acc = src) {
    if (T.isUnd(func)) {
        func = (rs, v) => rs + v
    }
    forEach(src, (v, k, src) => {
        acc = func(acc, v, k, src);
    });
    return acc
}

/**
 * {@link reduce} in reverse
 * @see reduce
 * @param {any} src - source
 * @param {Function|Array|Object} func - reducer function
 * @param {any} [acc] - accumulator
 * @returns {any}
 */
function reduceRight(src, func, acc = src) {
    forEachRight(src, (v, k, src) => {
        acc = func(acc, v, k, src);
    });
}

/**
 * Extract single key-value pairs from object
 *
 * @param object
 * @return {Object[]}
 */
function keyValuePairs(object) {
    let entries = [];
    forEach(object, (v, k) => {
        entries.push(T.dict(k, v));
    });
    return entries;
}

/**
 * Like Object.entries extracts key-value pairs as [[key1,value1],[key2,value2],...]
 *
 * @param {Object} object
 * @return {Array[]}
 */
function entries(object) {
    let entries = [];
    forEach(object, (v, k) => {
        entries.push([k, v]);
    });
    return entries;
}

/**
 * Translate(rename) object fields using dict parameter, omitting undefined keys
 * @param {Object} source
 * @param {Object|Array} dict
 * @return {Array|Object|String}
 */
function translateObject(source, dict) {
    if (T.isArr(dict)) {
        filter(source, dict);
    }
    const keys = Object.keys(dict);
    let res = filter(source, keys);
    for (let key of keys) {
        res[dict[key]] = res[key];
        delete res[key];
    }
    return res;
}

/**
 * Recursive deep merge {@param target} with {@param source}
 *
 * @param {Object|Array} target
 * @param {Object|Array} source
 * @param {Array} excludeKeys - keys to be skipped while merging
 * @param maxDepth - maximum recursive depth
 * @param allowUnsafeProps - allow unsafe properties like __proto__
 * @return {Object|Array}
 */
function deepMerge(target,
                   source,
                   {
                       excludeKeys = [],
                       maxDepth = 999,
                       allowUnsafeProps = false
                   } = {excludeKeys: [], maxDepth: 999, allowUnsafeProps: false},
                   depth = 0) {
    if (depth >= maxDepth) return target;
    forEach(source, (v, k) => {
        if (excludeKeys && contains(excludeKeys, k)) return;
        if (allowUnsafeProps && contains(UNSAFE_PROPS, k)) return;
        if (T.isObj(source[k])) {
            target[k] = deepMerge(emptyOf(source[k]), source[k], {excludeKeys, maxDepth, depth: depth + 1});
        } else
            target[k] = v
    });
    return target;
}

/**
 * Recursive deep clone {@param source}
 * @see deepMerge
 *
 * @param {Object|Array} source
 * @param {Array} excludeKeys - keys to be skipped while merging
 * @param {Number} [maxDepth] - maximum recursive depth
 * @param {boolean} [allowUnsafeProps] - allow unsafe properties like __proto__
 * @return {Object|Array}
 */
function deepClone(source,
                   {
                       excludeKeys = [],
                       maxDepth = 999,
                       allowUnsafeProps = false
                   } = {excludeKeys: [], maxDepth: 999, allowUnsafeProps: false},) {
    return deepMerge(emptyOf(source), source, {excludeKeys, maxDepth, allowUnsafeProps})
}

function _keyGen(key, fnName) {
    let keyGen = key
    if (T.isStr(key)) {
        keyGen = (item)=> item[key]
    } else if (T.isArr(key)) {
        keyGen = (item) => reduce(item, (res, v, k)=> {
            if (contains(key, k)) {
                return res + v;
            }
        }, "")
    } else if (!T.isFun(key)) {
        throw Error(`${fnName} key only accepts: String, [String,...], Function`)
    }
    return keyGen;
}

/**
 * Join arrays of objects based on a generated or static key
 *
 * @param {Function|String} key
 * @param {Object[]} lists
 * @return {Object}
 */
function join(key, ...lists) {
    if (lists.length === 0) return [];
    if (!all(lists, T.isArr)) throw Error("Join only accepts arrays of data!");
    let keyGen = _keyGen(key, "Join")
    const joined = {};
    forEach(lists, (list)=>{
        forEach(list, (item) => {
            const joinKey = keyGen(item);
            const presentValue = joined[joinKey];
            if (!presentValue) {
                joined[joinKey] = item;
            } else {
                joined[joinKey] = concat(presentValue, item)
            }
        });
    })

    return (joined)

}

/**
 * Group arrays based on generated or static key(s)
 *
 * @param {Function|String} key
 * @param {Object[]} lists
 * @return {Object}
 */
function groupBy(key, ...lists) {
    if (lists.length === 0) return {};
    if (!all(lists, T.isArr)) throw Error("GroupBy only accepts arrays of data!");
    let keyGen = _keyGen(key, "GroupBy")
    const group = {};
    forEach(lists, (list)=>{
        forEach(list, (item) => {
            const joinKey = keyGen(item);
            const presentValue = group[joinKey];
            if (!presentValue) {
                group[joinKey] = [item];
            } else {
                group[joinKey] = concat(presentValue, item)
            }
        });
    })

    return (group)

}

// noinspection JSUnusedGlobalSymbols
module.exports = {
    ANY, ALL, BREAK, item, contains, add, remove, toggle, objMatchOne, objMatchAll,
    deepMerge, deepClone, forN, forEachRange, forEach, forEachRight, firstIndex, first,
    startsWith, lastIndex, last, endsWith, reverse, any, all, filter, filterRight, reduce, reduceRight,
    map, flatMap, keyValuePairs, entries, maxIndex, max, minIndex, min,
    translateObject, omit, join, groupBy, objectValues
}