简体   繁体   中英

Convert object with arrays to array of objects using Ramda or Lodash

Example input:

const obj = {
  paths: ['path1', 'path2'],
  thumbnails: ['thumb1', 'thumb2'],
  sizes: [ // may not always be presented
    [100, 200],
    [120, 220],
  ],
};

Expected output:

const result = [
  {
    path: 'path1',
    thumbnail: 'thumb1',
    size: [100, 200],
  },
  {
    path: 'path2',
    thumbnail: 'thumb2',
    size: [120, 220],
  },
];

Bonus points for:

const result1 = [
  {
    path: 'path1',
    thumbnail: 'thumb1',
    width: 100,
    height: 200,
  },
  {
    path: 'path2',
    thumbnail: 'thumb2',
    width: 120,
    height: 220,
  },
];

// without sizes

const result2 = [
  {
    path: 'path1',
    thumbnail: 'thumb1',
  },
  {
    path: 'path2',
    thumbnail: 'thumb2',
  },
];

How would I achieve this with Ramda or Lodash?

Updated answer

Here we extract the fields we care about into arrays, turn that into a multidimensional array (number of records by number of elements in an array), transpose that array, and then zip it back together as an object.

We use the same list of field names for the extraction and for reconstituting an object. It looks like this:

 const inflate = (names) => (obj) => map (zipObj (names)) (transpose (props (names) (obj))) const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]} console .log (inflate (['paths', 'thumbnails', 'sizes']) (obj))
 .as-console-wrapper {max-height: 100% !important; top: 0}
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script> <script> const {map, zipObj, transpose, props, paths} = R </script>

For your extended version, just run a subsequent map over the results:

const inflate = (names) => (obj) =>  
  map (zipObj (names)) (transpose (props (names) (obj)))
    .map (({sizes: [width, height], ...rest}) => ({...rest, width, height}))

A specification-based approach

Ori Drori's answer challenged me to find a specification-based version that lets us declare that paths becomes path and the first element of sizes becomes width . Here is one rather nasty version of that idea. I'm out of time to clean it up, but you can see at least the skeleton of an interesting idea in this:

 const regroup = (spec, names = uniq (values (spec) .map (x => x.split ('.') [0]))) => (obj) => map (applySpec (map (pipe (split ('.'), path)) (spec))) (map (zipObj (names)) (transpose (props (names) (obj)))) const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]} const spec = {path: 'paths', thumbnail: 'thumbnails', width: 'sizes.0', height: 'sizes.1'} console .log (regroup (spec) (obj))
 .as-console-wrapper {max-height: 100% !important; top: 0}
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script> <script> const {uniq, values, map, applySpec, pipe, split, path, zipObj, transpose, props} = R </script>

Original answer

(I originally misread and thought you wanted something like the Cartesian product of the elements, not the matched indices one. I'm leaving it here as it does answer what to my mind is a more interesting question.)

TLDR

Below we discuss several implementations. A configurable one written in Ramda looks like this:

const inflate = pipe (map (unwind), pipeWith (chain))
// ...
inflate (['paths', 'thumbnails', 'sizes', 'sizes']) (obj)

A powerful, but less flexible vanilla one looks like this:

const inflate = (obj, _, __, field = Object .entries (obj) .find (([k, v]) => Array .isArray (v))) => 
  field ? field [1] .flatMap ((v) => inflate ({...obj, [field [0]]: v})) : obj
// ...
inflate (obj)

Ramda implementation

Ramda has a function dedicated to this: unwind . It takes a property name and returns an array of values, one for each element of the array at that name. We can then simply pipe these together with chain , like this:

 const inflate = pipeWith (chain) ([ unwind ('paths'), unwind ('thumbnails'), unwind ('sizes'), unwind ('sizes') ]) const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]} console .log (inflate (obj))
 .as-console-wrapper {max-height: 100% !important; top: 0}
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script> <script> const {pipeWith, chain, unwind} = R </script>

If pipeWith is unfamiliar, this is equivalent to

const inflate = pipe (
  unwind ('paths'),
  chain (unwind ('thumbnails')),
  chain (unwind ('sizes')),
  chain (unwind ('sizes'))
)

where the chain calls are acting here similar to Array.prototype.flatMap .

More abstract Ramda implementation

