12

I have a bunch of object attributes coming in as dot-delimited strings like "availability_meta.supplier.price", and I need to assign a corresponding value to record['availability_meta']['supplier']['price'] and so on.

Not everything is 3 levels deep: many are only 1 level deep and many are deeper than 3 levels.

Is there a good way to assign this programmatically in JavaScript? For example, I need:

["foo.bar.baz", 1]  // --> record.foo.bar.baz = 1
["qux.qaz", "abc"]  // --> record.qux.qaz = "abc"
["foshizzle", 200]  // --> record.foshizzle = 200

I imagine I could hack something together, but I don't have any good algorithm in mind so would appreciate suggestions. I'm using Lodash if that's helpful, and open to other libraries that may make quick work of this.

Edit

This is on the backend and run infrequently, so not necessary to optimize for size, speed, etc. In fact code readability would be a plus here for future devs.

Edit 2

To clarify further, I need to be able to do this assignment multiple times for the same object.

halfer
  • 19,824
  • 17
  • 99
  • 186
Tyler
  • 11,272
  • 9
  • 65
  • 105
  • 5
    1. Split it by `.` 2. Use `for`. 3. ??????? 4. PROFIT!!!!111 – zerkms Jan 21 '15 at 02:19
  • Ha yes it's all about that `???????` section – Tyler Jan 21 '15 at 02:20
  • Angular's [`$parse`](https://docs.angularjs.org/api/ng/service/$parse) does this quite well. Maybe have a look at the [source code](https://github.com/angular/angular.js/blob/master/src/ng/parse.js#L956) for that – Phil Jan 21 '15 at 02:20
  • 1
    @tyler it's nothing there actually - you split, iterate and assign – zerkms Jan 21 '15 at 02:21
  • @zerkms could you write out a quick sample? The assignment part with multiple lengths is what I find tricky – Tyler Jan 21 '15 at 02:22
  • There is no multiple "lengths". It's as simple as `item = item[key];` in a loop. – zerkms Jan 21 '15 at 02:24
  • This isn't an exact duplicate so I'm not going to mark it as such but if you `split` the dotted string into an array, you can then use this answer ~ http://stackoverflow.com/a/20987675/283366 – Phil Jan 21 '15 at 02:29
  • possible duplicate of [Accessing nested JavaScript objects with string key](http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key) – loganfsmyth Jan 21 '15 at 02:33
  • @loganfsmyth that's fine for reading properties but OP wants to write them – Phil Jan 21 '15 at 02:34
  • @Phil Decent point, removed my vote. The logic is pretty much the same though. At least now the link to the other is in the comments. – loganfsmyth Jan 21 '15 at 02:36
  • possible duplicate of [Convert Javascript string in dot notation into an object reference](http://stackoverflow.com/questions/6393943/convert-javascript-string-in-dot-notation-into-an-object-reference) –  Jan 21 '15 at 02:36
  • also a duplicate of http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key and many, many others. – Ben Grimm Jan 21 '15 at 02:50
  • @loganfsmyth it is not a duplicate. Please see my EDIT 2 above – Tyler Jan 21 '15 at 02:56
  • @torazaburo it is not a duplicate. Please see my EDIT 2 above – Tyler Jan 21 '15 at 02:57
  • @BenGrimm it is not a duplicate. Please see my EDIT 2 above – Tyler Jan 21 '15 at 02:57
  • And yes it is ironic to duplicate a comment about the question not being a duplicate – Tyler Jan 21 '15 at 02:58
  • It may not be a duplicate but the concepts are so closely related that it might as well be. –  Jan 21 '15 at 03:42

6 Answers6

14

You mentioned lodash in your question, so I thought I should add their easy object set() and get() functions. Just do something like:

_.set(record, 'availability_meta.supplier.price', 99);

You can read more about it here: https://lodash.com/docs#set

These functions let you do more complex things too, like specify array indexes, etc :)

Matt Way
  • 32,319
  • 10
  • 79
  • 85
  • 1
    Agreed. While understanding how to implement something like this can be a good exercise in data structures, not having to implement _and maintain_ it yourself is pretty much always a plus. Since the OP explicitly mentions already using lodash, this seems like it should be the accepted answer. – codermonkeyfuel Jan 23 '16 at 04:42
8

Something to get you started:

function assignProperty(obj, path, value) {
    var props = path.split(".")
        , i = 0
        , prop;

    for(; i < props.length - 1; i++) {
        prop = props[i];
        obj = obj[prop];
    }

    obj[props[i]] = value;

}

Assuming:

var arr = ["foo.bar.baz", 1];

You'd call it using:

assignProperty(record, arr[0], arr[1]);

Example: http://jsfiddle.net/x49g5w8L/

Andrew Whitaker
  • 124,656
  • 32
  • 289
  • 307
  • Note that you are missing a param when you call assignProperty. – ianaya89 Jan 21 '15 at 02:33
  • @PastorBones: I've rolled back your edit. The way it's written is as designed. This is subtle, but the reason it goes to `length - 1` is because after the loop ends, `i` is equal to the last property (in the example, `baz`). So the final line in the function assigns the value to the correct property. – Andrew Whitaker Apr 29 '16 at 17:19
  • @AndrewWhitaker I actually caught my mistake a few minutes later and you beat me to it, Thx for keeping humble...I will go write 9,001 lines of code now for my error. – Pastor Bones May 04 '16 at 02:30
  • This doesn't work unless your object already has all properties leading up to the property being set. You'll get an undefined error if foo doesn't have a bar object already defined above. Which pretty much makes it useless. – Zambonilli Jun 30 '16 at 17:31
  • @Zambonilli: You're absolutely right, it is useless for that use case, because it wasn't designed for it. It was designed for the OP's question, which implies (to me at least) that `record` and its children have the requisite properties in order for this to work. – Andrew Whitaker Jun 30 '16 at 23:55
  • Fair enough. For those looking for a solution that fills in missing properties the second answer, lodash.set, works with sub-properties. – Zambonilli Jul 06 '16 at 18:16
