adam.js

/*
 * adam
 * https://github.com/gamtiq/adam
 *
 * Copyright (c) 2014-2020 Denis Sikuler
 * Licensed under the MIT license.
 */


/**
 * Functions to create, process and test objects.
 * 
 * @module adam
 */


"use strict";

/*jshint latedef:nofunc*/

var getOwnPropertyNames, getPropertySymbols, getPrototypeOf;

/*jshint laxbreak:true*/
/*
 * Return own property names of given object.
 * 
 * @param {Object} obj
 *      Object whose own property names should be returned.
 * @return {Array}
 *      Own property names of the given object.
 */
getOwnPropertyNames = typeof Object.getOwnPropertyNames === "function"
                        ? Object.getOwnPropertyNames
: function getOwnPropertyNames(obj) {
    var result = [],
        sKey;
    for (sKey in obj) {
        if (obj.hasOwnProperty(sKey)) {
            result.push(sKey);
        }
    }
    return result;
};

/*
 * Return prototype of given object.
 * 
 * @param {Object} obj
 *      Object whose prototype should be returned.
 * @return {Object}
 *      The prototype of the given object.
 */
getPrototypeOf = typeof Object.getPrototypeOf === "function"
                    ? Object.getPrototypeOf
: function getPrototypeOf(obj) {
    /*jshint proto:true*/
    return obj 
            ? (obj.constructor
                ? obj.constructor.prototype
                : (obj.__proto__ || Object.prototype)
                )
            : null;
};
/*jshint laxbreak:false*/


if (typeof Object.getOwnPropertySymbols === "function") {
    /**
     * Return list of all symbol property keys for given object including keys from prototype chain.
     * 
     * This function is defined only when `Object.getOwnPropertySymbols` is available.
     * 
     * @param {Object} obj
     *      Object to be processed.
     * @return {Array}
     *      List of all found symbol property keys.
     * @alias module:adam.getPropertySymbols
     */
    getPropertySymbols = function getPropertySymbols(obj) {
        var exceptList = {},
            getOwnPropertySymbols = Object.getOwnPropertySymbols,
            getPrototypeOf = Object.getPrototypeOf,
            result = [],
            nI, nL, propName, symbolList;
        if (obj && typeof obj === "object") {
            do {
                symbolList = getOwnPropertySymbols(obj);
                for (nI = 0, nL = symbolList.length; nI < nL; nI++) {
                    propName = symbolList[nI];
                    if (! (propName in exceptList)) {
                        result.push(propName);
                        exceptList[propName] = true;
                    }
                }
                obj = getPrototypeOf(obj);
            } while (obj);
        }
        return result;
    };
}


/**
 * Return class of given value (namely value of internal property `[[Class]]`).
 * 
 * @param {Any} value
 *      Value whose class should be determined.
 * @return {String}
 *      String indicating value class.
 * @alias module:adam.getClass
 */
function getClass(value) {
    var sClass = Object.prototype.toString.call(value);
    return sClass.substring(8, sClass.length - 1);
}

/**
 * Check whether given value is a `Map` or a `WeakMap`.
 * 
 * @param {Any} value
 *      Value that should be checked.
 * @param {Boolean} [bWeak=false]
 *      By default the passed value is only checked for `Map`.
 *      If you specify `true` for `bWeak`, the value will be also checked for `WeakMap`.
 * @return {Boolean}
 *      `true` when the passed value is a `Map` or `WeakMap`, otherwise `false`.
 * @alias module:adam.isMap
 * @see {@link module:adam.getClass getClass}
 */
function isMap(value, bWeak) {
    var sClass = getClass(value);
    return sClass === "Map" || (Boolean(bWeak) && sClass === "WeakMap");
}

/**
 * Return type of given value.
 * 
 * @param {Any} value
 *      Value whose type should be determined.
 * @return {String}
 *      For `NaN` - `'nan'`, for `null` - `'null'`, otherwise - result of `typeof` operator.
 * @alias module:adam.getType
 */
function getType(value) {
    /*jshint laxbreak:true*/
    var sType = typeof value;
    return value === null
            ? "null"
            : (sType === "number" && isNaN(value) ? "nan" : sType);
}

/**
 * Return number of all or filtered fields of specified object or map.
 * 
 * @param {Object} obj
 *      Object or map to be processed.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter specifying fields that should be counted (see {@link module:adam.checkField checkField})
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 * @return {Integer}
 *      Number of all or filtered fields of specified object.
 * @alias module:adam.getSize
 * @see {@link module:adam.getFields getFields}
 */
function getSize(obj, settings) {
    return getFields(obj, settings).length;
}

/**
 * Check whether number of all or filtered fields of specified object/map is more than the given value.
 * 
 * @param {Object} obj
 *      Object or map to be checked.
 * @param {Number} nValue
 *      Value that should be used for comparison with number of fields.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter specifying fields that should be counted (see {@link module:adam.checkField checkField})
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 * @return {Boolean}
 *      `true`, when number of all or filtered fields is more than the given value, otherwise `false`.
 * @alias module:adam.isSizeMore
 * @see {@link module:adam.getFields getFields}
 */
function isSizeMore(obj, nValue, settings) {
    /*jshint unused:false*/
    if (settings) {
        settings = copy(settings, {});
    }
    else {
        settings = {};
    }
    nValue++;
    settings.limit = nValue;
    return getFields(obj, settings).length === nValue;
}

