2

I have this object {country:{town:{company:{boss:{}}}}} and this string country.town.cityhouse.major, I need to updatr the object from the string, but keeping the previous properties and data.

{
  country: {
    town: {
      company: {
        boss: {}
      },
      cityhouse: {
        major: {}
      }
    }
  }
}

This is what I have so far:

function updateObj(object, path) {
            const newPath = path.split('.');
            let temp = {...object};

            for (let i = 0; i < newPath.length; i++) {
                let mid = temp[newPath[i]];
                if (mid) {
                    temp = mid;
                } else {
                    temp[newPath[i]] = {};
                }
            }

            return temp;
        }

        const obj = { country: { town: { company: { boss: {} }}}};
        const r = updateObj(obj, 'country.town.cityhouse.major');

        console.log(r);

but it responds:

{
  company: {
    boss: {}
  },
  cityhouse: {},
  major: {}
}

Any hint on this?

assembler
  • 3,098
  • 12
  • 43
  • 84

4 Answers4

4

You can clean this up a little using logical nullish assignment(??=) and a for...of loop.

function updateObj(object, path) {
  let result = { ...object };

  let temp = result;
  for (const k of path.split('.')) {
    temp = temp[k] ??= {};
  }

  return result;
}

const obj = { country: { town: { company: { boss: {} } } } };
const r = updateObj(obj, 'country.town.cityhouse.major');

console.log(r);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Recursive option

function updateObj(object, path) {

  const result = { ...object };

  const [prop, ...pathArr] = path.split('.');

  result[prop] = pathArr.length 
    ? updateObj((result[prop] ??= {}), pathArr.join('.')) 
    : (result[prop] ??= {});

  return result;
}

const obj = { country: { town: { company: { boss: {} } } } };
const r = updateObj(obj, 'country.town.cityhouse.major');

console.log(r);
.as-console-wrapper { max-height: 100% !important; top: 0; }
pilchard
  • 12,414
  • 5
  • 11
  • 23
  • This is a very nice solution, but, I would add the information that the logical nullish is a new feature and some old browsers or browser versions doesn't support it. https://caniuse.com/?search=logical%20nullish – Calvin Nunes Sep 17 '21 at 16:03
  • Agreed. Could be replaced with an OR shortcircuit `temp = temp[k] || (temp[k] = {});`, or a more verbose ternary `temp = temp[k] !== undefined ? temp[k] : (temp[k] = {});` – pilchard Sep 17 '21 at 16:28
2

This code would normally be written recursively, so you need to have a variable to hold the current scope (the path you walked on through the object) to do the job, and so i created the variable called scope to do that job.

function updateObj(object, path) {
    const newPath = path.split('.');
    let temp = {...object};
    let scope = temp;
    for (let i = 0; i < newPath.length; i++) {
        let cur = scope[newPath[i]];
        if (!cur) {
            scope = scope[newPath[i]] = {};
        } else {
            scope = cur;
        }
    }

    return temp;
}

const obj = { country: { town: { company: { boss: {} }}}};
const r = updateObj(obj, 'country.town.cityhouse.major');

console.log(r);

This is a recursive implementation, but since it requires copying the object and passing it to the next recursive call it's not an efficient one, If i find a better more efficient implementation, I'll update this.

function updateObjRec(object, path, depth = 0) {
    // base case
    // if depth is equal to path length then it's over
    if (depth === path.length) {
        return {};
    }

    const cur = path[depth];
    const scope = object[cur];

    // here we have 2 cases
    // current field already exists
    update = {};
    if (scope) {
        update = updateObjRec({ ...scope}, path, depth + 1);
    } else {
        update = updateObjRec({}, path, depth + 1);
    }

    // merge with given object
    object[cur] = update;

    return object
}

const obj = { country: { town: { company: { boss: {} }}}};
const r = updateObjRec(obj, 'country.town.cityhouse.major'.split('.'));

console.log(r);

Update The recursive code can be rewritten this way to be more efficient

function updateObj(obj, path) {
    const temp = { ...obj };
    const p = path.split('.');

    const updateRec = (scope, depth = 0) => {
        // base case
        if (depth === p.length) return;

        const cur = p[depth];
        if (!scope[cur]) {
          scope[cur] = {};
        }
        updateRec(scope[cur], depth + 1);
    }

    updateRec(temp);

    return temp;
}

const obj = { country: { town: { company: { boss: {} }}}};
const r = updateObj(obj, 'country.town.cityhouse.major');

console.log(r);
  • 3
    Might be worth adding the recursive "normal" that you've alluded to as well. –  Sep 17 '21 at 14:47
  • Just added it, although not very efficient, recursive code is usually easier to implement but not as efficient as iterative code, I'll update it once I find a better way. – Mohamed abdelmagid Sep 17 '21 at 15:13
1

This isn't ideal, but it's a start. Basically, you're returning temp, which is the lowest layer. you want to return the root. Also when you can't find the next layer, you were creating it, but you weren't updating temp.

function updateObj(object, path) {
    const newPath = path.split('.');
    let temp = {...object};
    const result = temp; // need to return this

    for (let i = 0; i < newPath.length; i++) {
        let mid = temp[newPath[i]];
        if (mid) {
            temp = mid;
        } else {
            temp[newPath[i]] = {};
            temp = temp[newPath[i]] // need this
        }
    }

    return result;
}

const obj = { country: { town: { company: { boss: {} }}}};
const r = updateObj(obj, 'country.town.cityhouse.major');

console.log(r);
0

I think this is what you want to achieve

const obj = {
  country: {
    town: {
      company: { boss: {} },
    },
  },
};

obj.country.town.cityhouse = {
  major: {},
};

console.log(obj);
Pablo Silió
  • 304
  • 1
  • 9