emre-ozgun
emre-ozgun

Reputation: 762

Creating an 'object accessor' function - Deeply nested javascript objects

Motivation: Given a deeply nested object, there is 'currency' key at level N.However, level N might not be uniform, meaning that currency key of object A might be located at level 4 and the same key might be located at 9th level of objectB

What I'm trying to achieve is to create a uniform accessor. An instant accessor that would be generated only once and be used when encountered similar array of objects (deeply nested).

Currently, via recursion I've managed to construct the 'accessor path' as a string. How could I apply this to my object ? Is it plausible to attempt this? I'd appreciate your input, thought and opinion.

// testObj[level][id][title][name][currency] === 'USD'

const testObj = {
  level: {
    id: {
      title: {
        name: {
          currency: "USD"
        }
      }
    }
  }
}

// testObj2[a][b][c][currency]
const testObj2 = {
  a: {
    b: {
      c: {
        currency: "USD"
      }
    }
  }
}

const testObj3 = {
  a: {
    b: {
      c: {
        d: {
          e: {
            currency: "USD"
          }
        }
      }
    }
  }
}

const objectAccessCreator = (obj, targetKey, keysArray = []) => {
  for (const [key, value] of Object.entries(obj)) {
    if (key !== targetKey) {
      keysArray.push(key);

      if (typeof value !== 'object' || typeof value === null) {
        return -1;
      } else {
        return objectAccessCreator(value, 'currency', keysArray);
      }
    }
    if (key === targetKey) {
      keysArray.push(key);
    }
  }

  let accessChain = '';

  for (const key of keysArray) {
    accessChain += `[${key}]`;
  }
  return accessChain;
}

const targetKey = 'currency';

// [a][b][c][d][e][currency]
console.log(objectAccessCreator(testObj3, targetKey));

//[level][id][title][name][currency]
console.log(objectAccessCreator(testObj, targetKey));


//[a][b][c][currency]
console.log(objectAccessCreator(testObj2, targetKey));

Upvotes: 0

Views: 180

Answers (2)

KooiInc
KooiInc

Reputation: 122916

A somewhat simplified approach. retrievePathAndValue returns the path and its value in a small object ({path, value,}). If you want to retrieve the value later from the resulting path, you can use [result].path and some function like the one in the selected answer or something like this (see example of the latter).

const  [ testObj, testObj2, testObj3 ] = testData();
const targetKey = 'currency';

console.log(retrievePathAndValue(testObj, targetKey));
console.log(retrievePathAndValue(testObj2, targetKey));
console.log(retrievePathAndValue(testObj3, targetKey));

function retrievePathAndValue(obj, key, path = ``) {
  for (let k of Object.keys(obj)) {
    if (obj[key]) {
      return {
        path: `${path}${path.length < 1 ? `` : `.`}${key}`,
        value: obj[key],
      };
    }

    if (obj[k] instanceof Object && !Array.isArray(obj[k])) {
      return retrievePathAndValue(
        obj[k], 
        key, 
        `${path}${path.length < 1 ? `` : `.`}${k}` );
    }
  }
  
  return {
    path: `[${key}] NOT FOUND`,
    value: undefined, 
  };
}

function testData() {
  const testObj = {
    level: {
      id: {
        title: {
          name: {
            currency: "USD"
          }
        }
      }
    }
  }

  // testObj2[a][b][c][currency]
  const testObj2 = {
    a: {
      b: {
        c: {
          currency: "USD"
        }
      }
    }
  }

  const testObj3 = {
    a: {
      b: {
        c: {
          d: {
            e: {
              currency: "USD"
            }
          }
        }
      }
    }
  }
  return [ testObj, testObj2, testObj3 ];
}
.as-console-wrapper {
    max-height: 100% !important;
}

Upvotes: 1

Terry Lennox
Terry Lennox

Reputation: 30685

You could split the string to a path array, then use Array.reduce() to return the property value.

I'd wrap this up in a getValue() function that takes an object and path arguments.

const objectAccessCreator = (obj, targetKey, keysArray = []) => {
  for (const [key, value] of Object.entries(obj)) {
    if (key !== targetKey) {
      keysArray.push(key);

      if (typeof value !== 'object' || typeof value === null) {
        return -1;
      } else {
        return objectAccessCreator(value, 'currency', keysArray);
      }
    }
    if (key === targetKey) {
      keysArray.push(key);
    }
  }

  let accessChain = '';

  for (const key of keysArray) {
    accessChain += `[${key}]`;
  }
  return accessChain;
}

const targetKey = 'currency';

function getValue(obj, path) {
    return path.split(/[\[\]]+/).filter(s => s).reduce((obj, key) => { 
        return obj[key];
    }, obj);
}

const testObjects = [
  { level: { id: { title: { name: { currency: "USD" } } } } },
  { a: { b: { c: { currency: "USD" } } } },
  { a: { b: { c: { d: { e: { currency: "USD" } } } } } }
];
  
for(let testObj of testObjects) { 
    const path = objectAccessCreator(testObj, targetKey);
    console.log('Generated path:', path);
    console.log('Value at path:', getValue(testObj, path));
}
    
.as-console-wrapper { max-height: 100% !important; }

I'd also suggest maybe using a '.' separator for the paths, this simplifies the logic somewhat:

const objectAccessCreator = (obj, targetKey, keysArray = []) => {
  for (const [key, value] of Object.entries(obj)) {
    if (key !== targetKey) {
      keysArray.push(key);

      if (typeof value !== 'object' || typeof value === null) {
        return -1;
      } else {
        return objectAccessCreator(value, 'currency', keysArray);
      }
    }
    if (key === targetKey) {
      keysArray.push(key);
    }
  }

  return keysArray.join('.');
}

const targetKey = 'currency';

function getValue(obj, path) {
    return path.split('.').reduce((obj, key) => { 
        return obj[key];
    }, obj);
}

const testObjects = [
  { level: { id: { title: { name: { currency: "USD" } } } } },
  { a: { b: { c: { currency: "USD" } } } },
  { a: { b: { c: { d: { e: { currency: "USD" } } } } } }
];
  
for(let testObj of testObjects) { 
    const path = objectAccessCreator(testObj, targetKey);
    console.log('Generated path:', path);
    console.log('Value at path:', getValue(testObj, path));
}
    
.as-console-wrapper { max-height: 100% !important; }

And an example of returning the targetKey value directly from a new getValue() function:

const targetKey = 'currency';

function getValue(obj, property) {
    for(let key in obj) {
        if (obj[key] && (typeof(obj[key]) === 'object')) {
            let value = getValue(obj[key], property);
            if (value !== null) {
                return value;
            }
        } else if (key === property) {
            return obj[key];
        }
    }
    return null;
}

const testObjects = [
  { level: { id: { title: { name: { currency: "USD" } } } } },
  { a: { b: { c: { currency: "USD" } } } },
  { a: { b: { c: { d: { e: { currency: "USD" } } } } } }
];
  
for(let testObj of testObjects) { 
    console.log(`Value of "${targetKey}":`, getValue(testObj, targetKey));
}
    
.as-console-wrapper { max-height: 100% !important; }

Upvotes: 1

Related Questions