/**
 * Check whether given value is an empty value i.e. `null`, `undefined`, `0`, empty object, empty array,
 * empty map, empty set or empty string.
 * 
 * @param {Any} value
 *      Value to be checked.
 * @return {Boolean}
 *      `true` if the value is an empty value, otherwise `false`.
 * @alias module:adam.isEmpty
 * @see {@link module:adam.getClass getClass}
 * @see {@link module:adam.isSizeMore isSizeMore}
 */
function isEmpty(value) {
    /*jshint eqeqeq:false, eqnull:true, laxbreak:true*/
    var sClass = getClass(value);
    return value == null
            || value === 0
            || value === ""
            || (typeof value === "object" && ! isSizeMore(value, 0))
            || (sClass === "Array" && value.length === 0)
            || ((sClass === "Map" || sClass === "Set") && value.size === 0);
}

/**
 * Check whether given value has (or does not have) specified kind (type or class).
 * 
 * @param {Any} value
 *      Value to be checked.
 * @param {String} sKind
 *      Type or class for check. Can be any value that is returned by {@link module:adam.getType getType} 
 *      and {@link module:adam.getClass getClass}, or one of the following:
 *      
 *    * `empty` - check whether the value is empty (see {@link module:adam.isEmpty isEmpty})
 *    * `even` - check whether the value is an even integer
 *    * `false` - check whether the value is a false value
 *    * `infinity` - check whether the value is a number representing positive or negative infinity
 *    * `integer` - check whether the value is an integer number
 *    * `negative` - check whether the value is a negative number
 *    * `numeric` - check whether the value is a number or a string that can be converted to number
 *    * `odd` - check whether the value is an odd integer
 *    * `positive` - check whether the value is a positive number
 *    * `real` - check whether the value is a real number
 *    * `true` - check whether the value is a true value
 *    * `zero` - check whether the value is `0`
 *    
 *      If exclamation mark (`!`) is set before the kind, it means that the check should be negated
 *      i.e. check whether given value does not have the specified kind.
 *      For example, `!real` means: check whether the value is not a real number.
 * @return {Boolean}
 *      `true` if value has the specified kind (or does not have when the check is negated), otherwise `false`.
 * @alias module:adam.isKindOf
 * @see {@link module:adam.getClass getClass}
 * @see {@link module:adam.getType getType}
 * @see {@link module:adam.isEmpty isEmpty}
 */
function isKindOf(value, sKind) {
    /*jshint laxbreak:true*/
    var bNegate = sKind.charAt(0) === "!",
        sType = getType(value),
        bInfinity, bInteger, bResult;
    if (bNegate) {
        sKind = sKind.substring(1);
    }
    bResult= sType === sKind
                || getClass(value) === sKind
                || (sKind === "true" && Boolean(value))
                || (sKind === "false" && ! value)
                || (sKind === "empty" && isEmpty(value))
                || (sKind === "numeric"
                    && ( sType === "number"
                            || (sType === "string" && value && ! isNaN(Number(value))) ) )
                || (sType === "number"
                    && ((sKind === "zero" && value === 0)
                        || (sKind === "positive" && value > 0)
                        || (sKind === "negative" && value < 0)
                        || ((bInfinity = (value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY)) && (sKind === "infinity"))
                        || (! bInfinity 
                                && ( ((bInteger = (value === Math.ceil(value))) && sKind === "integer")
                                    || (bInteger && sKind === "even" && value % 2 === 0)
                                    || (bInteger && sKind === "odd" && value % 2 !== 0)
                                    || (! bInteger && sKind === "real") )
                            )
                        )
                    );
    return bNegate ? ! bResult : bResult;
}

/**
 * Check whether the field of given object/map/array/set corresponds to specified condition(s) or filter(s).
 * 
 * @param {Object} obj
 *      Object, map (including WeakMap), array or set (including WeakSet) to be processed.
 * @param {String | Symbol} field
 *      Field that should be checked.
 * @param {Any} filter
 *      A filter or array of filters specifying conditions that should be checked. A filter can be:
 *      
 *    * a function; if the function returns a true value it means that the field corresponds to this filter;
 *      the following parameters will be passed into the function: field value, field name, reference to the object and operation settings
 *    * a regular expression; if the field value (converted to string) matches the regular expression it means
 *      that the field corresponds to this filter
 *    * a string; value can be one of the following:
 *      - `own` - if the field is own property of the object it means that the field corresponds to this filter
 *      - `!own` - if the field is not own property of the object it means that the field corresponds to this filter
 *      - any other value - is used as the check when calling {@link module:adam.isKindOf isKindOf};
 *        if {@link module:adam.isKindOf isKindOf} returns `true` for the given field value and the filter
 *        it means that the field corresponds to this filter
 *    * an object (referred below as condition);
 *      - if the object has `and` or `or` field, its value is used as subfilter and will be passed in recursive call of `checkField`
 *        as value of `filter` parameter; the field name (`and` or `or`) will be used as value of `filterConnect` setting (see below);
 *        if the result of the recursive call is `true` it means that the field corresponds to this filter
 *      - if the object has `field` field, its value is used as filter for the field name (property key) that is being checked;
 *        the filter is applied to the field name (property key) in recursive call of `checkField`
 *        as if the key is a tested field value of special object that is created for the check purposes
 *        (i.e. `checkField({field: field}, "field", filter.field, {filterConnect: settings.filterConnect})`)
 *      - if the object has `value` field, its value is used as filter; if the field value strictly equals to the filter value
 *      - if the object has `inside` field, its value is used as filter; if the filter value "contains" the field value
 *        it means that the field corresponds to this filter;
 *        the filter value can be:
 *        + an array or a string; in such case `indexOf` is used to check presence of the field value in the filter value;
 *        + a set; in such case `has` is used to check presence of the field value in the filter value;
 *        + a map or an object; when `key` field is specified in the condition, the filter value will be checked
 *           for presence of such key/value pair; if `true` is set as value for `key` field,
 *           value of `field` parameter will be used as the key;
 *           when `key` field is absent in the condition, the field value will be checked for presence
 *           in the list of values of the map/object;
 *        + any other value; in such case the field value is not inside the filter value;
 *      - in any other case if the field value strictly equals to the object it means that the field corresponds to this filter
 *    * any other value; if the field value strictly equals to the filter value it means that the field corresponds to this filter
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported (setting's default value is specified in parentheses):
 *     
 *   * `filterConnect`: `String` (`and`) - a boolean connector that should be used when array of filters is specified
 *      in `filter` parameter; valid values are the following: `and`, `or` (case-insensitive); any other value is treated as `and`
 *   * `value`: `Any` - a value that should be used for check instead of field's value
 * @return {Boolean}
 *      `true` if the field corresponds to specified filter(s), otherwise `false`.
 * @alias module:adam.checkField
 * @see {@link module:adam.getClass getClass}
 * @see {@link module:adam.isKindOf isKindOf}
 */
