简体   繁体   中英

JS/lodash - converting an array of arrays to object

I'm working on Node/JS with lodash, and am trying to convert an array of arrays to a hash object such that:

[ [ 'uk', 'london', 'british museum' ],
[ 'uk', 'london', 'tate modern' ],
[ 'uk', 'cambridge', 'fitzwilliam museum' ],
[ 'russia', 'moscow', 'tretyakovskaya gallery' ],
[ 'russia', 'st. petersburg', 'hermitage' ],
[ 'russia', 'st. petersburg', 'winter palace' ],
[ 'russia', 'st. petersburg', 'russian museum' ] ]

becomes this kind of hash/tree structure:

{ uk: { 
    london: [ 'british museum', 'tate modern' ],
    cambridge: [ 'fitzwilliam museum' ]
    },
russia: {
    moscow: [ 'tretyakovskaya gallery' ],
    'st petersburg': ['hermitage', 'winter palace', 'russian museum']
    }
}

So far I've used this kind of code:

function recTree(arr) {
    // We only take in arrays of arrays
    if (arr.constructor === Array && arr[0].constructor === Array) {
        // If array has a single element, return it
        if (arr.length === 1) {
            return arr[0];
        }
        // Group array by first element
        let grouped = _.groupBy(arr, function(o) {
            return o[0]
        });
        let clean = _.mapValues(grouped, function(o) {
            return _.map(o, function(n) {
                // Cut off first element
                let tail = n.slice(1);

                if (tail.constructor === Array && tail.length == 1) {
                    // If there is a single element, return it
                    return tail[0];
                } else {
                    return tail;
                }
            });
        });
        return _.mapValues(clean, recTree)
    } else {
        // If it's not an array of arrays, return it
        return arr;
    }
}

I'm wondering if there is a cleaner, more functional way of doing this than what I've programmed so far. Ideally, I'd like to function to be able to accept arrays of arrays of arbitrary (but constant, such that all inner arrays are the same) length (not just 3)

Here's a lodash solution that works for any arrays with a variable length.

  1. Use lodash#reduce to reduce the array into its object form.

  2. In each reduce iteration:

    2.1. We get the path of the value that we want to set, eg uk.london , using lodash#initial .

    2.2. Use lodash#last , to get the value from inner array that we want to concatenate.

    2.3 Use lodash#get to get any existing array from the object using the path , if it doesn't get any value then it defaults to an empty array. After getting the value, we concatenate the last item of the inner array towards the obtained value.

    2.4 Use lodash#set to set the resulting object from the value taken from 2.3 using the path taken from 2.1.


var result = _.reduce(array, function(object, item) {
  var path = _.initial(item);
  var value = _.get(object, path, []).concat(_.last(item));
  return _.set(object, path, value);
}, {});

 var array = [ ['uk', 'london', 'british museum'], ['uk', 'london', 'tate modern'], ['uk', 'cambridge', 'fitzwilliam museum'], ['russia', 'moscow', 'tretyakovskaya gallery'], ['russia', 'st. petersburg', 'hermitage'], ['russia', 'st. petersburg', 'winter palace'], ['russia', 'st. petersburg', 'russian museum'] ]; var result = _.reduce(array, function(object, item) { var path = _.initial(item); var value = _.get(object, path, []).concat(_.last(item)); return _.set(object, path, value); }, {}); console.log(result); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script> 

You could use Array#reduce for creating/accessing the nested data structure and push the last element of the array.

EDIT: This solution works for an arbitrary length of the inner arrays.

 var array = [['foo', 'bar', 'baz', 42], ['uk', 'london', 'british museum'], ['uk', 'london', 'tate modern'], ['uk', 'cambridge', 'fitzwilliam museum'], ['russia', 'moscow', 'tretyakovskaya gallery'], ['russia', 'st. petersburg', 'hermitage'], ['russia', 'st. petersburg', 'winter palace'], ['russia', 'st. petersburg', 'russian museum']], object = {}; array.forEach(function (a) { a.slice(0, -1).reduce(function (o, k, i, kk) { return o[k] = o[k] || kk.length - 1 - i && {} || []; }, object).push(a[a.length - 1]); }); console.log(object); 
 .as-console-wrapper { max-height: 100% !important; top: 0; } 