6

What about this?

function convertDotPathToNestedObject(path, value) {
  const [last, ...paths] = path.split('.').reverse();
  return paths.reduce((acc, el) => ({ [el]: acc }), { [last]: value });
}

convertDotPathToNestedObject('foo.bar.x', 'FooBar')
// { foo: { bar: { x: 'FooBar' } } }
ondrejbartas
  • 117
  • 1
  • 3
  • I'm working with GraphQL, and this is close to the sort of thing I need to work with deeply nested updates. See: https://medium.com/pro-react/a-brief-talk-about-immutability-and-react-s-helpers-70919ab8ae7c – Jeff Lowery Apr 18 '17 at 16:52
  • thanks. i was really just wasting my time trying to write this myself. – Patrick Michaelsen Mar 28 '22 at 19:38
2

Just do

record['foo.bar.baz'] = 99;

But how would this work? It's strictly for the adventurous with a V8 environment (Chrome or Node harmony), using Object.observe. We observe the the object and capture the addition of new properties. When the "property" foo.bar.baz is added (via an assignment), we detect that this is a dotted property, and transform it into an assignment to record['foo']['bar.baz'] (creating record['foo'] if it does not exist), which in turn is transformed into an assignment to record['foo']['bar']['baz']. It goes like this:

function enable_dot_assignments(changes) {

    // Iterate over changes
    changes.forEach(function(change) {

        // Deconstruct change record.
        var object = change.object;
        var type   = change.type;
        var name   = change.name;

        // Handle only 'add' type changes
        if (type !== 'add') return;

        // Break the property into segments, and get first one.
        var segments = name.split('.');
        var first_segment = segments.shift();

        // Skip non-dotted property.
        if (!segments.length) return;

        // If the property doesn't exist, create it as object.
        if (!(first_segment in object)) object[first_segment] = {};

        var subobject = object[first_segment];

        // Ensure subobject also enables dot assignments.
        Object.observe(subobject, enable_dot_assignments);

        // Set value on subobject using remainder of dot path.
        subobject[segments.join('.')] = object[name];

        // Make subobject assignments synchronous.
        Object.deliverChangeRecords(enable_dot_assignments);

        // We don't need the 'a.b' property on the object.
        delete object[name];
    });
}

Now you can just do

Object.observe(record, enable_dot_assignments);
record['foo.bar.baz'] = 99;

Beware, however, that such assignments will be asynchronous, which may or may not work for you. To solve this, call Object.deliverChangeRecords immediately after the assignment. Or, although not as syntactically pleasing, you could write a helper function, also setting up the observer:

function dot_assignment(object, path, value) {
    Object.observe(object, enable_dot_assignments);
    object[path] = value;
    Object.deliverChangeRecords(enable_dot_assignments);
}

dot_assignment(record, 'foo.bar.baz', 99);
1

Something like this example perhaps. It will extend a supplied object or create one if it no object is supplied. It is destructive in nature, if you supply keys that already exist in the object, but you can change that if that is not what you want. Uses ECMA5.

/*global console */
/*members split, pop, reduce, trim, forEach, log, stringify */
(function () {
    'use strict';

    function isObject(arg) {
        return arg && typeof arg === 'object';
    }

    function convertExtend(arr, obj) {
        if (!isObject(obj)) {
            obj = {};
        }

        var str = arr[0],
            last = obj,
            props,
            valProp;

        if (typeof str === 'string') {
            props = str.split('.');
            valProp = props.pop();
            props.reduce(function (nest, prop) {
                prop = prop.trim();
                last = nest[prop];
                if (!isObject(last)) {
                    nest[prop] = last = {};
                }

                return last;
            }, obj);

            last[valProp] = arr[1];
        }

        return obj;
    }

    var x = ['fum'],
        y = [
            ['foo.bar.baz', 1],
            ['foo.bar.fum', new Date()],
            ['qux.qaz', 'abc'],
            ['foshizzle', 200]
        ],
        z = ['qux.qux', null],
        record = convertExtend(x);

    y.forEach(function (yi) {
        convertExtend(yi, record);
    });

    convertExtend(z, record);
    document.body.textContent = JSON.stringify(record, function (key, value, Undefined) {
        /*jslint unparam:true */
        /*jshint unused:false */
        if (value === Undefined) {
            value = String(value);
        }

        return value;
    });
}());
Xotic750
  • 22,914
  • 8
  • 57
  • 79
0

it's an old question, but if anyone still looking for a solution can try this

function restructureObject(object){
    let result = {};
    for(let key in object){
        const splittedKeys = key.split('.');
        if(splittedKeys.length === 1){
            result[key] = object[key];
        }
        else if(splittedKeys.length > 2){
            result = {...result, ...{[splittedKeys.splice(0,1)]: {}} ,...restructureObject({[splittedKeys.join('.')]: object[key]})}
        }else{
            result[splittedKeys[0]] = {[splittedKeys[1]]: object[key]}
        }
        
    }

    return result
}