function checkField(obj, field, filter, settings) {
    /*jshint laxbreak:true*/
    var sClass = getClass(obj),
        bMap = sClass === "Map" || sClass === "WeakMap",
        bSet = sClass === "Set" || sClass === "WeakSet",
        test = true,
        bAnd, data, nI, nL, sKey, sType, value;
    if (! settings) {
        settings = {};
    }
    if ("value" in settings) {
        value = settings.value;
    }
    else if (bMap) {
        value = obj.get(field);
    }
    else if (bSet) {
        value = field;
    }
    else {
        value = obj[field];
    }
    bAnd = settings.filterConnect;
    bAnd = typeof bAnd !== "string" || bAnd.toLowerCase() !== "or";
    if (getClass(filter) !== "Array") {
        filter = [filter];
    }
    for (nI = 0, nL = filter.length; nI < nL; nI++) {
        test = filter[nI];
        switch (getClass(test)) {
            case "Function":
                test = Boolean(test(value, field, obj, settings));
                break;
            case "String":
                if (test === "own") {
                    test = obj[bMap || bSet ? "has" : "hasOwnProperty"](field);
                }
                else if (test === "!own") {
                    test = ! obj[bMap || bSet ? "has" : "hasOwnProperty"](field);
                }
                else {
                    test = isKindOf(value, test);
                }
                break;
            case "RegExp":
                test = test.test(typeof value === "symbol" ? value.toString() : value);
                break;
            case "Object":
                if ("and" in test) {
                    test = checkField(obj, field, test.and, {filterConnect: "and"});
                }
                else if ("or" in test) {
                    test = checkField(obj, field, test.or, {filterConnect: "or"});
                }
                else if ("field" in test) {
                    test = checkField({field: field}, "field", test.field, {filterConnect: settings.filterConnect});
                }
                else if ("value" in test) {
                    test = test.value === value;
                }
                else if ("inside" in test) {
                    data = test.inside;
                    sType = getClass(data);
                    switch (sType) {
                        case "Array":
                        case "String":
                            test = data.indexOf(value) > -1;
                            break;
                        case "Map":
                        case "Object":
                            sKey = test.key;
                            if (sKey) {
                                if (sKey === true) {
                                    sKey = field;
                                }
                                test = sType === "Map"
                                    ? (data.has(sKey) && data.get(sKey) === value)
                                    : ((sKey in data) && data[sKey] === value);
                            }
                            else {
                                test = sType === "Map"
                                    ? (Array.from(data.values()).indexOf(value) > -1)
                                    : (getValueKey(data, value) !== null);
                            }
                            break;
                        case "Set":
                            test = data.has(value);
                            break;
                        default:
                            test = false;
                    }
                }
                else {
                    test = test === value;
                }
                break;
            default:
                test = test === value;
        }
        if ((bAnd && ! test) || (! bAnd && test)) {
            break;
        }
    }
    return test;
}

/**
 * Check whether the value corresponds to specified condition(s) or filter(s).
 * 
 * This function is a "wrap" for the following code:
 * ```js
 * checkField({value: value}, "value", filter, settings);
 * ```
 * 
 * @param {Any} value
 *      Value that should be checked.
 * @param {Any} filter
 *      A filter or array of filters specifying conditions that should be checked.
 *      See {@link module:adam.checkField checkField} for details.
 * @param {Object} [settings]
 *     Operation settings. See {@link module:adam.checkField checkField} for details.
 * @return {Boolean}
 *      `true` if the value corresponds to specified filter(s), otherwise `false`.
 * @alias module:adam.checkValue
 * @see {@link module:adam.checkField checkField}
 */
function checkValue(value, filter, settings) {
    return checkField({value: value}, "value", filter, settings);
}

/**
 * Return list of all keys of specified object/map/array/set.
 * 
 * @param {Any} value
 *      Value that should be processed.
 * @return {Array | null}
 *      List of all keys of specified object/map/array/set or `null` if another value is passed.
 * @alias module:adam.getKeys
 * @see {@link module:adam.getFields getFields}
 */
