0

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?

4 Answers 4

2

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>

Sign up to request clarification or add additional context in comments.

3 Comments

Nice, I considered doing such a mapping, but chose to go in another direction. An interesting extension would be using a specification such as {path: 'paths', thumbnail: 'thumbnails', width: 'sizes.0', height: 'sizes.1'},
Updated my answer to include a version with that sort of specification.
I like your solution, very clean
2

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.

2 Comments

Spec-based approach is neat. Also running Ramda implementation, More abstract Ramda implementation and Powerful, but less flexible vanilla implementation didn't produce an intended result. sizes property is of type number and property names are off
Right, as I said, I originally misread the question, but kept them here as the answer to a somewhat related question. The only ones close to your actual requirements are the first two.
0

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] : ''}))

2 Comments

thanks! just updated the question, sizes property may not be always presented as a whole
thanks, though vanilla js solution is mostly trivial I'm looking for a ramda way of doing this specifically for the sake of our code base style
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.