Here is an approach similar to the one from KarelG. It is different enough, though, to warrant its own answer.
Differences
This one passes the ancestors as a parameter rather than keeping them as a free variable in the function. Free variables make code harder to understand and much harder to test.
It separates out the recursive function from the public function, keeping them together in a closure. This is a less common technique now that people are using some sort of module in many places, but it can still be useful, especially on the web. That is the (() => {/* ... return some function */ })()
syntax.
It uses a Set as the recursive accumulator, to avoid duplicates.
It ignores the gender. I can't see a reason for checking it. If the child says that the mother is so-and-so, I don't see any point to double-checking that `mother.sex === 'f'``. I could be missing something here.
While it keeps the existing API of supplying a full person object and returning a list of names, it would be relatively easy to switch it to supplying just the names or to return a list of people. More on that below.
I named it allDescendents
as that seemed more accurate than allChildren
.
Code
const allDescendents = (() => {
const all = (family, name, children = new Set()) => {
const kids = family.filter(p => p.father === name || p.mother === name).map(k => k.name)
kids.forEach(kid => (children.add(kid), all(family, kid, children)))
return children
}
return (family, person) => Array.from(all(family, person.name))
})()
const bourbons = [ // https://en.wikipedia.org/wiki/Bourbon_family_tree
{name: 'Philip III', sex: 'm', father: 'Louis IX', mother: 'Margaret'},
{name: 'Robert', sex: 'm', father: 'Louis IX', mother: 'Margaret'},
{name: 'Charles', sex: 'm', father: 'Philip III', mother: '?'},
{name: 'Louis I', sex: 'm', father: 'Robert', mother: 'Beatrice'},
{name: 'Philip IV', sex: 'm', father: 'Charles', mother: '?'},
{name: 'Isabella', sex: 'f', father: 'Charles', mother: '?'},
{name: 'Peter I', sex: 'm', father: 'Louis I', mother: 'Mary'},
{name: 'James I', sex: 'm', father: 'Louis I', mother: 'Mary'},
{name: 'John II', sex: 'm', father: 'Philip IV', mother: '?'},
{name: 'Peter II', sex: 'm', father: 'James I', mother: 'Jeanne'},
{name: 'John I', sex: 'm', father: 'James I', mother: 'Jeanne'},
{name: 'Charles V', sex: 'm', father: 'John II', mother: '?'},
{name: 'Joanna', sex: 'f', father: 'Peter I', mother: 'Isabella'},
{name: 'Louis II', sex: 'f', father: 'Peter I', mother: 'Isabella'},
{name: 'James II', sex: 'm', father: 'John I', mother: 'Catherine'},
{name: 'Louis', sex: 'm', father: 'John I', mother: 'Catherine'},
/* .. */
]
const louis1st = bourbons[3]
console.log(allDescendents(bourbons, louis1st))
Variants
This is factored in such a way as to make serious change fairly easy. Most of these changes can also be combined.
Return a Set
It might make more sense to return a Set
of values rather than an Array
. Presumably you do not want this collection to include duplicates. A Set
is the right data structure for that. This solution uses a Set
internally, and it's trivial to make that the public interface. Just remove the call to Array.from
:
- return (family, person) => Array.from(all(family, person.name))
+ return (family, person) => all(family, person.name)
Supply a Name
Since you're returning a collection of names, perhaps it would be easier to supply a name for the input. We can do this by altering the input parameter to receive the name:
- return (family, person) => Array.from(all(family, person.name))
+ return (family, name) => Array.from(all(family, name))
Return People
Conversely, you might prefer to return the people objects directly. A list of names is only so helpful. A list containing the full Person object might be easier to work with. This is what that would look like:
- const all = (family, name, children = new Set()) => {
- const kids = family.filter(p => p.father === name || p.mother === name).map(k => k.name)
+ const all = (family, person, children = new Set()) => {
+ const kids = family.filter(p => p.father === person.name || p.mother === person.name)
- return (family, person) => Array.from(all(family, person.name))
+ return (family, person) => Array.from(all(family, person))
Avoiding Default Parameters
If the default parameters in the function bother you (red squiggles in your editor, for instance), then you can move the parameters to the exported function. I like to keep the complexity in one place, which is why I have the defaults on the recursive function, but there is a good argument for simplifying the recursive function at the expense of the public function; it does make the recursive function somewhat easier to understand.
- const all = (family, name, children = new Set()) => {
+ const all = (family, name, children) => {
- return (family, person) => Array.from(all(family, person.name))
+ return (family, person) => Array.from(all(family, person.name, new Set()))
Making a module
Finally, we can easily turn this into a module. If you are deploying to the browser, this will still require a build step, but it does simplify the code: Here is one version:
const all = (family, name, children = new Set()) => {
const kids = family.filter(p => p.father === name || p.mother === name).map(k => k.name)
kids.forEach(kid => (children.add(kid), all(family, kid, children)))
return children
}
const allDescendents = (family, person) => Array.from(all(family, person.name))
export {allDescendents}