function getKeys(value) {
    var sClass = getClass(value);
    if (sClass === "Map" || sClass === "Set") {
        return Array.from( value.keys() );
    }
    else if (sClass === "Array" || getType(value) === "object") {
        return Object.keys(value);
    }
    return null;
}

/**
 * Return list of all or filtered fields of specified object or map.
 * 
 * Fields are searched (checked) in the object itself and in its prototype chain.
 * 
 * @param {Object} obj
 *      Object or map to be processed.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter specifying fields that should be selected (see {@link module:adam.checkField checkField})
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 *   * `limit` - a maximum number of fields that should be included into result;
 *      after the specified number of fields is attained, the search will be stopped
 *   * `pairs`: `object | boolean | 'list' | 'obj'` - whether list of field-value pairs should be returned
 *      instead of list of fields; when `true` is set it is the same as `{form: 'obj'}`;
 *      when a string is set it is the same as `{form: <string>}`;
 *      an object value specifies a form of field-value pairs and can have
 *      the following fields:
 *      - `form`: `'list' | 'obj'` - whether an item of the result list should be `[field, value]` array
 *          or `{key: field, value: value}` object; default value is `'obj'`;
 *          any string that is different from `'list'` is treated as `'obj'`;
 *      - `type` - the same as `form`;
 *      - `key`: `string` - for object items of the result list specifies name of field
 *          that contains processed object's field; default value is `key`;
 *      - `value`: `string` - for object items of the result list specifies name of field
 *          that contains processed object's field value; default value is `value`;
 * @return {Array}
 *      List of all or filtered fields of specified object.
 * @alias module:adam.getFields
 * @see {@link module:adam.checkField checkField}
 * @see {@link module:adam.getKeys getKeys}
 */
function getFields(obj, settings) {
    /*jshint latedef:false, laxbreak:true*/
    
    function getValue(key) {
        return bObject
            ? obj[key]
            : obj.get(key);
    }
    
    function isLimitReached() {
        return nLimit > 0 && result.length >= nLimit;
    }
    
    function processKeyList(keyList) {
        var key, nI, nL, pair;
        for (nI = 0, nL = keyList.length; nI < nL; nI++) {
            key = keyList[nI];
            if ((bMap || ! (key in addedKeyMap)) && (bAll || checkField(obj, key, filter, options))) {
                if (sPairType === "obj") {
                    pair = {};
                    pair[sPairKey] = key;
                    pair[sPairValue] = getValue(key);
                }
                else if (sPairType === "list") {
                    pair = [key, getValue(key)];
                }
                else {
                    pair = key;
                }
                result.push(pair);
                if (isLimitReached()) {
                    break;
                }
                if (bObject) {
                    addedKeyMap[key] = null;
                }
            }
        }
    }
    
    var addedKeyMap = {},
        bAll = ! settings || ! ("filter" in settings),
        bMap = isMap(obj),
        bObject = ! bMap,
        options = settings || {},
        getOwnPropertySymbols = Object.getOwnPropertySymbols,
        bProcessSymbols = typeof getOwnPropertySymbols === "function",
        filter = bAll ? null : options.filter,
        bOwn = bAll ? false : filter === "own",
        bNotOwn = bAll ? false : filter === "!own",
        bUseFilter = bAll ? false : ! bOwn && ! bNotOwn,
        nLimit = options.limit || 0,
        pairs = options.pairs,
        result = [],
        target = obj,
        sPairKey, sPairType, sPairValue;
    if (pairs) {
        sPairType = typeof pairs;
        if (sPairType === "boolean") {
            pairs = {form: "obj"};
        }
        else if (sPairType === "string") {
            pairs = {form: pairs};
        }
        sPairType = pairs.form || pairs.type || "obj";
        if (sPairType.toLowerCase() !== "list") {
            sPairType = "obj";
        }
        sPairKey = pairs.key || "key";
        sPairValue = pairs.value || "value";
    }
    while (target) {
        if (bAll || bUseFilter || ((bOwn || bMap) && target === obj) || (bNotOwn && target !== obj)) {
            processKeyList(
                bObject
                    ? getOwnPropertyNames(target)
                    : getKeys(target)
            );
            if (bObject && bProcessSymbols && ! isLimitReached()) {
                processKeyList(getOwnPropertySymbols(target));
            }
            if (isLimitReached()) {
                break;
            }
        }
        target = bObject && (bAll || bNotOwn || bUseFilter)
                    ? getPrototypeOf(target)
                    : null;
    }
    return result;
}

/**
 * Return the name of field (or list of names) having the specified value in the given object or map.
 * 
 * @param {Object} obj
 *      Object or map to be checked.
 * @param {Any} value
 *      Value that should be searched for.
 * @param {Boolean} [bAll]
 *      Whether names of all found fields having the specified value should be returned.
 *      Default value is `false`.
 * @return {Array | String | null}
 *      Names of fields (when `bAll` is `true`) or a field name having the specified value,
 *      or `null` when the object do not contain the specified value.
 * @alias module:adam.getValueKey
 */
