I've got many<>many table which stores Bill Of Material hierarchy data like:
id | childId
a | b
a | c
c | d
d | a
I need to find closed loops, like in this case d
points back to a
, which breaks the system since some recursive functions would never stop.
My idea was to transform this data structure from flat to hierarchical, then iterate over hierarchical structure using recursion.
I am looking up some knowledge on Web and most of the solutions of transforming flat to nested structure, cover the case where there is:
id | parentId
structure, where top level items has parentId
set to root value (e.g. zero).
I am a self-taught systems developer for many years, but this is a moment where I am lacking the education many of you had during studies :)
EDIT: Graphical examples
Valid trees:
a)
A
/ \
B C
\
D
/ \
E F
b)
K -> E
/ \
G B
/|\
H I J
Invalid tree (closed loop)
A
/ \
B C<----\
\ |
D /
/ \ /
E F
\
G
/|\
H I J
After bit of a trial and error, here is what I've came up with.
Given the flat list of relations, it builds the lookup, which is then used in a recursive traversal function. Traversal function "remembers" the original part that was requested ($initialPart
) and throws an Exception when cycle is detected. E.g. exception: Loop detected: f=>c=>d=>f!
Script:
$rows = [
['assemblyId' => 'a', 'partId' => 'b'],
['assemblyId' => 'a', 'partId' => 'c'],
['assemblyId' => 'c', 'partId' => 'd'],
['assemblyId' => 'd', 'partId' => 'e'],
['assemblyId' => 'd', 'partId' => 'f'],
['assemblyId' => 'f', 'partId' => 'c'],
['assemblyId' => 'f', 'partId' => 'g'],
['assemblyId' => 'g', 'partId' => 'h'],
['assemblyId' => 'g', 'partId' => 'i'],
['assemblyId' => 'g', 'partId' => 'j'],
];
$assemblyLookup = [];
foreach ($rows as $row) {
$assemblyLookup[$row['assemblyId']][$row['partId']] = $row;
}
function getTree(&$part, $initialPart = null, $path = '', $level = 0, $maxLevel = 100) {
global $assemblyLookup;
if ($initialPart === null) {
$initialPart = $part;
}
if ($level >= $maxLevel) {
return;
}
$path .= '=>' . $part['partId'];
if (is_array($assemblyLookup[$part['partId']])) {
$lookup = $assemblyLookup[$part['partId']];
foreach ($lookup as $child) {
if ($child['partId'] === $initialPart['partId']) {
$circularPath = substr($path, 2) . '=>' . $child['partId'] . '!';
throw new Exception("Loop detected: " . $circularPath);
return;
}
$part['children'][$child['partId']] = $child;
getTree($part['children'][$child['partId']], $initialPart, $path, ++$level, $maxLevel);
}
} else {
$path .= ';';
$part['path'] = substr($path, 2); // Strip arrow from left side
}
return $part;
}
$part = ['partId' => 'f'];
$tree = getTree($part);