Source: index.js

/** ****************************************************************************************************
 * File: index.js
 * Project: lightmap
 * @author Nick Soggin <iSkore@users.noreply.github.com> on 11-Jun-2018
 *******************************************************************************************************/
'use strict';

const { version } = require( './package.json' );

/**
 * LightMap
 * @augments Map
 */
class LightMap extends Map
{
    /**
     * constructor
     * @param {array[]} [n] - constructed map
     * @param {object} [opts] - construction options
     * @param {boolean} [opts.deepTransformToMap=true] - deep transform map-looking arrays to a LightMap
     */
    constructor( n = [], opts = { deepTransformToMap: true } )
    {
        super( n );

        if ( opts.deepTransformToMap ) {
            this.deepTransformToMap();
        }
    }

    /**
     * @typedef {function(value, key, ref)} FilterCallback
     * @callback FilterCallback
     * @param {*} value - map value
     * @param {string | *} key - map key
     * @param {LightMap} refMap - original LightMap
     * @returns {boolean} - a comparison or boolean value to
     */

    /**
     * filter
     * @description
     * Filter LightMap based on keys and values based in.
     * Comparisons can be made on the key and/or value.
     * @param {FilterCallback} fn - comparison method
     * @return {LightMap} - returns new LightMap with filtered results
     * @example
     * const _ = new LightMap();
     * _.set( 'key', 'value' );
     * _.set( 'key1', 'value1' );
     *
     * const result = _.filter(
     *     ( v, k ) => k === 'key'
     * );
     *
     * // -> LightMap { 'key' => 'value' }
     */
    filter( fn )
    {
        const arr = new LightMap();

        for ( const [ key, value ] of this ) {
            if ( fn( value, key, this ) ) {
                arr.set( key, value );
            }
        }

        return arr;
    }

    /**
     * @typedef {function(value, key, ref)} MapCallback
     * @callback MapCallback
     * @param {*} value - map value
     * @param {string | *} key - map key
     * @param {LightMap} refMap - original LightMap
     * @returns {Array<string|*,*>} - the new key-value pair to be remapped
     */

    /**
     * map
     * @description
     * Map LightMap with new key and/or value.
     * Return the new item in a "tuple" form matching the Map paradigm (`[ x, y ]`).
     * @param {MapCallback} fn - map method
     * @return {LightMap} - returns new LightMap with mapped results
     * @example
     * const _ = new LightMap();
     * _.set( 'key', 'value' );
     *
     * const result = _.map(
     *     ( v, k ) => [ k + 1, v + 1 ]
     * );
     *
     * // -> LightMap { 'key1' => 'value1' }
     */
    map( fn )
    {
        const arr = new LightMap();

        for ( const [ key, value ] of this ) {
            const entry = fn( value, key, this ) || [ undefined, undefined ];
            arr.set( entry[ 0 ] || key, entry[ 1 ] );
        }

        return arr;
    }

    /**
     * @typedef {function(r, value, key, ref)} ReduceCallback
     * @callback ReduceCallback
     * @param {*} value - carriage value
     * @param {Array<string|*,*>} mapKeyPair - map key-value pair
     * @param {string | *} key - map key
     * @param {LightMap} refMap - original LightMap
     * @returns {*} - the carriage value
     */

    /**
     * reduce
     * @description
     * Reduce LightMap with new value.
     * Must return the carriage value just like Array.reduce.
     * @param {ReduceCallback} fn - reducing method
     * @param {*} r - carriage value
     * @return {*} - returns reduced result
     * @example
     * const _ = new LightMap();
     * _.set( 'key', 'value' );
     *
     * const result = _.reduce(
     *     ( r, [ k, v ] ) => {
     *         r += `Key: ${ k }\n`;
     *         r += `Value: ${ v }\n`;
     *         return r;
     *     }, ''
     * );
     *
     * // -> 'Key: key\nValue: value\n'
     */
    reduce( fn, r )
    {
        for ( const [ key, value ] of this ) {
            r = fn( r, [ key, value ], key, this );
        }

        return r;
    }