function getValueKey(obj, value, bAll) {
    /*jshint laxbreak:true*/
    var result = [],
        key, list, nI, nL;
    if (isMap(obj)) {
        list = Array.from(obj.entries());
        for (nI = 0, nL = list.length; nI < nL; nI++) {
            key = list[nI];
            if (key[1] === value) {
                if (bAll) {
                    result.push(key[0]);
                }
                else {
                    return key[0];
                }
            }
        }
    }
    else {
        for (key in obj) {
            if (obj[key] === value) {
                if (bAll) {
                    result.push(key);
                }
                else {
                    return key;
                }
            }
        }
    }
    return result.length
            ? result
            : null;
}

/**
 * Return list of all or filtered field values of specified object or map.
 * 
 * @param {Object} obj
 *      Object or map to be processed.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter specifying fields that should be selected (see {@link module:adam.checkField checkField})
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 * @return {Array}
 *      List of all or filtered field values of specified object.
 * @alias module:adam.getValues
 * @see {@link module:adam.checkField checkField}
 */
function getValues(obj, settings) {
    /*jshint laxbreak:true*/
    var result = [],
        bObject = ! isMap(obj),
        bAll = ! settings || ! ("filter" in settings),
        filter = bAll ? null : settings.filter,
        key, keyList, nI, nL;

    function processKey(keyValue) {
        if (bAll || checkField(obj, keyValue, filter, settings)) {
            result[result.length] = bObject
                ? obj[keyValue]
                : obj.get(keyValue);
        }
    }

    if (bObject) {
        for (key in obj) {
            processKey(key);
        }
    }
    else {
        keyList = getKeys(obj);
        for (nI = 0, nL = keyList.length; nI < nL; nI++) {
            processKey(keyList[nI]);
        }
    }
    
    return result;
}

/**
 * Return name of first free (absent) field/key of specified object/map, that conforms to the following pattern:
 * &lt;prefix&gt;&lt;number&gt;
 * 
 * @param {Object} obj
 *      Object or map in which a free field/key should be found.
 *      If `null` (or any false value) is set, the first value that conforms to the pattern will be returned.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported (setting's default value is specified in parentheses):
 *     
 *   * `checkPrefix`: `Boolean` (`false`) - specifies whether pattern consisting only from prefix (without number) should be checked
 *   * `prefix`: `String` (`f`) - prefix of sought field
 *   * `startNum`: `Integer` (`0`) - starting number which is used as part of pattern by search/check
 * @return {String}
 *      Name of field which is absent in the specified object and conforms to the pattern.
 * @alias module:adam.getFreeField
 */
function getFreeField(obj, settings) {
    var bObject = ! isMap(obj),
        bCheckPrefix, nStartNum, sField, sPrefix;
    if (! settings) {
        settings = {};
    }
    sPrefix = settings.prefix;
    nStartNum = settings.startNum;
    bCheckPrefix = settings.checkPrefix;
    if (typeof sPrefix !== "string") {
        sPrefix = "f";
    }
    if (! nStartNum) {
        nStartNum = 0;
    }
    if (bCheckPrefix) {
        sField = sPrefix;
        nStartNum--;
    }
    else {
        sField = sPrefix + nStartNum;
    }
    if (obj) {
        while (bObject ? (sField in obj) : obj.has(sField)) {
            sField = sPrefix + (++nStartNum);
        }
    }
    return sField;
}

/**
 * Create object (map) from list of objects/maps.
 * Fields of the created object are values of specified field/key of objects/maps,
 * values of the created object are corresponding items of the list.
 * 
 * @param {Array} list
 *      List of objects/values to be processed.
 * @param {Function | String} [keyField]
 *      Specifies names of fields of the created object. Can be name of field or method whose value is used
 *      as field name of the created object, or function that returns the field name.
 *      In the latter case the following parameters will be passed in the function:
 *      the source object (an item of the list), the created object, the index of the source object, operation settings.
 *      When the parameter is not set, items of the list are used as field names.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported (setting's default value is specified in parentheses):
 *     
 *   * `deleteKeyField`: `Boolean` (`false`) - specifies whether key field (whose value is field name of the created object)
 *     should be deleted from the source object (an item of the list)
 *   * `filter` - a filter specifying objects that should be included into result (see {@link module:adam.checkField checkField});
 *     an item of the list will be used as value for check
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 * @return {Object}
 *      Object created from the given list.
 * @alias module:adam.fromArray
 * @see {@link module:adam.checkField checkField}
 */
function fromArray(list, keyField, settings) {
    /*jshint laxbreak:true*/
    var nL = list.length,
        result = {},
        bAll, bMap, bDeleteKeyField, bFuncKey, filter, filterConnect, item, field, nI, sKeyName;
    if (nL) {
        if (! settings) {
            settings = {};
        }
        bAll = ! ("filter" in settings);
        filter = bAll ? null : settings.filter;
        filterConnect = settings.filterConnect;
        bFuncKey = typeof keyField === "function";
        bDeleteKeyField = Boolean(settings.deleteKeyField && keyField);
        if (! bFuncKey) {
            sKeyName = keyField;
        }
        for (nI = 0; nI < nL; nI++) {
            item = list[nI];
            bMap = isMap(item, true);
            if (bFuncKey) {
                field = sKeyName = keyField(item, result, nI, settings);
            }
            else {
                field = sKeyName
                    ? (bMap
                        ? item.get(sKeyName)
                        : item[sKeyName]
                    )
                    : item;
                if (typeof field === "function") {
                    field = field.call(item);
                }
            }
            if (bAll || checkField(result, field, filter, {value: item, filterConnect: filterConnect})) {
                if (bDeleteKeyField) {
                    if (bMap) {
                        item.delete(sKeyName);
                    }
                    else {
                        delete item[sKeyName];
                    }
                }
                result[field] = item;
            }
        }
    }
    return result;
}

