recursive search
This particular subject was recently discussed in a recent post. Here is an implementation of a general-purpose function called deepFind
, which performs recursive searches on any input value.
Below, we present a basic dataset called data
and showcase how the deepFind
function can be used to search for specific matches within the data.
const data =
[ { a: 1, b: 1 }
, { a: 2, b: 2, c: { d: [ { e: 2 } ] } }
, { a: 3, b: { c: { d: { e: { f: 3 } } } } }
]
const deepFind = (f, obj = {}) =>
{ if (Object (obj) === obj)
{ if (f (obj) === true)
return obj
for (const [ k, v ] of Object.entries (obj))
{ const res =
deepFind (f, v)
if (res !== undefined)
return res
}
}
return undefined
}
console.log
( deepFind (x => x.a === 1, data) // { a: 1, b: 1 }
, deepFind (x => x.e === 2, data) // { e: 2 }
, deepFind (x => Array.isArray(x.d), data) // { d: [ { e: 2 } ] }
, deepFind (x => x.f === 3, data) // { f: 3 }
, deepFind (x => x.e && x.e.f === 3, data) // { e: { f: 3 } }
, deepFind (x => x.z === 9, data) // undefined
)
The functionality of deepFind
is limited to direct matches using ===
. By utilizing a higher-order function f
, however, we can customize its behavior in more meaningful ways.
string match with deepFind
Here, we demonstrate a specialized string-matching function named search
that leverages the capabilities of deepFind
:
const search = (query = "", data) =>
deepFind
( o =>
Object.values (o) .some (v =>
String (v) === v && v .includes (query))
, data
)
search ("D", result)
// { name: "Donna Shomaker", ... }
search ("Du", result)
// { name: "Ron Duluth", ... }
search ("ng3", result)
// { name: "Jimmy Dawson", sneak: "string3 string3 string3", ... }
search ("zzz", result)
// undefined
Feel free to test the results in your own browser environment
const deepFind = (f, obj = {}) =>
{ if (Object (obj) === obj)
{ if (f (obj) === true)
return obj
for (const [ k, v ] of Object.entries (obj))
{ const res =
deepFind (f, v)
if (res !== undefined)
return res
}
}
return undefined
}
const search = (query = "", data) =>
deepFind
( o =>
Object.values (o) .some (v =>
String (v) === v && v .includes (query))
, data
)
const result =
[ { name: 'Donna Shomaker'
, custNumber: '6658924351'
, sneak: 'string1 string1 string1'
, foo: false
, bar: false
}
, { name: 'Ron Duluth'
, custNumber: '8812654434'
, sneak: 'string2 string2 string2'
, foo: false
, bar: false
}
, { name: 'Jimmy Dawson'
, custNumber: '8908198230'
, sneak: 'string3 string3 string3'
, foo: false
, bar: false
}
]
console.log (search ("D", result))
// { name: "Donna Shomaker", ... }
console.log (search ("Du", result))
// { name: "Ron Duluth", ... }
console.log (search ("ng3", result))
// { name: "Jimmy Dawson", sneak: "string3 string3 string3", ... }
console.log (search ("zzz", result))
// undefined
fetching multiple search results
The previous program only outputs the first matching result. If you need all matches, using generators would be a suitable approach. Let's see how it can be done by introducing a generator function.
const deepFindAll = function* (f, o = {})
{ if (Object (o) === o)
{ if (f (o) === true)
yield o
for (const [ _, v ] of Object.entries (o))
yield* deepFindAll (f, v)
}
}
Next, we create searchAll
based on our newly defined generator function:
const searchAll = (query = "", data = {}) =>
Array.from
( deepFindAll
( o =>
Object.values (o) .some (v =>
String (v) === v && v .includes (query))
, data
)
)
searchAll ("81", result)
// [ { custNumber: '8812654434', ... }
// , { custNumber: '8908198230', ... }
// ]
searchAll ("Du", result)
// [ { name: "Ron Duluth", ... } ]
searchAll ("zzz", result)
// []
Execute searchAll
in your browser to observe the outcomes
const deepFindAll = function* (f, o = {})
{ if (Object (o) === o)
{ if (f (o) === true)
yield o
for (const [ _, v ] of Object.entries (o))
yield* deepFindAll (f, v)
}
}
const searchAll = (query = "", data = {}) =>
Array.from
( deepFindAll
...
...
]
console.log (searchAll ("81", result))
// [ { custNumber: '8812654434', ... }
// , { custNumber: '8908198230', ... }
// ]
console.log (searchAll ("Du", result))
// [ { name: "Ron Duluth", ... } ]
console.log (searchAll ("zzz", result))
// []
case insensitive search
In the aforementioned search
function, matching is performed with v .includes (query)
. However, exploiting a higher-order function allows us to specify behavior as needed.
An alternative method involving searchAll
could look like this:
const searchAll = (query = "", data = {}) =>
Array.from
( deepFindAll
( o =>
Object.values (o) .some (v =>
String (v) === v
<b>&& v .toLowerCase () .includes (query .toLowerCase ())</b>
, data
)
)
This modification may seem convoluted. It's crucial to abstract and clearly communicate our intentions by defining separate functions for each operation.
const anyString = f => o =>
Object.values (o) .some (v =>
String (v) === v && f (v))
const caseInsenstiveMatch = (x, y) =>
x.toLowerCase () .includes (y.toLowerCase ())
const searchAll = (query = "", data = {}) =>
Array.from
( deepFindAll
( <b>anyString (v =>
caseInsenstiveMatch (v, query))</b>
, data
)
)
Separating behaviors and creating distinct functions are fundamental aspects of developing quality software. Comparing search
and searchAll
side by side underscores the importance of clarity and modularity. The introduction of helper functions like anyString
and caseInsensitiveSearch
not only enhances code readability but also promotes reusability where necessary.
const search = (query = "", data) =>
deepFind
( anyString (v =>
caseInsenstiveMatch (v, query))
, data
)
const searchAll = (query = "", data = {}) =>
Array.from
( deepFindAll
( anyString (v =>
caseInsenstiveMatch (v, query))
, data
)
)
contramap
Utilizing higher-order functions offers numerous benefits in terms of code organization and clarity. Below, we define straightforward match
and lower
functions. Through the use of contramap
, we unite these functions seamlessly.
The key emphasis here lies in keeping each function simple and focused. A concise function is easier to validate, troubleshoot, and integrate with other similar functions. The Unix philosophy of "Do one thing and do it well" should resonate strongly at this point.
const contramap = (f, g) =>
(x, y) => f (g (x), g (y))
<b>const match = (x = "", y = "") =>
x .includes (y)
const lower = (x = "") =>
x .toLowerCase ()
const caseInsenstiveMatch =
contramap (match, lower)</b>
const anyString = (f) => (o = {}) =>
Object.values (o) .some (v =>
String (v) === v && f (v))
const searchAll = (query = "", data = {}) =>
Array.from
( deepFindAll
( anyString (v =>
caseInsenstiveMatch (v, query))
, data
)
)
Contramap unlocks additional potential that might not be immediately apparent. For those intrigued by this concept, I recommend delving into Monoidal Contravariant Functors are actually useful! by Brian Lonsdorf. Despite its title, the author excels at simplifying seemingly complex concepts.