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
-
const identity = x =>
x
const findPath =
( f = identity
, o = {}
, path = []
) =>
Object (o) === o
? f (o) === true
? path
: findPath1 (f, Object .entries (o), path)
: undefined
If the input is an object, we pass it to the user's search function f
. If the user's search function returns true
, a match has been found and we return the path
. 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 return undefined
. We write the helper, findPath1
-
const None =
Symbol ()
const findPath1 =
( f = identity
, [ [ k, v ] = [ None, None ], ...more ]
, path = []
) =>
k === None
? undefined
: findPath (f, v, [ ...path, k ])
|| findPath1 (f, more, path)
If the key/value pairs have been exhausted, there is nothing left to search so return undefined
. Otherwise we have a key k
and a value v
; append k
to the path and recursively search v
for a match. If there is not a match, recursively search the remaining key/values, more
, using the same path
.
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 -
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' }
]
}
findPath (offer => offer.id === 6, opportunitiesById)
// [ '3', '1' ]
The path returned leads us to the object we wanted to find -
opportunitiesById['3']['1']
// { id: 6, name: 'offer 1' }
We can specialize findPath
to make an intuitive findByOfferId
function -
const findByOfferId = (q = 0, data = {}) =>
findPath (o => o.id === q, data)
findByOfferId (3, opportunitiesById)
// [ '2', '0' ]
opportunitiesById['2']['0']
// { id: 3, name: 'offer 1' }
Like Array.prototype.find
, it returns undefined
if a match is never found -
findByOfferId (99, opportunitiesById)
// undefined
Expand the snippet below to verify the results in your own browser -
const identity = x =>
x
const None =
Symbol ()
const findPath1 =
( f = identity
, [ [ k, v ] = [ None, None ], ...more ]
, path = []
) =>
k === None
? undefined
: findPath (f, v, [ ...path, k ])
|| findPath1 (f, more, path)
const findPath =
( f = identity
, o = {}
, path = []
) =>
Object (o) === o
? f (o) === true
? path
: findPath1 (f, Object .entries (o), path)
: undefined
const findByOfferId = (q = 0, data = {}) =>
findPath (o => o.id === q, data)
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' }
]
}
console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]
console .log (opportunitiesById['2']['0'])
// { id: 3, name: 'offer 1' }
console .log (findByOfferId (99, opportunitiesById))
// undefined
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
-
const identity = x =>
x
const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
if (f (o) === true)
yield path
else
yield* findPathGen1 (f, Object .entries (o), path)
}
And using mutual recursion like we did last time, we call on helper findPathGen1
-
const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
yield* findPathGen (f, v, [ ...path, k ])
}
Finally, we can implement findPath
and the specialization findByOfferId
-
const first = ([ a ] = []) =>
a
const findPath = (f = identity, o = {}) =>
first (findPathGen (f, o))
const findByOfferId = (q = 0, data = {}) =>
findPath (o => o.id === q, data)
It works the same -
findPath (offer => offer.id === 3, opportunitiesById)
// [ '2', '0' ]
findPath (offer => offer.id === 99, opportunitiesById)
// undefined
findByOfferId (3, opportunitiesById)
// [ '2', '0' ]
findByOfferId (99, opportunitiesById)
// undefined
And as a bonus, we can implement findAllPaths
easily using Array.from
-
const findAllPaths = (f = identity, o = {}) =>
Array .from (findPathGen (f, o))
findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById)
// [ [ '2', '0' ], [ '3', '1' ] ]
Verify the results by expanding the snippet below
const identity = x =>
x
const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
if (f (o) === true)
yield path
else
yield* findPathGen1 (f, Object .entries (o), path)
}
const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
yield* findPathGen (f, v, [ ...path, k ])
}
const first = ([ a ] = []) =>
a
const findPath = (f = identity, o = {}) =>
first (findPathGen (f, o))
const findByOfferId = (q = 0, data = {}) =>
findPath (o => o.id === q, data)
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' }
]
}
console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]
console .log (findByOfferId (99, opportunitiesById))
// undefined
// --------------------------------------------------
const findAllPaths = (f = identity, o = {}) =>
Array .from (findPathGen (f, o))
console .log (findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById))
// [ [ '2', '0' ], [ '3', '1' ] ]