/**
 * Return the value of the first element/field in the passed array/object/map/set that satisfies the specified filter(s).
 * 
 * If value passed for selection is not an array/set nor an object/map and `settings.defaultValue` is not set, the value will be returned as is.
 * If no element in the passed array/set satisfies the specified filter(s),
 * `settings.defaultValue` or the last element of the array/set (or `undefined` when the array is empty) will be returned.
 * If no field in the passed object/map satisfies the specified filter(s), `settings.defaultValue` or `undefined` will be returned.
 *
 * @param {Any} filter
 *      Filter that should be used to select a value (see {@link module:adam.checkField checkField} for details).
 * @param {Any} from
 *      An array/object/map/set from which a value should be selected.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported (setting's default value is specified in parentheses):
 *     
 *   * `defaultValue`: `Any` - default value that should be used when no element/field in the passed array/object/map/set
 *      satisfies the specified filter(s) or when value of `from` parameter is not an array/set nor an object/map
 *   * `filterConnect`: `String` (`and`) - a boolean connector that should be used when array of filters is specified
 *      in `filter` parameter (see {@link module:adam.checkField checkField} for details)
 * @return {Any}
 *      The value of first element/field in the passed array/object/map/set that satisfies the specified filter(s),
 *      or `settings.defaultValue`, or the value of `from` parameter or `undefined`.
 * @alias module:adam.select
 * @see {@link module:adam.checkField checkField}
 */
function select(filter, from, settings) {
    /*jshint latedef:false, laxbreak:true*/
    function getDefaultValue(value) {
        return "defaultValue" in options
            ? options.defaultValue
            : value;
    }

    var sClass = getClass(from),
        options = settings || {},
        result = getDefaultValue(from),
        key, keyList, nI, nL, nLast;
    if (sClass === "Array" || sClass === "Set") {
        if (sClass === "Set") {
            from = getKeys(from);
        }
        nL = from.length;
        if (nL) {
            nLast = nL - 1;
            for (nI = 0; nI < nL; nI++) {
                if (checkField(from, nI, filter, options)) {
                    result = from[nI];
                    break;
                }
                else if (nI === nLast) {
                    result = getDefaultValue(from[nI]);
                }
            }
        }
        else {
            result = getDefaultValue(key);
        }
    }
    else if (sClass === "Map") {
        result = getDefaultValue(key);
        keyList = getKeys(from);
        for (nI = 0, nL = keyList.length; nI < nL; nI++) {
            key = keyList[nI];
            if (checkField(from, key, filter, options)) {
                result = from.get(key);
                break;
            }
        }
    }
    else if (getType(from) === "object") {
        result = getDefaultValue(key);
        for (key in from) {
            if (checkField(from, key, filter, options)) {
                result = from[key];
                break;
            }
        }
    }
    return result;
}

/**
 * Divide given object or map into 2 parts: the first part includes specified fields, the second part includes all other fields.
 * 
 * @param {Object} obj
 *      Object or map to be divided.
 * @param {Array | Object | null} firstObjFields
 *      List of names of fields that should be included in the first part,
 *      or an object defining those fields.
 *      If value is not specified (`null` or `undefined`), `filter` setting should be used to divide fields into parts.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter that should be used to divide fields into parts (see {@link module:adam.checkField checkField});
 *      fields conforming to the filter will be included in the first part,
 *      fields that do not conform to the filter will be included in the second part
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 * @return {Array}
 *      Created parts: item with index 0 is the first part, item with index 1 is the second part.
 * @alias module:adam.split
 * @see {@link module:adam.checkField checkField}
 */
function split(obj, firstObjFields, settings) {
    /*jshint laxbreak:true*/
    var bObject = ! isMap(obj),
        first = bObject ? {} : new Map(),
        second = bObject ? {} : new Map(),
        result = [first, second],
        bByName, filter, key, keyList, nI, nL;

    function getTarget() {
        return (bByName ? key in firstObjFields : checkField(obj, key, filter, settings))
            ? first
            : second;
    }

    if (! settings) {
        settings = {};
    }
    if (firstObjFields) {
        bByName = true;
        if (typeof firstObjFields.length === "number") {
            firstObjFields = fromArray(firstObjFields);
        }
    }
    else {
        bByName = false;
        filter = settings.filter;
    }
    if (bObject) {
        for (key in obj) {
            getTarget()[key] = obj[key];
        }
    }
    else {
        keyList = getKeys(obj);
        for (nI = 0, nL = keyList.length; nI < nL; nI++) {
            key = keyList[nI];
            getTarget().set(key, obj.get(key));
        }
    }
    return result;
}

/**
 * Remove filtered fields/elements from specified object/map/array/set.
 * 
 * @param {Array | Object} obj
 *      Array, set, object or map to be processed.
 * @param {Any} filter
 *      A filter or array of filters specifying fields or elements that should be removed
 *      (see {@link module:adam.checkField checkField}).
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` parameter (see {@link module:adam.checkField checkField})
 * @return {Array | Object}
 *      Processed array, set, object or map.
 * @alias module:adam.remove
 * @see {@link module:adam.checkField checkField}
 */
