I have an object, of which parameters contain and array of object. I receive 1 object id and I need to find its position in that whole mess. With procedural programming I got it working with:
const opportunitiesById = {
1: [
{ id: 1, name: 'offer 1' },
{ id: 2, name: 'offer 1' }
],
2: [
{ id: 3, name: 'offer 1' },
{ id: 4, name: 'offer 1' }
],
3: [
{ id: 5, name: 'offer 1' },
{ id: 6, name: 'offer 1' }
]
};
const findObjectIdByOfferId = (offerId) => {
let opportunityId;
let offerPosition;
const opportunities = Object.keys(opportunitiesById);
opportunities.forEach(opportunity => {
const offers = opportunitiesById[opportunity];
offers.forEach((offer, index) => {
if (offer.id === offerId) {
opportunityId = Number(opportunity);
offerPosition = index;
}
})
});
return { offerPosition, opportunityId };
}
console.log(findObjectIdByOfferId(6)); // returns { offerPosition: 1, opportunityId: 3 }
However this is not pretty and I want to do that in a functional way. I've looked into Ramda, and I can find an offer when I'm looking into a single array of offers, but I can't find a way to look through the entire object => each array to find the path to my offer.
R.findIndex(R.propEq('id', offerId))(opportunitiesById[1]);
The reason I need to know the path is because I then need tho modify that offer with new data and update it back where it is.
Thanks for any help
I would transform your object into pairs.
So for example transforming this:
into that:
Then you can iterate over that array and reduce each array of offers to the index of the offer you're looking for. Say you're looking for offer #21, the above array would become:
Then you return the first tuple which second element isn't equal to
-1
:Here's how I'd suggest doing this:
Then you can take that path to modify your offer as you see fit:
You could piece it together using lots of little functions but I want to show you how to encode your intentions in a more straightforward way. This program has an added benefit that it will return immediately. Ie, it will not continue to search thru additional key/value pairs after a match is found.
Here's a way you can do it using mutual recursion. First we write
findPath
-If the input is an object, we pass it to the user's search function
f
. If the user's search function returnstrue
, a match has been found and we return thepath
. If there is not match, we search each key/value pair of the object using a helper function. Otherwise, if the input is not an object, there is no match and nothing left to search, so returnundefined
. We write the helper,findPath1
-If the key/value pairs have been exhausted, there is nothing left to search so return
undefined
. Otherwise we have a keyk
and a valuev
; appendk
to the path and recursively searchv
for a match. If there is not a match, recursively search the remaining key/values,more
, using the samepath
.Note the simplicity of each function. There's nothing happening except for the absolute minimum number of steps to assemble a
path
to the matched object. You can use it like this -The path returned leads us to the object we wanted to find -
We can specialize
findPath
to make an intuitivefindByOfferId
function -Like
Array.prototype.find
, it returnsundefined
if a match is never found -Expand the snippet below to verify the results in your own browser -
In this related Q&A, I demonstrate a recursive search function that returns the matched object, rather than the path to the match. There's other useful tidbits to go along with it so I'll recommend you to give it a look.
Scott's answer inspired me to attempt an implementation using generators. We start with
findPathGen
-And using mutual recursion like we did last time, we call on helper
findPathGen1
-Finally, we can implement
findPath
and the specializationfindByOfferId
-It works the same -
And as a bonus, we can implement
findAllPaths
easily usingArray.from
-Verify the results by expanding the snippet below
Here's another approach:
We start with this generator function:
which can be used to find all the paths in an object:
and then, with this little helper function:
we can write
which we can call like
We can then use these functions to write a simple version of your function:
It is trivial to extend this to get all paths for which the value satisfies the predicate, simply replacing
find
withfilter
:There is a concern with all this, though. Even though
findPath
only needs to find the first match, and even thoughgetPaths
is a generator and hence lazy, we force the full run of it with[...getPaths(o)]
. So it might be worth using this uglier, more imperative version:This is what it looks like all together:
Another brief note: the order in which the paths are generated is only one possibility. If you want to change from pre-order to post-order, you can move the
yield p
line ingetPaths
from the first line to the last one.Finally, you asked about doing this with functional techniques, and mentioned Ramda. As the solution from customcommander shows, you can do this with Ramda. And the (excellent as always) answer from user633183 demonstrates, it's possible to do this with mainly functional techniques.
I still find this a somewhat simpler approach. Kudos to customcommander for finding a Ramda version, because Ramda is not particularly well-suited for recursive tasks, but still the obvious approach to something that has to visit the nodes of a recursive structure like a JS object is to use a recursive algorithm. I'm one of the authors of Ramda, and I haven't even tried to understand how that solution works.
Update
user633183 pointed out that this would be simpler, and still lazy: