Reputation: 762
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
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
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