Power Query: chain of command
At work, I needed to traverse a table and get a "chain of command" - that is, automatically generate a list of someone's manager, and their manager, and their manager, until it reaches the top of the organisation (the Director General).
I knew that Power Query had the capacity for recursive functions, and this was a great opportunity to try it out.
The task that I actually needed to do was more complex than this, because there were side lookups that I needed to do, but the core engine of it is illustrated here.
In a galaxy, far, far away...
I love Star Wars, and I can think of no better fictional world to illustrate this with. Instead of subordinates and bosses, I'll use Jedi students and masters. Our recursive function will trace back student-master lineages.
| Student | Master |
|---|---|
| Anakin Skywalker | Obi-Wan Kenobi |
| Obi-Wan Kenobi | Qui-Gon Jinn |
| Luke Skywalker | Obi-Wan Kenobi |
| Ben Solo | Luke Skywalker |
| Rey | Luke Skywalker |
For convenience, here is the table in Power Query for you to copy and paste:
Code
#table(
{"Student", "Master"},
{
{"Anakin Skywalker", "Obi-Wan Kenobi"},
{"Obi-Wan Kenobi", "Qui-Gon Jinn "},
{"Luke Skywalker", "Obi-Wan Kenobi"},
{"Ben Solo", "Luke Skywalker "},
{"Rey", "Luke Skywalker"}
}
)
Results
We'll call our function getLineage - the code is presented below this.
If we call getLineage("Ben Solo"), we will expect to get:
| List |
|---|
| Ben Solo |
| Luke Skywalker |
| Obi-Wan Kenobi |
| Qui-Gon Jinn |
If we call getLineage("Anakin Skywalker"), we will expect to get:
| List |
|---|
| Anakin Skywalker |
| Obi-Wan Kenobi |
| Qui-Gon Jinn |
Functions
lookup
We first need to set up a lookup function, which will fetch just one value when given one input.
(targetName as text) as text =>
try
Table.SelectRows(
source,
each [Student] = targetName
){0}[Master]
otherwise
"Not found"
getLineage
This is the main core of the engine. It doesn't look like much; perhaps that because Power Query does all of the hard stuff for you, but it can be tricky to mentally reason about List.Generate, so it was difficult for me to compose at first.
Here is the official documentation on this function, for reference.
The "recursive" part here is each lookup(_), which will use _ as the most recent value in the generated list as the input for the next name to lookup() on.
(name as text) as list =>
let
chain = List.Generate(
() => lookup(name), // starting value
each _ <> "Not found", // stopping condition
each lookup(_) // at each iteration
),
final = List.Combine({
{name}, // first name in the chain
chain // the rest of the chain
})
in
final