function remove(obj, filter, settings) {
    var key, keyList, nI, nL;
    if (obj && typeof obj === "object") {
        switch (getClass(obj)) {
            case "Array":
                key = obj.length;
                while(key--) {
                    if (checkField(obj, key, filter, settings)) {
                        obj.splice(key, 1);
                    }
                }
                break;
            case "Map":
            case "Set":
                keyList = getKeys(obj);
                for (nI = 0, nL = keyList.length; nI < nL; nI++) {
                    key = keyList[nI];
                    if (checkField(obj, key, filter, settings)) {
                        obj.delete(key);
                    }
                }
                break;
            default:
                for (key in obj) {
                    if (checkField(obj, key, filter, settings)) {
                        delete obj[key];
                    }
                }
        }
    }
    return obj;
}

/**
 * Empty the given value according to the following rules:
 * 
 *   * for array, `Map` or `Set`: removes all elements from the value
 *   * for object: removes all own fields from the value
 *   * for string: returns empty string
 *   * for number: returns `0`
 *   * otherwise: returns `undefined`
 * 
 * @param {Any} value
 *      Value to be processed.
 * @return {Any}
 *      Processed value (for array, object, `Map` or `Set`) or empty value corresponding to the given value.
 * @alias module:adam.empty
 */
function empty(value) {
    var sField, sType;
    switch (getType(value)) {
        case "object":
            sType = getClass(value);
            if (sType === "Array") {
                value.length = 0;
            }
            else if (sType === "Map" || sType === "Set") {
                value.clear();
            }
            else {
                for (sField in value) {
                    delete value[sField];
                }
            }
            break;
        case "string":
            value = "";
            break;
        case "number":
            value = 0;
            break;
        default:
            value = sField;
    }
    return value;
}

/**
 * Reverse or negate the given value according to the following rules:
 * 
 *   * for array or set: creates a copy and reverses order of elements in it
 *   * for object or map: creates a copy where fields are  swapped with their values (i.e. for `{a: "b", c: "d"}` returns `{b: "a", d: "c"}`)
 *   * for string: reverses order of its characters
 *   * for number: returns negated value (i.e. `- value`)
 *   * for boolean: returns negated value (i.e. `! value`)
 *   * otherwise: returns source value without modification
 * 
 * @param {Any} value
 *      Value to be processed.
 * @return {Any}
 *      Processed value.
 * @alias module:adam.reverse
 */
function reverse(value) {
    var cache, item, nI, nL, sField;
    switch (getType(value)) {
        case "object":
            switch (getClass(value)) {
                case "Array":
                    value = value.slice(0).reverse();
                    break;
                case "Map":
                    cache = Array.from(value.entries());
                    value = new Map();
                    for (nI = 0, nL = cache.length; nI < nL; nI++) {
                        item = cache[nI];
                        value.set(item[1], item[0]);
                    }
                    break;
                case "Set":
                    value = new Set( getKeys(value) );
                    break;
                default:
                    cache = {};
                    for (sField in value) {
                        cache[ value[sField] ] = sField;
                    }
                    value = cache;
            }
            break;
        case "string":
            value = value.split("").reverse().join("");
            break;
        case "number":
            value = - value;
            break;
        case "boolean":
            value = ! value;
            break;
    }
    return value;
}

/**
 * Transform the given value applying the specified operation.
 * 
 * @param {Any} value
 *      Value to be transformed.
 * @param {String} sAction
 *      Operation that should be applied to transform the value. Can be one of the following:
 *      
 *    * `array` - convert the value to array (using `Array(value)`)
 *    * `boolean` - convert the value to boolean value (using `Boolean(value)`)
 *    * `empty` - empty the value (see {@link module:adam.empty empty})
 *    * `function` - convert the value to function (using `Function(value)`)
 *    * `integer` - try to convert the value to an integer number (using `Math.round(Number(value)`)
 *    * `map` - try to convert the value to Map (using `new Map(value)` or `new Map([[value, value]])`)
 *    * `number` - try to convert the value to number (using `Number(value)`)
 *    * `object` - convert the value to object (using `Object(value)`)
 *    * `promise` or `resolve` - convert the value to resolved promise (using `Promise.resolve(value)`)
 *    * `reject` - convert the value to rejected promise (using `Promise.reject(value)`)
 *    * `reverse` - reverse the value (see {@link module:adam.reverse reverse})
 *    * `set` - try to convert the value to Set (using `new Set(value)` or `new Set([value])`)
 *    * `string` - convert the value to string (using `String(value)`)
 *    * otherwise - source value
 * @return {Any}
 *      Transformed value.
 * @alias module:adam.transform
 * @see {@link module:adam.empty empty}
 * @see {@link module:adam.reverse reverse}
 */
function transform(value, sAction) {
    /*jshint evil:true, -W064*/
    var result;
    switch (sAction) {
        case "array":
            result = Array(value);
            break;
        case "boolean":
            result = Boolean(value);
            break;
        case "empty":
            result = empty(value);
            break;
        case "function":
            result = Function(value);
            break;
        case "integer":
            result = Math.round(Number(value));
            break;
        case "map":
            try {
                result = new Map(value);
            }
            catch (e) {
                result = new Map([[value, value]]);
            }
            break;
        case "number":
            result = Number(value);
            break;
        case "object":
            result = Object(value);
            break;
        case "promise":
        case "resolve":
            result = Promise.resolve(value);
            break;
        case "reject":
            result = Promise.reject(value);
            break;
        case "reverse":
            result = reverse(value);
            break;
        case "set":
            try {
                result = new Set(value);
            }
            catch (e) {
                result = new Set([value]);
            }
            break;
        case "string":
            result = String(value);
            break;
        default:
            result = value;
    }
    return result;
}

