How Can I Retrieve Dynamically Specified, Arbitrary And Deeply Nested Values From A Javascript Object Containing Strings, Objects, And Arrays?
Solution 1:
Here is a solution using object-scan.
The only tricky part is the transformation of the search input into what object-scan expects.
// const objectScan = require('object-scan');const response = { id: '0', version: '0.1', interests: [{ categories: ['baseball', 'football'], refreshments: { drinks: ['beer', 'soft drink'] } }, { categories: ['movies', 'books'], refreshments: { drinks: ['coffee', 'tea'] } }], goals: [{ maxCalories: { drinks: '350', pizza: '700' } }] };
constfind = (haystack, needle) => {
const r = objectScan(
[needle.match(/[^.[\]]+/g).join('.')],
{ rtn: 'value', useArraySelector: false }
)(haystack);
return r.length === 1 ? r[0] : r.reverse();
};
console.log(find(response, 'interests[categories]'));
// => [ 'baseball', 'football', 'movies', 'books' ]console.log(find(response, 'interests.refreshments.drinks'));
// => [ 'beer', 'soft drink', 'coffee', 'tea' ]console.log(find(response, 'goals[maxCalories][drinks]'));
// => 350
.as-console-wrapper {max-height: 100%!important; top: 0}
<scriptsrc="https://bundle.run/object-scan@13.8.0"></script>
Disclaimer: I'm the author of object-scan
Solution 2:
Update
After thinking about this over night, I've decided that I really don't like the use of coarsen
here. (You can see below that I waffled about it in the first place.) Here is an alternative that skips the coarsen
. It does mean that, for instance, passing "id"
will return an array containing that one id, but that makes sense. Passing "drinks"
returns an array of drinks, wherever they are found. A consistent interface is much cleaner. All the discussion about this below (except for coarsen
) still applies.
// utility functionsconstlast = (xs) =>
xs [xs.length - 1]
constendsWith = (x) => (xs) =>last(xs) == x
constpath = (ps) => (obj) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
constgetPaths = (obj) =>
typeof obj == 'object'
? Object .entries (obj)
.flatMap (([k, v]) => [
[k],
...getPaths (v) .map (p => [k, ...p])
])
: []
consthasSubseq = ([x, ...xs]) => ([y, ...ys]) =>
y == undefined
? x == undefined
: xs .length > ys .length
? false
: x == y
? hasSubseq (xs) (ys)
: hasSubseq ([x, ...xs]) (ys)
// helper functionsconstfindPartialMatches = (p, obj) =>
getPaths (obj)
.filter (endsWith (last (p)))
.filter (hasSubseq (p))
.flatMap (p => path (p) (obj))
constname2path = (name) => // probably not a full solutions, but ok for now
name .split (/[[\].]+/g) .filter (Boolean)
// main functionconstnewGetRowValue = (name, obj) =>
findPartialMatches (name2path (name), obj)
// sample datalet response = {id: "0", version: "0.1", interests: [{categories: ["baseball", "football"], refreshments: {drinks: ["beer", "soft drink"]}}, {categories: ["movies", "books"], refreshments: {drinks: ["coffee", "tea"]}}], goals: [{maxCalories: {drinks: "350", pizza: "700"}}]};
// demo
[
'interests[refreshments].drinks',
'interests[drinks]',
'drinks',
'interests[categories]',
'goals',
'id',
'goals.maxCalories',
'goals.drinks'
] .forEach (
name =>console.log(`"${name}" --> ${JSON.stringify(newGetRowValue(name, response))}`)
)
.as-console-wrapper {max-height: 100%!important; top: 0}
Original Answer
I still have some questions about your requirements. See my comment on the question for details. I'm making an assumption here that your requirements are slightly more consistent than suggested: mostly that the nodes in your name must be present, and the nesting structure must be as indicated, but that there might be intermediate nodes not mentioned. Thus "interests.drinks"
would include the values of both interests[0].drinks
and "interests[1].refreshments.drinks"
, but not of "goals.maxCategories.drinks"
, since that does not include any "interests"
node.
This answer also has a bit of a hack: the basic code would return an array for any input. But there are times when that array has only a single value, and usually we would want to return just that value. That is the point of the coarsen
function used in findPartialMatches
. It's an ugly hack, and if you can live with id
yielding ["0"]
in an array, I would remove the call to coarsen
.
Most of the work here uses arrays for the path rather than your name value. I find it much simpler, and simply convert to that format before doing anything substantial.
Here is an implementation of this idea:
// utility functionsconstlast = (xs) =>
xs [xs.length - 1]
constendsWith = (x) => (xs) =>last(xs) == x
constpath = (ps) => (obj) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
constgetPaths = (obj) =>
typeof obj == 'object'
? Object .entries (obj)
.flatMap (([k, v]) => [
[k],
...getPaths (v) .map (p => [k, ...p])
])
: []
consthasSubseq = ([x, ...xs]) => ([y, ...ys]) =>
y == undefined
? x == undefined
: xs .length > ys .length
? false
: x == y
? hasSubseq (xs) (ys)
: hasSubseq ([x, ...xs]) (ys)
// helper functionsconstcoarsen = (xs) =>
xs.length == 1 ? xs[0] : xs
constfindPartialMatches = (p, obj) =>
coarsen (getPaths (obj)
.filter (endsWith (last (p)))
.filter (hasSubseq (p))
.flatMap (p => path (p) (obj))
)
constname2path = (name) => // probably not a full solutions, but ok for now
name .split (/[[\].]+/g) .filter (Boolean)
// main functionconstnewGetRowValue = (name, obj) =>
findPartialMatches (name2path (name), obj)
// sample datalet response = {id: "0", version: "0.1", interests: [{categories: ["baseball", "football"], refreshments: {drinks: ["beer", "soft drink"]}}, {categories: ["movies", "books"], refreshments: {drinks: ["coffee", "tea"]}}], goals: [{maxCalories: {drinks: "350", pizza: "700"}}]};
// demo
[
'interests[refreshments].drinks',
'interests[drinks]',
'drinks',
'interests[categories]',
'goals',
'id',
'goals.maxCalories',
'goals.drinks'
] .forEach (
name =>console.log(`"${name}" --> ${JSON.stringify(newGetRowValue(name, response))}`)
)
.as-console-wrapper {max-height: 100%!important; top: 0}
We start with two simple utility functions:
last
returns the last element of an arrayendsWith
simply reports if the last element of an array equals a test value
Then a few more substantial utility functions:
path
takes an array of node names, and an object and finds the value of at that node path in an object.getPaths
takes an object and returns all the paths found in it. For instance, the sample object will yield something like this:[ ["id"], ["version"], ["interests"], ["interests", "0"], ["interests", "0", "categories"], ["interests", "0", "categories", "0"], ["interests", "0", "categories", "1"], ["interests", "0", "drinks"], // ... ["goals"], ["goals", "0"], ["goals", "0", "maxCalories"], ["goals", "0", "maxCalories", "drinks"], ["goals", "0", "maxCalories", "pizza"] ]
hasSubseq
reports whether the elements of the first argument can be found in order within the second one. ThushasSubseq ([1, 3]) ([1, 2, 3, 4)
returnstrue
, buthasSubseq ([3, 1]) ([1, 2, 3, 4)
returnsfalse
. (Note that this implementation was thrown together without a great deal of thought. It might not work properly, or it might be less efficient than necessary.)
After that we have three helper functions. (I distinguish utility functions from helper functions this way: utility functions may be useful in many places in the project and even across projects. Helper functions are specific to the problem at hand.):
coarsen
was discussed above and it simply turns single-element arrays into scalar values. There's a good argument for removing this altogether.findPartialMatches
is central. It does what our main function is designed to do, but using an array of node names rather than a dot/bracket-separated string.name2path
converts the dot/bracket-separated string into an array. I would move this up to the utility section, except that I'm afraid that it may not be as robust as we would like.
And finally, the main function simply calls findPartialMatches
using the result of name2path
on the name
parameter.
The interesting code is findPartialMatches
, which gets all the paths in the object, and then filters the list to those that end with the last node of our path, then further filters these to the ones that have our path as a subsequence, retrieves the values at each of these paths, wraps them in an array, and then calls the unfortunate coarsen
on this result.
Post a Comment for "How Can I Retrieve Dynamically Specified, Arbitrary And Deeply Nested Values From A Javascript Object Containing Strings, Objects, And Arrays?"