    /**
     * @typedef {function(value, key, ref)} FindCallback
     * @callback FindCallback
     * @param {*} value - map value
     * @param {string | *} key - map key
     * @param {LightMap} refMap - original LightMap
     * @returns {Array<string|*,*>} - the found value
     */

    /**
     * find
     * @description
     * The `find()` method returns the tuple pair of the first element in the LightMap that satisfies the provided
     * testing function.
     * @param {FindCallback} fn - find method
     * @return {Array<*|*>|undefined} - returns tuple result or undefined if no matches are found
     * @example
     * const _ = new LightMap();
     * _.set( 'key', 'value' );
     * _.set( 'key1', 'value1' );
     *
     * const result = _.find(
     *     ( v, k, arr ) => {
     *         return v === 'value';
     *     }
     * );
     *
     * // -> [ 'key', 'value' ]
     */
    find( fn )
    {
        for ( const [ key, value ] of this ) {
            if ( fn( value, key, this ) ) {
                return [ key, value ];
            }
        }

        return undefined;
    }

    /**
     * findAll
     * @description
     * Alias for `filter`
     * @param {FilterCallback} fn - same as filter method
     * @return {LightMap} - returns new LightMap with matching results
     * @example
     * const _ = new LightMap();
     * _.set( 'key', 'value' );
     * _.set( 'key1', 'value' );
     * _.set( 'key2', 'value2' );
     *
     * const result = _.findAll(
     *      ( v, k, arr ) => {
     * 			return v === 'value';
     * 		}
     * );
     *
     * // -> LightMap { 'key' => 'value', 'key1' => 'value' }
     */
    findAll( fn )
    {
        return this.filter( fn );
    }

    /**
     * sortKeys
     * @description
     * Map LightMap with sorted key-value pairs.
     * @param {Function} [fn] - sorting method
     * @return {LightMap} - returns new LightMap with sorted results
     * @example
     * const _ = new LightMap();
     * _.set( 'key2', 'value2' );
     * _.set( 'key1', 'value1' );
     * _.set( 'key', 'value' );
     *
     * const result = _.sortKeys();
     *
     * // -> LightMap { 'key' => 'value', 'key1' => 'value1', 'key2' => 'value2' }
     */
    sortKeys( fn = ( a, b ) => String( a ).localeCompare( String( b ) ) )
    {
        const keys = [ ...this.keys() ].sort( fn );
        let i      = 0;

        return this.map(
            ( v, k, iterator ) => {
                const key = keys[ i++ ];
                return [ key, iterator.get( key ) ];
            }
        );
    }

    /**
     * sortValues
     * @description
     * Map LightMap with sorted key-value pairs sorted by value.
     * @param {Function?} fn - sorting method
     * @return {LightMap} - returns new LightMap with sorted results
     * @example
     * const _ = new LightMap();
     * _.set( 'key', 'value2' );
     * _.set( 'key1', 'value1' );
     * _.set( 'key2', 'value' );
     *
     * const result = _.sortValues();
     *
     * // -> LightMap { 'key2' => 'value', 'key1' => 'value1', 'key' => 'value2' }
     */
    sortValues( fn = ( a, b ) => String( a ).localeCompare( String( b ) ) )
    {
        const entries = [ ...this.entries() ].sort( ( a, b ) => fn( a[ 1 ], b[ 1 ] ) );
        let i         = 0;

        return this.map(
            () => {
                const [ key, value ] = entries[ i++ ];
                return [ key, value ];
            }
        );
    }

    // TODO: improve this later, not sufficient yet
    equals( n )
    {
        if ( n instanceof LightMap ) {
            return this.size === n.size;
        }

        return false;
    }

    /**
     * version
     * @description
     * return LightMap module version
     * @return {string} - returns LightMap module version
     * @example
     * this.version();
     *
     * // -> v0.0.0
     */
    version()
    {
        return LightMap.version();
    }