/**
 * Copy all or filtered fields from source object/map/array/set into target object/map/array/set, applying specified transformation if necessary.
 * 
 * @param {Object} source
 *      Object/map/array/set whose fields will be copied.
 * @param {Object} target
 *      Object/map/array/set into which fields should be copied.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter that should be used to select fields for copying (see {@link module:adam.checkField checkField});
 *      fields conforming to the filter will be copied
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 *   * `transform` - an action/operation that should be applied to get field's value that will be copied
 *      instead of value from source object; can be a string specifying transformation (see {@link module:adam.transform transform})
 *      or a function whose result will be used as field's value; object with the following fields will be passed into the function:
 *      - `field` - field name
 *      - `value` field value from source object
 *      - `source` - reference to the source object
 *      - `target` - reference to the target object
 *      - `settings` - operation settings
 * @return {Object}
 *      Reference to the target object (value of `target` parameter).
 * @alias module:adam.copy
 * @see {@link module:adam.checkField checkField}
 * @see {@link module:adam.transform transform}
 */
function copy(source, target, settings) {
    /*jshint laxbreak:true*/
    var bAll = true,
        sClass = getClass(source),
        bMap = sClass === "Map",
        bSet = sClass === "Set",
        bFuncAction, action, filter, key, keyList, nI, nL;

    function copyValue() {
        var value;
        if (bAll || checkField(source, key, filter, settings)) {
            if (bMap) {
                value = source.get(key);
            }
            else if (bSet) {
                value = key;
            }
            else {
                value = source[key];
            }
            if (action) {
                value = bFuncAction
                    ? action({source: source, target: target, field: key, value: value, settings: settings})
                    : transform(value, action);
            }
            if (bMap) {
                target.set(key, value);
            }
            else if (bSet) {
                target.add(value);
            }
            else {
                target[key] = value;
            }
        }
    }

    if (! settings) {
        settings = {};
    }
    if ("filter" in settings) {
        filter = settings.filter;
        bAll = false;
    }
    action = settings.transform;
    if (typeof action === "function") {
        bFuncAction = true;
    }
    if (bMap || bSet) {
        keyList = getKeys(source);
        for (nI = 0, nL = keyList.length; nI < nL; nI++) {
            key = keyList[nI];
            copyValue();
        }
    }
    else {
        for (key in source) {
            copyValue();
        }
    }
    return target;
}

/**
 * Change all or filtered fields of object/map/array, applying specified action/transformation.
 * 
 * @param {Object} obj
 *      Object/map/array whose fields should be changed.
 * @param {Function | String} action
 *      An action/operation that should be applied to get new field value.
 *      See description of `transform` setting of {@link module:adam.copy copy}.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter that should be used to select fields for modification (see {@link module:adam.checkField checkField});
 *      fields conforming to the filter will be changed
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 * @return {Object}
 *      Modified object (value of `obj` parameter).
 * @alias module:adam.change
 * @see {@link module:adam.checkField checkField}
 * @see {@link module:adam.copy copy}
 */
function change(obj, action, settings) {
    /*jshint laxbreak:true*/
    settings = settings
                ? copy(settings, {})
                : {};
    settings.transform = action;
    return copy(obj, obj, settings);
}

/**
 * Create new object containing all or filtered fields of the source object/map/array/set,
 * applying specified action/transformation for field values.
 * 
 * @param {Array | Object} obj
 *      Object/map/array/set whose fields should be copied.
 * @param {Function | String} action
 *      An action/operation that should be applied to get field value that will be saved in created object.
 *      See description of `transform` setting of {@link module:adam.copy copy}.
 * @param {Object} [settings]
 *     Operation settings. Keys are settings names, values are corresponding settings values.
 *     The following settings are supported:
 *     
 *   * `filter` - a filter that should be used to select fields for copying (see {@link module:adam.checkField checkField});
 *      fields conforming to the filter will be copied in created object
 *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
 *      in `filter` setting (see {@link module:adam.checkField checkField})
 * @return {Object}
 *      Created object containing processed fields.
 * @alias module:adam.map
 * @see {@link module:adam.checkField checkField}
 * @see {@link module:adam.copy copy}
 */
function map(obj, action, settings) {
    /*jshint laxbreak:true*/
    var target;
    switch (getClass(obj)) {
        case "Array":
            target = [];
            break;
        case "Map":
            target = new Map();
            break;
        case "Set":
            target = new Set();
            break;
        default:
            target = {};
    }
    settings = settings
                ? copy(settings, {})
                : {};
    settings.transform = action;
    return copy(obj, target, settings);
}

// Exports

module.exports = {
    change: change,
    checkField: checkField,
    checkValue: checkValue,
    copy: copy,
    empty: empty,
    fromArray: fromArray,
    getClass: getClass,
    getFields: getFields,
    getFreeField: getFreeField,
    getKeys: getKeys,
    getPropertySymbols: getPropertySymbols,
    getSize: getSize,
    getType: getType,
    getValueKey: getValueKey,
    getValues: getValues,
    isEmpty: isEmpty,
    isKindOf: isKindOf,
    isMap: isMap,
    isSizeMore: isSizeMore,
    map: map,
    remove: remove,
    reverse: reverse,
    select: select,
    split: split,
    transform: transform
};