Find and return a data-structure's nested value by a given keypath

155 views Asked by At

So, we are working with data-objects parsed from JSON-strings.

We did several exercises (8 out of 10 well done), but I'm stuck with the ninth.

The assignment says:

Find and return the value at the given nested path in the JSON [data]-object.

And what it gives us is to start with:

function findNestedValue(obj, path) {
}

The object we are working with/on is:

const sampleDate = {
  people: [{
    name: 'Alice',
    age: 30,
  }, {
    name: 'Bob',
    age: 25,
  }, {
    name: 'Charlie',
    age: 35,
  }],
  city: 'New York',
  year: 2023,
};

If I look in the test.js file (where there is the code for npm to test the results of our function), it says:

test('findNestedValue should find and return the value at the given nested path', () => {

  expect(findNestedValue(sampleData, 'people[0].name')).toBe('Alice');
  expect(findNestedValue(sampleData, 'city')).toBe('New York');
});

Honestly, I'm lost, completely. Any tip? I'm lost, lost lost.

Any advice?

 for (let key in obj) {
    if (Array.isArray(obj[key])) {
      for (let i = 0; i < obj[key].length; i++) {
        if (obj[key][i] === path)
    else {
        if (obj[key] === path)

I was trying something like that, but I'm really just wandering in the dark.

4

There are 4 answers

7
Unmitigated On

You could first replace all square brackets, then split on . to get all elements of the path. Array#reduce can be used to access each key in turn.

function findNestedValue(obj, path) {
  return path.replaceAll('[', '.').replaceAll(']', '').split('.')
    .reduce((acc, curr) => acc?.[curr], obj);
}
const o = { 
people: [ 
{ name: 'Alice', age: 30 }, 
{ name: 'Bob', age: 25 }, 
{ name: 'Charlie', age: 35 }, 
], 
city: 'New York', 
year: 2023, };
console.log(findNestedValue(o, 'people[0].name'));
console.log(findNestedValue(o, 'city'));

10
Alexander Nenashev On

Just use a generated function. In other cases make sure your path values aren't 3rd party to avoid XSS attacks:

function findNestedValue(obj, path) {
    try {
        return new Function('obj', 'return obj.' + path)(obj);
    } catch (_) {}
}
const o = { 
people: [ 
{ name: 'Alice', age: 30 }, 
{ name: 'Bob', age: 25 }, 
{ name: 'Charlie', age: 35 }, 
], 
city: 'New York', 
year: 2023, };
console.log(findNestedValue(o, 'people[0].name'));
console.log(findNestedValue(o, 'city'));

2
Nina Scholz On

You could split the path with a regular expression which removes speparators as well.

For getting a value, you could iterate the key, check if the key exist and assign the property value to obj.

Finally return obj.

function findNestedValue(obj, path) {
    for (const key of path.match(/[^\[\]\.]+/g)) { // anything without [].
        if (!(key in obj)) return;
        obj = obj[key];
    }
    return obj;
}

const
    object = { people: [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }, { name: 'Charlie', age: 35 }], city: 'New York', year: 2023 };

console.log(findNestedValue(object, 'people[0].name')); // 'Alice'
console.log(findNestedValue(object, 'city')); // 'New York'

4
Scott Sauyet On

Building your own

As this is a homework assignment, the goal should be to help you create your own version.

The most important part if figuring how to break this problem down into component parts. It seems to me that there is a clear way to do so for this problem.

You have a string representing a path. But a string is not the most useful tool here (unless you take the path given by Alexander Nenashev and try to turn your string into code with eval, or the Function constructor. Note that if you do try that, your instructor is likely to pelt you with programming textbooks, not to mention giving you no credit for the assignment.)

Much more useful would be an array of node names. Instead of people[0].name, it would be very useful to have ['people', 0, 'name']. This makes finding the nested property much easier (more below).

So with that breakdown, we have two goals:

  • breaking the path down into constituent parts in an array
  • using this new array, pull out the named properties one at a time from a working object.

Breaking down the path

Breaking the path down is the job of a parser. Parsers are a big field, but we won't need the full power of general-purpose parsers because we're about to make some simplifying assumptions. That means that we can parse with relatively simple tools.

Assumptions

We don't have full specifications for the input we may receive. I would suggest that we start by trying to capture the sorts of input obviously similar to the test cases. If the instructor comes back with more complex cases, you can rethink this.

This means that we should worry about simple properties only, string ones including only letter and digit characters, and number ones containing only digits. This means that we're going to ignore the comments pointing out that some answers won't work for input such as people[0]['name.first']. We need to be able to handle things like people, 0, and name, but not objects with property names such as 'name.first'. If we later get additional requirements, we'll come back and refactor our solution to match.

Parsing

If we look at some sample paths, we might have simple things like 'city', somewhat more complex ones like 'people[0].name', or even more more complex ones like 'foo[0][42].bar[1].baz.qux[7]'. We can look at the punctuation between our component parts, things like [, ][, ]., ., and ]. One way to parse our input is to split the string at any of these bits of punctuation.

If you've already studied regular expressions, then they will make this simple enough, just call String.prototype.replace, passing an appropriate regular expression with the global flag. But if you haven't done regex yet, one possibility would be to use String.prototype.replaceAll several times in a row, once for each of the punctuation strings you want, turning, say, 'foo[0][42].bar[1].baz.qux[7]' into 'foo|0|42|bar|1|baz|qux|7|'. Then you can use String.prototype.split to turn that into ['foo', '0', '42', 'bar', '1', 'baz', 'qux', '7', '']. Note the empty string at the end. We'll need to remove that, which we can do with Array.prototype.filter.

At this point, we have an array of strings, ['foo', '0', '42', 'bar', '1', 'baz', 'qux', '7'] We originally said we wanted strings or integers, with the assumption that the integers would represent array indices. We run into a small snag here, because it's perfectly legitimate to have an object with a property which looks like the string version of an array index: {'1': 'foo', '2': 'bar'}. So how will we distinguish between actual array indices and object properties, between 0 and '0'? The answer is simple enough: we don't have to! JavaScript arrays are built on top of objects. When you ask for property 0 from an array, it converts that to the string '0'. So when searching for properties, this simply doesn't matter. (If we were constructing objects using these paths, we would need somewhat better parsing.)

Traversing our object

Now that we have our path in a convenient format, we need to walk along it, pulling out the appropriate objects at every step and returning the last one.

Recursive

If you've already studied recursion (if not, see next section), this would best be expressed with a recursive function. If the path array is empty, we're done, and we return our input object. If it's not, then we grab the property given in the first array entry from the object, and then recur using that result and the remainder of the path, returning the result of the recursion.

You will need to do some error processing, which can be done in either of two ways. First, we can stop as soon as the sought-after property doesn't exist, either throwing an error or returning undefined. Second, we could always look for the property on (obj || {}), giving ourselves a means to keep passing something useful through the chain of lookups, but still returning undefined. The first of these is more efficient. The second is arguably more elegant and is simpler to implement.

Iterative

Using your choice of a for-loop or a while-loop, you can loop through the elements of the array, updating a working object as you go. When you're through with the loop, you can return the working object.

You have exactly the same error-checking responsibilities as mentioned in the Recursive section above. You can either stop when the property doesn't exist -- again either throwing an error or returning undefined -- or you can make the working object undefined and eventually return at the end of the loop. Here, I think the argument in favor of efficiency probably wins, since the iterative code is not particularly elegant to begin, continuing the loop does not seem to simplify our code any.

Not the only breakdown

This is one approach to the problem. There are certainly others. The important thing, though, is to look to break complex problems down into simpler pieces. If that involves adding intermediate data structures such as our array of node identifiers, it's almost never a problem. And it can offer real benefit.