    /**
     * version
     * @description
     * return LightMap module version
     * @return {string} - returns LightMap module version
     * @example
     * LightMap.version();
     *
     * // -> v0.0.0
     */
    static version()
    {
        return `v${ version }`;
    }

    /**
     * mapToArray
     * @description
     * maps a LightMap object to an array of arrays in the Map Pattern (re-constructable pattern)
     * @return {Array} - returns array of tuple key-value pairs
     * @example
     *
     * const _ = new LightMap();
     * _.set( 'key', new LightMap( [ [ 'key1', 'value1' ] ] ) );
     *
     * const result = _.mapToArray();
     *
     * // -> [ [ 'key', [ [ 'key1', 'value1' ] ] ] ]
     */
    mapToArray()
    {
        return this.reduce(
            ( r, [ k, v ] ) => {
                if ( v instanceof LightMap ) {
                    v = v.toJSON();
                }

                r.push( [ k, v ] );
                return r;
            }, []
        );
    }

    /**
     * toObject
     * @description
     * maps a LightMap object's keys and values to an object
     * @return {Object} - returns Object of key-value pairs
     * @example
     *
     * const _ = new LightMap();
     * _.set( 'key', new LightMap( [ [ 'key1', 'value1' ] ] ) );
     *
     * const result = _.toObject();
     *
     * // -> { key: { key1: 'value1' } }
     */
    // TODO: check for circular references
    toObject()
    {
        const
            obj = Object.fromEntries( this ),
            keys = Object.keys( obj );
        
        for ( let i = 0; i < keys.length; i++ ) {
            const key = keys[ i ];
            if ( obj[ key ] instanceof Map ) {
                obj[ key ] = obj[ key ].toObject();
            }
        }

        return obj;
    }

    toJSON()
    {
        return this.mapToArray();
    }

    toString()
    {
        return JSON.stringify( this.mapToArray() );
    }

    /**
     * indexOf
     * @description
     * returns the first index at which a given element can be found in the array, or -1 if it is not present
     * @param {string} n - key name to search for
     * @return {number} - index of the element
     */
    indexOf( n )
    {
        let i = -1;

        return this.reduce(
            ( r, [ k ] ) => {
                if ( r === -1 ) {
                    i++;

                    if ( k === n ) {
                        r = i;
                    }
                }

                return r;
            }, -1
        );
    }

    deepTransformToMap()
    {
        this.forEach(
            ( v, k ) => this.set( k, this[ Symbol.constructMapByPattern ]( v ) )
        );
    }

    [ Symbol.constructMapByPattern ]( n )
    {
        return Array.isArray( n ) && n.every( v => Array.isArray( v ) && v.length === 2 ) ? new LightMap( n ) : n;
    }

    [ Symbol.search ]( n )
    {
        return this.indexOf( n );
    }

    /**
     * [ Symbol.replace ]
     * @description
     * symbol specifies the method that replaces matched substrings of a string
     * @param {LightMap} n - replacement mapping for string
     * @return {string} - string replaced with mapped values
     */
    [ Symbol.replace ]( n )
    {
        this.forEach(
            ( v, k ) => n = n.replace( k, v )
        );

        return n;
    }

    /**
     * [ Symbol.toPrimitive ]
     * @description
     * symbol that specifies a function valued property that is called to convert an object to a primitive value
     * @param {*} n - hint
     * @return {*} - hint handler
     */
    [ Symbol.toPrimitive ]( n )
    {
        if ( n === 'number' ) {
            return +this.size;
        }
        else {
            return this.toString();
        }
    }

    get [ Symbol.toStringTag ]()
    {
        return this.constructor.name;
    }

    static get [ Symbol.species ]()
    {
        return Map;
    }

    static [ Symbol.hasInstance ]( instance )
    {
        return !!instance &&
            instance.constructor &&
            instance.constructor.name === 'LightMap';
    }
}

module.exports = LightMap;