I am looping over an array of groups, each element containing a parent id.
$currentparent = $group['grpId']; //$group is the current element in a loop wrapped around this piece of code
$currentlevel = 0;
foreach($groups as $grp)
{
$parent = $grp['grpParentId'];
if($parent != $currentparent && $currentlevel != 6)
{
//adding layer
$currentlevel++;
//changing parent
$currentparent = $grp['grpParentId'];
}
if($currentlevel == 6)
{
//call a special function
}
else
{
//call the regular function
}
}
This works fine on a array like this:
group
-group
--group
---group
----group
----- group <- the only group on the 5th layer
but not with an array which has multiple groups on the 5th level:
group
-group
--group
--group
---group
----group
-----group <- 5th layer
----group
-----group <- 5th layer too, but diff parent
How can I resolve this to get the special function called, even when there are multiple groups with multiple parents on the fifth level in the array?
I hope that I formulated my question clear enough.
Try iterating trough the the groups always searching for groups whose level can be determinated by its parent level. Something like this:
// example input
$groups = array(
array('grpParentId' => 0, 'grpId' => 1, ),
array('grpParentId' => 1, 'grpId' => 2, ),
array('grpParentId' => 2, 'grpId' => 3, ),
array('grpParentId' => 3, 'grpId' => 4, ),
array('grpParentId' => 4, 'grpId' => 5, ),
array('grpParentId' => 5, 'grpId' => 6, ),
array('grpParentId' => 6, 'grpId' => 7, ),
array('grpParentId' => 5, 'grpId' => 8, ),
array('grpParentId' => 8, 'grpId' => 9, ),
);
shuffle($groups); // just for testing the logic does in fact tolerate randomly ordered input
$rootId = 1; // set to the rootnode's id
$grouplevels = array();
// find the rootnode first
foreach($groups as $i => $grp) {
if ($rootId == $grp['grpId']) {
$grouplevels[$rootId] = 1;
unset($groups[$i]);
break;
}
}
// figure out childgroup levels
do {
$old_count = count($groups);
foreach($groups as $i => $grp) {
if (in_array($grp['grpParentId'], array_keys($grouplevels))) {
// the current node's parent's level was determinated previously, we can tell this group's level as well
$grouplevels[$grp['grpId']] = $level = $grouplevels[$grp['grpParentId']]+1;
if ($level == 6) {
print $grp['grpId']."\n";
}
// remove from the "we dont know yet" list
unset($groups[$i]);
}
}
} while (count($groups) < $old_count); // run while we can sort out at least one group's level in the current iteration
// handle the case when not every group's level could been determinated!
if (!empty($groups)) {
print "inconsitency ahead!";
}
It can be solved by using a recursive pattern. The following code should do the trick. It can probably be optimized.
$currentparent = $group['grpId']; //$group is the current element in a loop wrapped around
$this piece of code
$currentlevel = 0;
// Initialize
foreach($groups as $grp)
{
$grp['children'] = array();
}
foreach($groups as $grp)
{
$parent = $grp['grpParentId'];
$parent['children'][] = $grp;
}
foreach($groups as $grp)
{
if(empty($parent['children'])) $root = $grp; // Alternatively check if parent is null or something.
}
function recursive_count($root, $lvl = 0)
{
if($currentlevel == 6)
{
//call a special function
}
else
{
//call the regular function
}
foreach($root['children'] as $children)
{
recursive_count($children, $lvl++);
}
}
recursive_count($root);
Update: If memory consumption is a problem references can be used when adding a group to the children array.
Update II: Even though the algorithm has 4 foreach and a recursive structure the running time is still O(n) where n is the size of the graph.
Related
Need suggestion on the best way to do a traversal on my hierarchy array (at this point I think it's a tree)
A snippet of my array is this:
$rows = array(
array(
'name' => "Main",
'id' => 1,
'parent_id' => 0,
'_children' => array(
array(
'name' => "Two",
'id' => 2,
'parent_id' => 1),
),
array(
'name' => "Three",
'id' => 3,
'parent_id' => 1,
'_children' => array(
array(
'name' => "Four",
'id' => 4,
'parent_id' => 3),
)),
)
)
);
So on that snippet, a quick explanation is that 'Main' node is root and it has 2 children "Two" and "Three" then "Three" has a child namely "Four".
The actual data is based on department and sub-departments so the nodes goes up to 5 layers.
The _children field for my layering is because I use Tabulator and that's the required hierarchy on what I want to achieve.
I was able to achieve using recursion the department hierarchy, now I need to traverse each department so I can add employees for each department on the same field "_children".
The reason I wasn't able to achieve adding the employees from the start, it's because when I do recursion it overwrites the employee on _children with the departments.
Any suggestion on how I should tackle the traversal?
Edit -
Here is my method that I used for hierarchy:
private function buildHierarchyDepartment(array $elements, $parentId = 0) {
$branch = array();
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = static::buildHierarchyDepartment($elements, $element['id']);
if ($children) {
$element['_children'] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
I'm not too sure how you want to add employees to the array so I've made some assumptions here.
This code will traverse through all elements of an array recursively until it finds an element that matches the parent ID. At this point, it will add the specified item to the _children property of that "parent".
NOTE: this can be simplified if you preferred passing the array by reference. For this example I've set it up so that it doesn't edit the original array (unless of course you overwrite the variable).
function addChild(array $main, array $item, $parent_id) {
foreach ($main as $key => $element) {
if ($parent_id === $element["id"]) {
// create _children element if not exist
if (!isset($element["_children"])) {
$element["_children"] = [];
}
$element["_children"][] = $item;
// specify $main[$key] here so that the changes stick
// outside this foreach loop
$main[$key] = $element;
// item added - break the loop
break;
}
// continue to check others if they have children
if (isset($element["_children"])) {
$element["_children"] = addChild($element["_children"], $item, $parent_id);
// specify $main[$key] here so that the changes stick
// outside this foreach loop
$main[$key] = $element;
}
}
return $main;
}
$employee = [
"id" => 99,
"name" => "Test Employee",
"parent_id" => 4,
];
$new_rows = addChild($rows, $employee, $employee["parent_id"]);
NOTE: this uses a strict comparison for $parent_id === $element["id"] meaning an int won't match a string. You can either convert these values into the same format or change to a non-strict compare ==.
I am working in a scoring system in php. When user chooses specific question, get extra points.
If anyone has a working PHP demo with code of a scoring system, he would be welcome. I mostly need a working "for" function:
"for question 1, if answer is abc, 5 points, else 0 points. for question 2, if aswer is bcd, 10 points, if answer is cft, 10 points, else 0 points", etc.
I would not want to have to fill my code with endless if statements.
For my current attempt, The error it displays:
Fatal error: Uncaught Error: Call to undefined function value() in C:\xampp\htdocs\score.php:18 Stack trace: #0 {main} thrown in C:\xampp\htdocs\score.php on line 18
Edit: I realized i might not have defined the "value" variable. It should give the value of "1 point" for now. How should i define it?
<?php
// Define questions and the correct answers
$questions = array(
'AB01' => 3, // correct answer to question AB01 is 3
'AB02' => 1, // correct answer to AB02 is code 1
'AB03' => 4, // correct answer to AB03 is hidden behind code 4
'AB04' => 2,
'AB05' => 1
// and so on
);
// Initialize counter variable
$points = 0;
// Check all questions in a loop
foreach ($questions as $variable=>$correct) {
// Call up participant's answer
$answer = value($variable);
// Check and count point if applicable
if ($answer == $correct) {
$points++; // synonymous with $points = $points + 1
}
}
// Show result ...
html('<p>You have scored'.$points.' points.</p>');
// ... or store in an internal variable
put('IV01_01', $points);
// ...
// Check all questions in a loop
foreach ($questions as $variable=>$correct) {
if (value($variable) == $correct) {
$points++;
}
}
// Show result or otherwise process
// ...
// Define questions and the values of possible answers
$questions = array(
'AB01' => array(1 => 2, 2 => 5, 3 => 3), // In question AB01, answer 1 has value 2, 2 has value 5, 3 has value 3
'AB02' => array(1 => 5, 2 => 4, 3 => 1), // In question AB02, values 5 (answer 1), 4 (2) und 1 (3) are assigned
'AB03' => array(1 => 0, 2 => 0, 3 => 5),
'AB04' => array(1 => 4, 2 => 0, 3 => 3),
'AB05' => array(1 => 2, 2 => 2, 3 => 5),
// u.s.w.
);
// Initialize counter variable
$points = 0;
// By using foreach you can just pass through the key-value pairs
foreach ($questions as $variable => $values) {
// Call up participant's answer
$answer = value($variable);
// Check if there is a value available for this answer (otherwise, do not award a point)
if (isset($values[$answer])) {
// Count the value
$points += $values[$answer];
}
}
// Show result or otherwise process
html('<p>You have scored'.$points.' points.</p>');
$points = valueSum('AB01');
// Show result or otherwise process
html('<p>You have scored '.$points.' points.</p>');
// Call up list of all items
$items = getItems('AB01');
// Initialize points variable
$points = 0;
// Pass through all items
foreach ($items as $item) {
// Question ID still has to be assembled
$id = 'AB01_'.sprintf('%02d', $item);
// Call up participant's answer
$answer = value($id);
// An error code may have been saved (e.g. -1 for "no response")
// Otherwise, count the answer
if ($answer > 0) {
$points += $answer;
}
}
// Show result or otherwise process
html('<p>You have scored '.$points.' points.</p>');
$points += $answer - 1;
// List of items - providing polarity in each case
$items = array(
'01' => +1,
'02' => -1,
'03' => +1,
'04' => +1,
'05' => -1
// and so on.
);
// Initialization of points variable
$points = 0;
// Pass through all items
foreach ($items as $item => $polarity) {
// Here the variable ID is built from the question and item IDs
$id = 'AB01_'.$item;
// Call up respondent's answer
$answer = value($id);
// Ignore if it is not a valid response
if ($answer < 1) {
// This means the rest are ignored in the FOR/FOREACH loop
continue;
}
// "Invert" answers
if ($polarity < 0) {
// In a 5-point scale, the inverted answer code has a value of 6
// the constant has to be adjusted for other differentations.
$answer = 6 - $answer;
}
// Add up
$points += $answer;
}
// Show result or otherwise process.
html('<p>You have scored'.$points.' points.</p>');
if (
(value('AB01_01') == 2) and
(value('AB01_02') == 2) and
(value('AB01_03') == 1) and
(value('AB01_04') == 1)
) {
// Count point, jump to a different part, or just display a message
html('<p>Correct</p>');
} else {
html('<p>Incorrect</p>');
}
// Define questions and their correct answers
// Only items are defined that will also be checked
$questions = array(
// In question AB01, 1 and 2 have to be checked, 3 and 4 must not be checked
'AB01' => array(1 => 2, 2 => 2, 3 => 1, 4 => 1),
// In question AB02, 2 and 3 have to be checked, 4 must not, and the value for 1 is irrelevant
'AB02' => array( 2 => 2, 3 => 2, 4 => 1),
// In AB03, all 4 have to be checked
'AB03' => array(1 => 2, 2 => 2, 3 => 2, 4 => 2),
// and so on.
'AB04' => array(1 => 1, 2 => 2, 3 => 1, 4 => 2),
'AB05' => array(1 => 2, 2 => 1, 3 => 2 )
);
// Initialize counter variable
$points = 0;
// Pass through all questions
foreach ($questions as $questionId => $answers) {
// Set the error counter for this question to 0
$error = 0;
foreach ($answers as $itemId => $default) {
// Assemble item ID
$id = $questionId.'_'.$itemId;
// Call up participant's answer
$answer = value($id);
// Verify answer is correct (actually: for falsehood)
if ($answer != $default) {
// In the event of any deviation, count as an error
$error++;
}
}
// Check if the question has been correctly answered
if ($error == 0) {
// Award a point
$points++;
}
}
// Show result or otherwise process
html('<p>You have scored '.$points.' points.</p>');
if ($points < 10) {
text('feedback1');
} elseif ($points < 20) {
text('feedback2');
} else {
text('feedback3');
}
$type = valueMean('AB01');
$use = valueMean('AB02');
if ($type < 1.5) {
text('typeA');
} elseif ($type <= 4.5) {
text('typeB');
} else {
text('typeC');
}
if ($use < 2.0) {
text('useA');
} elseif ($use < 4.0) {
text('useB');
} else {
text('useC');
}
?>
You will need to define a "value" function.
<?php
//Define it anywhere within the <?php tag
function value($arg){
// Your code logic here
}
I have a parent/child structure where the it can happen that parent can be deleted, and it's children are still going to be in the database. If that happen, the lowest parent should be set parent of 0.
I'm stuck with this problem because I'm not sure how to structure my (possibly recursive) loop.
I need to return an array of page ID's which parents do not exist; example: array(5, 9, 8);
This is my data set, and the structure can be connected through the parent id; we can see that page ID 8 and 9 have parent of 7 which does not exist:
evar_export($orphans($pages));
$data = array (
0 => array (
'id' => 1,
'url' => 'Home-Page',
'parent' => 0
),
1 => array (
'id' => 2,
'url' => 'page1',
'parent' => 1
),
4 => array (
'id' => 5,
'url' => 'page4',
'parent' => 4
),
5 => array (
'id' => 6,
'url' => 'page5',
'parent' => 5
),
6 => array (
'id' => 8,
'url' => 'no-parent-1',
'parent' => 7
),
7 => array (
'id' => 9,
'url' => 'no-parent-2',
'parent' => 7
)
);
I've tried recursion, but I don't know how to catch the end of the sub-tree:
$orphans = function($array, $temp = array(), $index = 0, $parent = 0, $owner = 0) use(&$orphans) {
foreach ($array as $p) {
if($index == 0) {
$owner = $p['id'];
}
if ($index == 0 || $p['id'] == $parent) {
$temp[] = $p['id'];
$result = $orphans($array, $temp, $index + 1, $p['parent'], $owner);
if (isset($result)) {
return $result;
}
}
else {
return $temp;
}
}
};
I named your data array "pages" for this example:
$orphans = array();
foreach($pages as $p)
{
if($p['parent'] == 0)
continue; //End this iteration and move on.
$id = $p['id'];
$parent = $p['parent'];
$parentExists = false;
foreach($pages as $p2)
{
if( $p2['id'] == $parent )
{
$parentExists = true;
break; //Found, so stop looking.
}
}
if(!$parentExists)
{
$orphans[] = $id;
}
}
If you var_dump the $orphans array after this runs, you would get:
array(2) {
[0]=>
int(8)
[1]=>
int(9)
}
Which appears to be the desired result. Unfortunately nesting another foreach within the foreach is required unless you modify your data structure so the IDs are the keys (which I would advise to reduce resource usage to process this). Using the continue / break control structures at least limits usage.
Clarification on Nested Foreach
An ideal data structure would use key value pairs over sequential items, especially when processing dynamic data, because the keys are unknown. Taking your data for example, getting the 4th item's URL is easy:
$id = $pages[4]['id'];
But there is no relational / logical association between the 4th item and the associated data. Its sequential based on the what ever built the data. If, instead, you assign the id as the key, then we could easily find the parent id of the page with id 4:
$parent = $pages[4]['parent'];
So when doing a simple parse of your data to find non-existing parents, you would just have to do this:
foreach($pages as $p)
{
if($p['parent'] == 0)
continue; //End this iteration and move on.
$id = $p['id'];
if(! isset($pages[$p['parent']])
{
$orphans[] = $id;
}
}
Because then we would know for sure that the key is the id and then logically process the data in that fashion. And considering something like a page id is a primary key (non-duplicate), this should be entirely possible.
But without having a logical association between the key and value in the array, we have to look at the entire data set to find matches for each iteration, causing an exponential explosion of resource usage to complete the task.
suppose I have table named categories such as:
id parent_id title
1 0 food
2 1 drinks
3 2 juice
4 0 furniture
5 3 tables
now I want to create dropdown menu on laravel such that it recursively displays child category under parent category with proper indentation or - mark as per depth.E.g.:
<select>
<option value="1">food</option>
<option value="2">-drinks</option>
<option value="3">--juice</option>
<option value="4">furniture</option>
<option value="5">-tables</option>
</select>
Above one is static but I want to generate dropdown structure dynamically as like above recursively for any depth of child category from categories table in laravel.
First of all, you could define a getCategories method on your controller. A recursive method. Ideally, you should implement something like this:
...
// utility method to build the categories tree
private function getCategories($parentId = 0)
{
$categories = [];
foreach(Category::where('parent_id', 0)->get() as $category)
{
$categories = [
'item' => $category,
'children' => $this->getCategories($category->id)
];
}
return $categories;
}
...
Right after, you should pass the final array/collection (or whatever you choose) to the view.
return view('my_view', ['categories' => $this->getCategories()])
Finally, you could use a solution similar to this one.
Not the most elegant, but gets the job done:
<?php
$data = [
['id' => 1, 'parent_id' => 0, 'title' => 'food'],
['id' => 2, 'parent_id' => 1, 'title' => 'drinks'],
['id' => 3, 'parent_id' => 2, 'title' => 'juice'],
['id' => 4, 'parent_id' => 0, 'title' => 'furniture'],
['id' => 5, 'parent_id' => 4, 'title' => 'tables']
];
function recursiveElements($data) {
$elements = [];
$tree = [];
foreach ($data as &$element) {
$element['children'] = [];
$id = $element['id'];
$parent_id = $element['parent_id'];
$elements[$id] =& $element;
if (isset($elements[$parent_id])) { $elements[$parent_id]['children'][] =& $element; }
else { $tree[] =& $element; }
}
return $tree;
}
function flattenDown($data, $index=0) {
$elements = [];
foreach($data as $element) {
$elements[] = str_repeat('-', $index) . $element['title'];
if(!empty($element['children'])) $elements = array_merge($elements, flattenDown($element['children'], $index+1));
}
return $elements;
}
$recursiveArray = recursiveElements($data);
$flatten = flattenDown($recursiveArray);
print_r($flatten);
/*
Outputs:
Array
(
[0] => food
[1] => -drinks
[2] => --juice
[3] => furniture
[4] => -tables
)
*/
Run get method on your Category Eloquent model or use query builder to get all the categories.
Then write a function and call it recursively as many times as you need. filter method would be really helpful to work with your categories collection
Something like this should work:
function getCategories($categories, &$result, $parent_id = 0, $depth = 0)
{
//filter only categories under current "parent"
$cats = $categories->filter(function ($item) use ($parent_id) {
return $item->parent_id == $parent_id;
});
//loop through them
foreach ($cats as $cat)
{
//add category. Don't forget the dashes in front. Use ID as index
$result[$cat->id] = str_repeat('-', $depth) . $cat->title;
//go deeper - let's look for "children" of current category
getCategories($categories, $result, $cat->id, $depth + 1);
}
}
//get categories data. In this case it's eloquent.
$categories = Category::get();
//if you don't have the eloquent model you can use DB query builder:
//$categories = DB::table('categories')->select('id', 'parent_id', 'title')->get();
//prepare an empty array for $id => $formattedVal storing
$result = [];
//start by root categories
getCategories($categories, $result);
Didn't test it myself, but the idea should be clear enough. The good thing is you're only executing a single query. The bad thing is you load the whole table into memory at once.
If your table has more columns that you don't need for this algorithm you should specify only the needed ones in your query.
So I have an array of items in php, some may be linked to others via a parent_id key. I'm looking to sort this array so that any items whose parent is in this array ends up positioned right below the parent.
example: (actual array has many more keys)
some_array[0]['id'] = 15001;
some_array[0]['parent_id'] = 14899;
some_array[1]['id'] = 14723;
some_array[1]['parent_id'] = 0; //parent_id of 0 means item has no parent of its own
some_array[2]['id'] = 14899;
some_array[2]['parent_id'] = 0;
some_array[3]['id'] = 15000;
some_array[3][parent_id'] = 14723;
I'd like to sort these so they end up in this order:
some_array[0]['id'] = 14723;
some_array[1]['id'] = 15000;
some_array[2]['id'] = 14899;
some_array[3]['id'] = 15001;
ie. items are just below their parents.
Thanks in advance!
My shorter version of mattwang's answer:
/**
* sort parents before children
*
* #param array $objects input objects with attributes 'id' and 'parent'
* #param array $result (optional, reference) internal
* #param integer $parent (optional) internal
* #param integer $depth (optional) internal
* #return array output
*/
function parent_sort(array $objects, array &$result=array(), $parent=0, $depth=0) {
foreach ($objects as $key => $object) {
if ($object->parent == $parent) {
$object->depth = $depth;
array_push($result, $object);
unset($objects[$key]);
parent_sort($objects, $result, $object->id, $depth + 1);
}
}
return $result;
}
Only actual difference is that it sorts an array of objects instead of an array of arrays.
I doubt that you guys are still looking for a real answer to this, but it might help out others with the same problem. Below is a recursive function to resort an array placing children beneath parents.
$initial = array(
array(
'name' => 'People',
'ID' => 2,
'parent' => 0
),
array(
'name' => 'Paul',
'ID' => 4,
'parent' => 2
),
array(
'name' => 'Liz',
'ID' => 5,
'parent' => 2
),
array(
'name' => 'Comus',
'ID' => 6,
'parent' => 3
),
array(
'name' => 'Mai',
'ID' => 7,
'parent' => 2
),
array(
'name' => 'Titus',
'ID' => 8,
'parent' => 3
),
array(
'name' => 'Adult',
'ID' => 9,
'parent' => 6
),
array(
'name' => 'Puppy',
'ID' => 10,
'parent' => 8
),
array(
'name' => 'Programmers',
'ID' => 11,
'parent' => 4
) ,
array(
'name' => 'Animals',
'ID' => 3,
'parent' => 0
)
);
/*---------------------------------
function parentChildSort_r
$idField = The item's ID identifier (required)
$parentField = The item's parent identifier (required)
$els = The array (required)
$parentID = The parent ID for which to sort (internal)
$result = The result set (internal)
$depth = The depth (internal)
----------------------------------*/
function parentChildSort_r($idField, $parentField, $els, $parentID = 0, &$result = array(), &$depth = 0){
foreach ($els as $key => $value):
if ($value[$parentField] == $parentID){
$value['depth'] = $depth;
array_push($result, $value);
unset($els[$key]);
$oldParent = $parentID;
$parentID = $value[$idField];
$depth++;
parentChildSort_r($idField,$parentField, $els, $parentID, $result, $depth);
$parentID = $oldParent;
$depth--;
}
endforeach;
return $result;
}
$result = parentChildSort_r('ID','parent',$initial);
print '<pre>';
print_r($result);
print '</pre>';
It's a wind down method that removes elements from the original array and places them into result set in the proper order. I made it somewhat generic for you, so it just needs you to tell it what your 'ID' field and 'parent' fields are called. Top level items are required to have a parent_id (however you name it) of 0.
You can use usort to sort by a user defined function:
function cmp($a, $b)
{
if ( $a['id'] == $b['id'] ) {
return 0;
} else if ( $a['parent_id'] ) {
if ( $a['parent_id'] == $b['parent_id'] ) {
return ( $a['id'] < $b['id'] ? -1 : 1 );
} else {
return ( $a['parent_id'] >= $b['id'] ? 1 : -1 );
}
} else if ( $b['parent_id'] ) {
return ( $b['parent_id'] >= $a['id'] ? -1 : 1);
} else {
return ( $a['id'] < $b['id'] ? -1 : 1 );
}
}
usort($some_array, "cmp");
Note: this will only work with a tree that is one level deep (meaning no children of children). For more complex trees you probably want to sort the data into a graph and then flatten it.
Edit: fixed to edit a case where $b has a parent but $a does not.
Just use usort() function and compare two different elements of the 'big array' in a way you need. This becomes then a question about 'how do I really decide which element is before which element?'.
The simple usort won't work if you want to support more than one layer of children. There's simply no way to know how two arbitrary elements compare without other information.
I didn't think about it much, so perhaps it doesn't work. But here's a sorting class:
class TopSort
{
private $sorted, $unsorted;
private $history;
public function sort(array $unsorted)
{
$this->sorted = array();
$this->unsorted = $unsorted;
$this->history = array();
usort($this->unsorted, function($a, $b)
{
return $b['id'] - $a['id'];
});
foreach ($this->unsorted as $i => $a)
if ($a['parent_id'] == 0) $this->visit($i);
return array_reverse($this->sorted);
}
private function visit($i)
{
if (!array_key_exists($i, $this->history))
{
$this->history[$i] = true;
foreach ($this->unsorted as $j => $a)
if ($a['parent_id'] == $this->unsorted[$i]['id']) $this->visit($j);
$this->sorted[] = $this->unsorted[$i];
}
}
}
$sorter = new TopSort();
$some_array = $sorter->sort($some_array);
The idea here is to first sort in reverse by id. Then build up a new array by inserting the deepest elements (those with no children) first. Since we initially sorted the array by reverse id, it should mean the entire thing is upside down. After reversing the array, it should be exactly like you want. (Of course one could unshift items onto the array to avoid the reverse operation, but that might be slower...)
And this is very unoptimized as it iterates over the entire array many, many times. With a little rework, it wouldn't need to do that.
Here's an alternative class that is more optimized:
class TopSort
{
private $sorted;
public function sort(array $nodes)
{
$this->sorted = array();
# sort by id
usort($nodes, function($a, $b) {
return $a['id'] - $b['id'];
});
# build tree
$p = array(0 => array());
foreach($nodes as $n)
{
$pid = $n['parent_id'];
$id = $n['id'];
if (!isset($p[$pid]))
$p[$pid] = array('child' => array());
if (isset($p[$id]))
$child = &$p[$id]['child'];
else
$child = array();
$p[$id] = $n;
$p[$id]['child'] = &$child;
unset($child);
$p[$pid]['child'][] = &$p[$id];
}
$nodes = $p['0']['child'];
unset($p);
# flatten array
foreach ($nodes as $node)
$this->flatten($node);
return $this->sorted;
}
private function flatten(array $node)
{
$children = $node['child'];
unset($node['child']);
$this->sorted[] = $node;
foreach ($children as $node)
$this->flatten($node);
}
}
$sorter = new TopSort();
$sorted = $sorter->sort($some_array);
It's a three step approach:
Sort by id (usort)
Build nested array structure.
Flatten array in pre-order.
By virtue of presorting by id, each group of children should be sorted correctly.