0

I wrote the following function to traverse a JSON-structured data to locate the value of the provided targetField and the paths pathArr, but I am struggling with returning result from the recursion, because the value for the targetField may not exist.

This function uses a global variable named result for returning the targetField's value, but this can get carried over to the next call of findValue(), which may not find the value but shows the last call's result. So I am wondering what's the best way of solving this problem without using the global variable result.

If the call of findValue() does not find the targetField's value, just return null as the result.

The pathArr is something like ["first_level", "second_level", "third_level"];

var result = null;
function findValue(jsonData, pathArr, targetField) {

if (pathArr.length == 0) {
    result = targetField in jsonData ? jsonData[targetField] : result;
    return result;
} else {

    var curNode = pathArr.shift();

    if (curNode in jsonData) {
        jsonData = jsonData[curNode];
    }

    if (Array.isArray(jsonData)) {
        jsonData.forEach(function(thisData) {
            findValue(thisData, pathArr, targetField);
        });
    } else {
        findValue(jsonData, pathArr, targetField);
    }
}   

return result;
}

Edit: Thanks to everyone who replied.

Using unobf's improved code, I used this testData to do a testing, but it seems only returning the last match:

var testData = {
"num_found" : 3,
"category" : "social",

"groups": [ 
{"group" : {
    "source" : [{"id" : "testID1", "num": 10, "field": "sociaology", "sub-subject" : "socialeconomy"}, 
                {"id" : "testID2", "num": 20, "field": "mathematics", "sub-subject": ""}, 
                {"id" : "testID3", "num": 7, "field": "biology", "sub-subject" : ""}
                ],
    "identifier" : "shelf-01-E-XW1"
}},
{"group" : {
    "source" : [{"id" : "testID4", "num": 50, "field": "sociaology2", "sub-subject" : ""}, 
                {"id" : "testID5", "num": 44, "field": "mathematics2", "sub-subject": ""}, 
                {"id" : "testID6", "num": 75, "field": "biology2", "sub-subject" : "european studies2"}
                ],
    "identifier" : "shelf-02-W-EW3"
}},

{"group" : {
    "source" : [{"id" : "testID7", "num": 59, "field": "sociaology3", "sub-subject" : "socialeconomy3"}, 
                {"id" : "testID8", "num": 47, "field": "mathematics3", "sub-subject": ""}, 
                {"id" : "testID9", "num": 76, "field": "biology3", "sub-subject" : "european studies3"}
                ],
    "identifier" : "shelf-03-W-GW5"
}}
]
};

and I found I had to change this line of code

return {value : result};

to

return result;

Test:

for (var i = 0; i < testData.groups.length; i++) {
    var value = findValue(testData.groups[i], ["group", "source"], "sub-subject");
    console.log("found value: " + value);
}
3
  • 1
    Look up breadth-first search (en.wikipedia.org/wiki/Breadth-first_search), and depth-first search (en.wikipedia.org/wiki/Depth-first_search), there's algorithms available that explains how to code this.. Commented Dec 31, 2014 at 15:20
  • Why are you calling findValue recursively without returning its return value, instead relying on that call setting a global variable (result)? Commented Dec 31, 2014 at 15:21
  • I consider the global variable result a temporary solution. I first tried to narrow the scope of the variable result inside the function, but it didn't work, but let me try it again. Commented Dec 31, 2014 at 15:25

2 Answers 2

1

Fundamentally, all of the paths through your function must return a value. Currently, there's a path that doesn't, the path that takes the forEach:

jsonData.forEach(function(thisData) {
    findValue(thisData, pathArr, targetField);
});

There, you probably want some (so you can stop when you find it) and a variable that you can set to the found value, e.g:

var retval;

and

jsonData.some(function(thisData) {
    retVal = findValue(thisData, pathArr, targetField);
    return !!retVal; // Stops the `some` loop, doesn't exit `findValue`
});
if (retVal) {
    return retVal;
}

Side note: You don't want to declare result outside the function, there's no need. The function can be entirely self-contained. Here's a version that is self-contained, for instance:

function findValue(jsonData, pathArr, targetField) {
    var result = null;

    if (pathArr.length == 0) {
        result = targetField in jsonData ? jsonData[targetField] : null;
    } else {

        var curNode = pathArr.shift();

        if (curNode in jsonData) {
            jsonData = jsonData[curNode];
        }

        if (Array.isArray(jsonData)) {
            jsonData.some(function(thisData) {
                result = findValue(thisData, pathArr, targetField);
                return !!result; // Stops the `some` loop, doesn't exit `findValue`
            });
        } else {
            result = findValue(jsonData, pathArr, targetField);
        }
    }

    return result;
}

Note: I didn't do a full review of the code, just looked at return values.

Sign up to request clarification or add additional context in comments.

4 Comments

thanks, I am trying to make the function entirely self-contained. Using a global variable to contain the return value is not a good idea at all.
Should use Array.find() instead of the closure retVal function - see my solution
@unobf: Array#find is neither better nor worse (except for requiring a polyfill) than using some.
@TonyGW: I see in another comment you have to support IE8. Note that Array#some (used above) will require a polyfill on such an old browser (it's from ES5, finalized in 2009), whereas anything modern will have Array#some (but may not have Array#find yet, as that's new in ES6, which hasn't been finalized yet).
1

Here is the solution with some test data

function findValue(jsonData, pathArr, targetField) {
    var result = null;

    if (pathArr.length == 0 && jsonData.hasOwnProperty(targetField)) {
        result = targetField in jsonData ? jsonData[targetField] : result;
        return { value : result};
    } else {
        jsonData = jsonData[pathArr.shift()]
        if (Array.isArray(jsonData)) {
            jsonData.forEach(function(thisData) {
                result = findValue(thisData, pathArr, targetField);
            });
        } else if (typeof jsonData !== 'undefined') {
            return findValue(jsonData, pathArr, targetField);
        }
    }   
    return result;
}

testData = [{
    hello : 'hello',
    pathArr: []
}, {
    child : {
        hello : false
    },
    pathArr: ['child']
}, {
    child : {
        hello : null
    },
    pathArr: ['child']
}, {
    child: {
        child: {
            hell: 'hell'
        }
    },
    pathArr: ['child', 'child']
}];



testData.forEach(function (json) {
    console.log(findValue(json, json.pathArr, 'hello'));
});

6 Comments

Note: Array#find is an ES6 feature not present in many current browsers. You'll need a polyfill if you rely on it.
The question did not indicate whether a browser-only solution is required. This will work in modern browsers and Node.js
The assumption for browsers is IE 8 or older, chrome, and firefox, with IE being the majority.
Updated the solution to do two more things: work in browsers and also be able to return a value if the value of the json attribute is "null"
thanks, let me do some testing and I'll come back and report :)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.