But this calls out for a further abstraction. We can make that more declarative by extracting the list of paths we want to expand, so that we can simply call inflate (['paths', 'thumbnails', 'sizes', 'sizes']) (obj) . This is an implementation:

 const inflate = pipe (map (unwind), pipeWith (chain)) const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]} console .log (inflate (['paths', 'thumbnails', 'sizes', 'sizes']) (obj))
 .as-console-wrapper {max-height: 100% !important; top: 0}
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script> <script> const {pipe, map, pipeWith, chain, unwind} = R </script>

Powerful, but less flexible vanilla implementation

But perhaps we don't want to specify the fields. If we just want to supply the object and expand all arrays, including nested ones, then we probably want to write a recursive version. At this point, Ramda would probably be a distraction. We can write a simple vanilla JS implementation like this:

 const inflate = (obj, _, __, field = Object .entries (obj) .find (([k, v]) => Array .isArray (v))) => field ? field [1] .flatMap ((v) => inflate ({...obj, [field [0]]: v})) : obj const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]} console .log (inflate (obj))
 .as-console-wrapper {max-height: 100% !important; top: 0}

This is powerful. It works with any array properties, even if they're multidimensional. But it is inflexible. You cannot choose not to expand certain array properties. Whether this is good or bad will depend upon your circumstances.

We could use Ramda in the same manner. Here's a partial conversion to using Ramda functions:

const inflate = (obj) => call (
  (field = pipe (toPairs, find (pipe (last, is(Array)))) (obj), [k, v] = field || []) =>
    field ? chain ((v) => inflate (assoc (k, v, obj))) (field [1]) : obj 
)

And we could continue to pull things out until we were totally point-free, but I think we'd slowly be losing readability here, unlike with the earlier Ramda versions, where we ended up with fairly elegant, readable implementations.

Names

None of these versions (in the original or updated answers) does name changes. We don't try to convert "sizes" to "size" or "paths" to "path". If you have control over the input format, I would suggest that you simply switch it there. If not, I would probably do that as a final step. You can do that using Ramda's applySpec or perhaps using something like renameKeys from Ramda's Cookbook . While it might be possible to fold this key renaming into the functions above, I would expect it to be less robust than simply doing a second pass.

You'll need to pass a mapping of old keys to new keys, use R.props to get the value using the old keys, transpose, and then zip back to an object using the new keys (the values of the mappings object):

 const {pipe, props, keys, transpose, map, zipObj, values} = R const fn = mapping => pipe( props(keys(mapping)), // get an array props by keys order transpose, // transpose them map(zipObj(values(mapping))) // map back to an object with the same order ) const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]} console.log(fn({ paths: 'path', thumbnails: 'thumbnail', sizes: 'size' })(obj))
 .as-console-wrapper {max-height: 100% !important; top: 0}
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>

You specifically want to do this using ramada only ?

Coz this can be achieved with vanilla as follows.

for expected output -

obj.paths.map((e, i) => ({ path: obj.paths[i], thumbnail: obj.thumbnails[i], size: obj.sizes[i] }))

for bonus output -

obj.paths.map((e, i) => ({ path: obj.paths[i], thumbnail: obj.thumbnails[i], width: obj.sizes[i][0], height: obj.sizes[i][1] }))

size array can be empty ie. [] -

obj.paths.map((e, i) => ({ path: obj.paths[i], thumbnail: obj.thumbnails[i], width: obj.sizes[i] !== undefined ? obj.sizes[i][0] : '', height: obj.sizes[i] !== undefined ? obj.sizes[i][1] : ''}))

With vanilla Javascript, we can handle with indices easily, but Ramda.js does not have them, so you can use R.forEachObjIndexed to handle your case.

const obj = {
  paths: ['path1', 'path2'],
  thumbnails: ['thumb1', 'thumb2'],
  sizes: [ // may not always be presented
    [100, 200],
    [120, 220],
  ],
};

const result = []
R.forEachObjIndexed((value, index) => {
  const currentData = {
    path: value,
  }
  if(obj.thumbnails && obj.thumbnails[index]) {
    currentData.thumbnail = obj.thumbnails[index]
  }
  if(obj.sizes && obj.sizes[index]) {
    const [width, height] = obj.sizes[index]
    currentData.width = width
    currentData.height = height
  }
  result.push(currentData)
}, obj.paths)

console.log(result)

You can try out with the playground here

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