ES6

 var array = [['foo', 'bar', 'baz', 42], ['uk', 'london', 'british museum'], ['uk', 'london', 'tate modern'], ['uk', 'cambridge', 'fitzwilliam museum'], ['russia', 'moscow', 'tretyakovskaya gallery'], ['russia', 'st. petersburg', 'hermitage'], ['russia', 'st. petersburg', 'winter palace'], ['russia', 'st. petersburg', 'russian museum']], object = {}; array.forEach( a => a .slice(0, -1) .reduce((o, k, i, kk) => o[k] = o[k] || kk.length - 1 - i && {} || [], object) .push(a[a.length - 1]) ); console.log(object); 
 .as-console-wrapper { max-height: 100% !important; top: 0; } 

As functional as it gets and without using lodash, in this solution the inner arrays can be of variable length:

 var arrayOfArrays = [ [ 'uk', 'london', 'british museum' ], [ 'uk', 'london', 'tate modern' ], [ 'uk', 'cambridge', 'fitzwilliam museum' ], [ 'russia', 'moscow', 'tretyakovskaya gallery' ], [ 'russia', 'st. petersburg', 'hermitage' ], [ 'russia', 'st. petersburg', 'winter palace' ], [ 'russia', 'st. petersburg', 'russian museum' ] ] var resultingObject = arrayOfArrays.reduce(function(obj, arr) { arr.reduce(function(parent, value, i) { if (i < arr.length - 1) { parent[value] = parent[value] || (i < arr.length - 2 ? {} : []); return parent[value]; } parent.push(value); }, obj); return obj; }, {}); console.log(resultingObject); 

Maybe grouping and mapping is not the more efficent way to solve your problem.

A better approach, as you elaborate your data just once, should be to create your map while you read your list.

And I would bail out of the function as soon as possible if prerequisite are not met, the code is easier to understand, to me.

The other issue I would not get in account is returning the first element of the input array in case the list has length just one: given a correct input format, any cardinality, you should return the same output type.

Your function return three different ones:

  • anything (in case the list is not recognized)
  • an array (in case the input list is ok but contains just one element)
  • an object with the mappings you need in other cases

I would throw an exception in case the list is not recognized (you already have the input) or return an object with the mappings in all other cases

var x = [ 
  [ 'uk', 'london', 'british museum' ],
  [ 'uk', 'london', 'tate modern' ],
  [ 'uk', 'cambridge', 'fitzwilliam museum' ],
  [ 'russia', 'moscow', 'tretyakovskaya gallery' ],
  [ 'russia', 'st. petersburg', 'hermitage' ],
  [ 'russia', 'st. petersburg', 'winter palace' ],
  [ 'russia', 'st. petersburg', 'russian museum' ] 
];

/*
 * function to add a single point of interest at the 
 * right place in the map
 *
 */
function remap(map, location) {
    var state, city, poi;
    [state, city] = location;
    poi = location.slice(2);
    if (!map[state]) {
        // create a new state property
        map[state]={};
    }
    if (!map[state][city]) {
        // first time we encounter a city: create its slot
        map[state][city]=[];
    }
    // Add the points of interest not in the list
    map[state][city].concat(
        poi.filter(function(p) { 
            return !map[state][city].includes(p);
        })
    );

    return map;
}

function recTree(arr) {
    // We only take in arrays of arrays 
    // if not, better to throw an exception!
    if (arr.constructor !== Array || arr[0].constructor !== Array) {
        throw "recTree: invalid parameter, we need an array of array";
    }
    return x.reduce(remap, {});
}

if you really need a _lodash version you can start from this

function recTree(arr) {
    // We only take in arrays of arrays 
    // if not, better to throw an exception!
    if (arr.constructor !== Array || arr[0].constructor !== Array) {
        throw "recTree: invalid parameter, we need an array of array";
    }
    return _.reduce(x, remap, {});
}

in both cases you can obtain your data (the try catch enclosure is optional) with:

try {
  // if you really trust your code use just the following line
  var mymap = recTree(arr);
} catch(e) {
  // do anything you can from recover for the error or rethrow to alert
  // developers that something was wrong with your input
}

References on MDN

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM