Asderex
Asderex

Reputation: 23

Convert an arbitrarily deeply nested value into an object key using Ramda or vanillaJS

I am using a library that returns a series of values in a slightly unconventional way. Rather than returning an array the library returns nested objects with the final leaf node included as a value. For example:

red.walk.fast.true becomes {red: {walk: {fast: 'true'}}} or climbing.ascending becomes {climbing: 'ascending'}

The format of nested objects actually suits my use case but I need a way of converting the final value into another nested object (with a null value - but this is not important) rather than a value. So:

{red: {walk: {fast: 'true'}}} becomes {red: {walk: {fast: {true: null}}}}

The formula needs to work for any arbitrlily deeply nested object. I am struggling to get a recursive function working with Ramda though. I thought the following would work:

const stringPropToObject = R.ifElse(
    R.is(String),
    R.assoc(R.__, null, {}),
    mapAllStringPropsToObjs);

const mapAllStringPropsToObjs = R.map(stringPropToObject)

const updatedObject = mapAllStringPropsToObjs({ a: { b: { c: 'd' } } })

console.log(updatedObject);

mapAllStringPropsToObjs (attempts) to map over the objects properties passing them to stringPropToObject. This then looks at the property passed in.

This results in "Cannot access 'mapAllStringPropsToObjs' before initialization". I understand why I am getting this error with the current order of declarations but I don't see how I can order them to avoid it - they are co-dependant functions.

Anyone know what I could do using Ramda or vanilla JS to be able to convert: {climbing: 'ascending'} into {climbing: {ascending: null}} and {red: {walk: {fast: 'true'}}} into {red: {walk: {fast: {true: null}}}} or any other arbitrarily nested value.

Thanks in advance for any suggestions.

Upvotes: 1

Views: 625

Answers (4)

Scott Sauyet
Scott Sauyet

Reputation: 50807

The simplest way to fix your co-dependent functions issue is to simply replace the reference in the first one to the second with a lambda:

const stringPropToObject = ifElse(
  is(String),
  assoc(__, null, {}),
  x => mapAllStringPropsToObjs(x)
);

const mapAllStringPropsToObjs = map(stringPropToObject)


const updatedObject = mapAllStringPropsToObjs({ a: { b: { c: 'd' } } })

console.log (updatedObject)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {ifElse, is, assoc, __, map} = R                               </script>

Ramda isn't particularly designed to work well with recursion, but this is of course a basic issue in the language.

I would probably have written this differently, in vanilla JS, with something like this:

const transform = (obj) => 
  Object .entries (obj) 
    .reduce ((a, [k, v]) => ({
      ...a,
      [k]: typeof v == 'string' ? {[v]: null} : transform (v)
    }), {})

But I think that your recursive Ramda solution with this minor tweak is simpler and cleaner.

Upvotes: 1

Ori Drori
Ori Drori

Reputation: 193087

As stated by @ScottSauyet you can use a function (anonymous or arrow) to call mapAllStringPropsToObjs() in the future when it's actually defined.

A piece of code that performs a delayed computation is called a thunk. According to wikipedia:

In computer programming, a thunk is a subroutine used to inject an additional calculation into another subroutine. Thunks are primarily used to delay a calculation until its result is needed, or to insert operations at the beginning or end of the other subroutine.

In addition, I would also replace R.assoc with a flipped R.objOf to generate the object.

const objOfNull = flip(objOf)(null);

const mapAllStringPropsToObjs = map(ifElse(
  is(String),
  objOfNull,
  x => mapAllStringPropsToObjs(x)
));

const updatedObject = mapAllStringPropsToObjs({ a: { b: { c: 'd' } } })

console.log (updatedObject)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {map, ifElse, is, flip, objOf} = R                               </script>

Upvotes: 1

Richard
Richard

Reputation: 7443

This solution is recursive and creates a new converted object instead of altering the old one.

function transformObject(obj, final = {}, curr = final) {
  Object.entries(obj).forEach(([key, value]) => {
    curr[key] = {}
    if (typeof value !== 'object') curr[key][value] = null
    else transformObject(value, final, curr[key])
  })
  return final
}

const dataOne = {climbing: {run: 'slow', hike: {mountain: 'fast'}}}
const dataTwo = {red: {walk: {fast: 'true'}}}

console.log(dataOne, transformObject(dataOne))
console.log(dataTwo, transformObject(dataTwo))

Upvotes: 0

Nina Scholz
Nina Scholz

Reputation: 386858

You could iterate the entries and if no object, assign a new one.

function f(object) {
    Object.entries(object).forEach(([k, v]) => {
        if (v && typeof v === 'object') f(v);
        else object[k] = { [v]: null };
    });
}

var object = { red: { walk: { fast: 'true' } } };

f(object);
console.log(object);

Upvotes: 1

Related Questions