9

I ran into this potential scenario that I posed to a few of my employees as a test question. I can think of a couple ways to solve this problem, but neither of them are very pretty. I was wondering what solutions might be best for this as well as any optimization tips. Here's the question:

Given some arbitrary string "mystr" in dot notation (e.g. mystr = "node1.node2.node3.node4") at any length, write a function called "expand" that will create each of these items as a new node layer in a js object. For the example above, it should output the following, given that my object name is "blah":

blah: { node1: { node2: { node3: { node4: {}}}}}

From the function call:

mystr = "node1.node2.node3.node4";
blah = {};
expand(blah,mystr);

Alternately, if easier, the function could be created to set a variable as a returned value:

mystr = "node1.node2.node3.node4";
blah = expand(mystr);

Extra credit: have an optional function parameter that will set the value of the last node. So, if I called my function "expand" and called it like so: expand(blah, mystr, "value"), the output should give the same as before but with node4 = "value" instead of {}.

4
  • w3schools.com/jsref/jsref_split.asp ??? Commented Jul 16, 2015 at 21:00
  • 1
    You should post your solution to CodeReview and ask for improvements there. Commented Jul 16, 2015 at 21:01
  • This is essentially a DFS (depth-first search), which uses a stack to keep track of the level you are on. Commented Jul 16, 2015 at 21:01
  • @dave: This is mostly hypothetical code, which is off-topic on Code Review. Commented Jul 16, 2015 at 21:03

4 Answers 4

23

In ES6 you can do it like this:

const expand = (str, defaultVal = {}) => {
  return str.split('.').reduceRight((acc, currentVal) => {
    return {
      [currentVal]: acc
    }
  }, defaultVal)
}

const blah = expand('a.b.c.d', 'last value')

console.log(blah)

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

Comments

12

Here's a method that popped up in my mind. It splits the string on the dot notation, and then loops through the nodes to create objects inside of objects, using a 'shifting reference' (not sure if that's the right term though).

The object output within the function contains the full object being built throughout the function, but ref keeps a reference that shifts to deeper and deeper within output, as new sub-objects are created in the for-loop.

Finally, the last value is applied to the last given name.

function expand(str, value)
{
    var items = mystr.split(".") // split on dot notation
    var output = {} // prepare an empty object, to fill later
    var ref = output // keep a reference of the new object

    //  loop through all nodes, except the last one
    for(var i = 0; i < items.length - 1; i ++)
    {
        ref[items[i]] = {} // create a new element inside the reference
        ref = ref[items[i]] // shift the reference to the newly created object
    }

    ref[items[items.length - 1]] = value // apply the final value

    return output // return the full object
}

The object is then returned, so this notation can be used:

mystr = "node1.node2.node3.node4";
blah = expand(mystr, "lastvalue");

5 Comments

@Oscar why the lack of semi-colons?
They are not mandatory, but it is considered good practice to have them. The function here could not be minified without them but then the comments also needed to go.
In my opinion it looks way cleaner without semicolons. If you know when you should use them, you can leave them out when it's safe. UglifyJS2 minifies this perfectly without semicolons. It's always a controversial topic, but JavaScript's semicolons are most of the times optional.
Douglas Crockford would not be happy with this response.
Well like I said it's a very controversial topic, hence the very different opinions in the comments of the topic you linked to.
1
var obj = {a:{b:{c:"a"}}};
const path = "a.b.c".split(".");
while(path.length > 1){
   obj = obj[path.shift()];
}
obj[path.shift()] = "a";

Comments

0

Having this:

const text = `auth.errors.invalid_otp
logout.success.message
common.operations.deleted.successful
validation.attributes.cart_item
common.operations.deleted.successful
validation.attributes.cart_item
common.messages.not_found
coupon.messages.max_usage
cart.messages.empty
cart.messages.empty`

You can convert it to json with this:

const jsonObj = text.split('\n').reduce((acc, cur) => {
    let obj = acc;
    const keys = cur.split('.');
    for (const key of keys) {
        obj[key] ??= keys.at(-1) == key ? '' : {};
        obj = obj[key]
    }
    return acc;
}, {});

console.log(JSON.stringify(jsonObj, null, 2));

Output:

{
  "auth": {
    "errors": {
      "invalid_otp": ""
    }
  },
  "logout": {
    "success": {
      "message": ""
    }
  },
  "common": {
    "operations": {
      "deleted": {
        "successful": ""
      }
    },
    "messages": {
      "not_found": ""
    }
  },
  "validation": {
    "attributes": {
      "cart_item": ""
    }
  },
  "coupon": {
    "messages": {
      "max_usage": ""
    }
  },
  "cart": {
    "messages": {
      "empty": ""
    }
  }
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.