Adaptive parent relative to children's attributes in a recursive array - php

I'm trying to make a simple tree structure where every task has a certain percentage of completion and its parent has to inherit average completion of its direct children, as seen conceptually on the picture below. (0s are percentages of completion, for example subtask2 could be 100% and subtask2 0%, which would give task1 50% completion and therefore stackoverflow would have 25%, given task2 is at 0)
The issue I'm having is that I need to start, apparently, from the deepest children, but I can't seem to figure out how to implement such reversal traversal from leafs to root.
I have tried with normal recursive as well as double for loop and both only achieve first level calculations (in the picture example task1 is calculated, but stackoverflow will remain 0).
Note: Only leafs can actually have completion percentage, since every other element, which is not a leaf, inherits percentage from its children. (how paradoxical)
If any of you have any ideas on how to implement such an algorithm, be it conceptually or actual code, I would very much appreciate any input.
Below is structure of this array (only kept relevant information):
[0] => Array
(
[title] => stackoverflow
[completion] => 0
[children] => Array
(
[0] => Array
(
[title] => task2
[completion] => 0
)
[1] => Array
(
[title] => task1
[completion] => 0
[children] => Array
(
[0] => Array
(
[title] => subtask2
[completion] => 100
)
[1] => Array
(
[title] => subtask1
[completion] => 0
)
)
)
)
)
I seem to be having a similar issue than the issue in this thread: Percentages and trees however, I need my task to have actual percentages, not only completed/non-completed. All math is completely linear, meaning that parent's percentage = (addition of all percentages of children) / (number of children)
Also var_export:
array (
0 =>
array (
'uuid' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
'title' => 'stackoverflow',
'completed' => '0',
'parent' => NULL,
'children' =>
array (
0 =>
array (
'uuid' => '72ce49a6-76e5-495e-a3f8-0f13d955a3b5',
'title' => 'task2',
'completed' => '0',
'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
),
1 =>
array (
'uuid' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
'title' => 'task1',
'completed' => '0',
'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
'children' =>
array (
0 =>
array (
'uuid' => 'ac5e9d37-8f14-4169-bcf2-e7b333c5faea',
'title' => 'subtask2',
'completed' => '0',
'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
),
1 =>
array (
'uuid' => 'f74b801f-c9f1-40df-b491-b0a274ffd301',
'title' => 'subtask1',
'completed' => '0',
'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
),
),
),
),
),
)

Here is a recursive function that passes the parent by reference until it finds a leaf and updates totals working backward.
function completionTree(&$elem, &$parent=NULL) {
// Handle arrays that are used only as a container... if we have children but no uuid, simply descend.
if (is_array($elem) && !isset($elem['uuid'])) {
foreach($elem AS &$child) {
completionTree($child, $elem);
}
}
// This array has children. Iterate recursively for each child.
if (!empty($elem['children'])) {
foreach ($elem['children'] AS &$child) {
completionTree($child, $elem);
}
}
// After recursion to handle children, pass completion percentages up to parent object
// If this is the top level, nothing needs to be done (but suppress that error)
if (#$parent['completed'] !== NULL) {
// Completion must be multiplied by the fraction of children it represents so we always add up to 100. Since values are coming in as strings, cast as float to be safe.
$parent['completed'] = floatval($parent['completed']) + (floatval($elem['completed']) * (1/count($parent['children'])));
}
}
// Data set defined statically for demonstration purposes
$tree = array(array (
'uuid' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
'title' => 'stackoverflow',
'completed' => '0',
'parent' => NULL,
'children' =>
array (
0 =>
array (
'uuid' => '72ce49a6-76e5-495e-a3f8-0f13d955a3b5',
'title' => 'task2',
'completed' => '0',
'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
),
1 =>
array (
'uuid' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
'title' => 'task1',
'completed' => '0',
'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
'children' =>
array (
0 =>
array (
'uuid' => 'ac5e9d37-8f14-4169-bcf2-e7b333c5faea',
'title' => 'subtask2',
'completed' => '0',
'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
),
1 =>
array (
'uuid' => 'f74b801f-c9f1-40df-b491-b0a274ffd301',
'title' => 'subtask1',
'completed' => '100',
'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
),
),
),
),
),
);
// Launch recursive calculations
completionTree($tree);
// Dump resulting tree
var_dump($tree);

Though this is answered, I'd like to leave a solution that seems a bit more intuitive (IMHO). Instead of passing down the parent, just handle the children first:
/**
* #param array $nodes
*
* #return array
*/
function calcCompletion(array $nodes): array {
// for each node in nodes
return array_map(function (array $node): array {
// if it has children
if (array_key_exists('children', $node) && is_array($node['children'])) {
// handle the children first
$node['children'] = calcCompletion($node['children']);
// update this node by *averaging* the children values
$node['completed'] = array_reduce($node['children'], function (float $acc, array $node): float {
return $acc + floatval($node['completed']);
}, 0.0) / count($node['children']);
}
return $node;
}, $nodes);
}

Well, this might be a little overhead, but you can use RecursiveArrayIterator. First, you must extend it to handle your tree structure:
class MyRecursiveTreeIterator extends RecursiveArrayIterator
{
public function hasChildren()
{
return isset($this->current()['children'])
&& is_array($this->current()['children']);
}
public function getChildren()
{
return new static($this->current()['children']);
}
}
Then using RecursiveIteratorIterator you can create an iterator which will process your tree starting from leaves:
$iterator = new RecursiveIteratorIterator(
new MyRecursiveTreeIterator($tasks),
RecursiveIteratorIterator::CHILD_FIRST
);
Then with this one, you can add your calculation logic:
$results = [];
$temp = [];
$depth = null;
foreach ($iterator as $node) {
if ($iterator->getDepth() === 0) {
// If there were no children use 'completion'
// else use children average
if (
is_null($depth)
|| !isset($temp[$depth])
|| !count($temp[$depth])
) {
$percentage = $node['completed'];
} else {
$percentage = array_sum($temp[$depth]) / count($temp[$depth]);
}
$results[$node['title']] = $percentage;
continue;
}
// Set empty array for current tree depth if needed.
if (!isset($temp[$iterator->getDepth()])) {
$temp[$iterator->getDepth()] = [];
}
// If we went up a tree, collect the average of children
// else push 'completion' for children of current depth.
if ($iterator->getDepth() < $depth) {
$percentage = array_sum($temp[$depth]) / count($temp[$depth]);
$temp[$depth] = [];
$temp[$iterator->getDepth()][] = $percentage;
} else {
$temp[$iterator->getDepth()][] = $node['completed'];
}
$depth = $iterator->getDepth();
}
Here is a demo.

Related

Unexpected behavior in recursive function

The task is to remove arrays recursively that have error => 4 (i.e key with that value) with their keys, and then turn remained arrays into objects.
The structure of incoming array might be different. Two examples of it are this ones:
// Example input #1
$ex_input_1 = array(
'files' => array(
0 => array(
'name' => 'file.jpg',
'size' => '244235',
'tmp_name' => '/usr/tmp/24ffds.tmp',
'error' => 0
),
1 => array(
'name' => '',
'size' => '',
'tmp_name' => '',
'error' => 4
)
),
'cover' => array(
'name' => '',
'size' => '',
'tmp_name' => '',
'error' => 4
),
'document' => array(
'name' => 'file.doc',
'size' => '244235',
'tmp_name' => '/usr/tmp/24ffds.tmp',
'error' => 0
)
);
// Example input #2
$ex_input_2 = array(
0 => array(
'name' => 'file.jpg',
'size' => '244235',
'tmp_name' => '/usr/tmp/24ffds.tmp',
'error' => 0
),
1 => array(
'name' => '',
'size' => '',
'tmp_name' => '',
'error' => 4
)
);
i.e an array that have name, size, tmp_name, error keys might be at any level down.
What I tried:
Tried to write a simple handler with two methods, where the first one is recursive handler and the second one is hydrator method. Here's it with relevant parts:
<?php
class FileInputParser
{
/**
* Recursively hydrate array entires skipping empty files
*
* #param array $files
* #return array
*/
public function hydrateAll(array $files)
{
foreach ($files as $name => $file) {
if (!is_array($file)) {
continue;
}
foreach ($file as $key => $value) {
if (is_array($value)) {
// Recursise call
$files[$name] = $this->hydrateAll($files[$name]);
} else {
$target = $this->hydrateSingle($file);
// Here I'm EXPLICTLY asking not to push an array, which has error = 4
// But it pushes anyway!!
if ($target !== false) {
unset($files[$name]);
}
}
}
}
return $files;
}
/**
* Hydrates a single file item
*
* #param array $file
* #return mixed
*/
private function hydrateSingle(array $file)
{
$entity = new stdclass;
$entity->name = $file['name'];
$entity->tmp_name = $file['tmp_name'];
$entity->error = $file['error'];
$entity->size = $file['size'];
if ($entity->error != 4) {
return $entity;
} else {
// Returning false to indicate, that this one should not be pushed in output
return false;
}
}
}
The problem
While at first glance it works, the problem is that, when I'm asking explicitly not to add an array that has error = 4 to output, but it continues to add!
You can run aforementioned code with input examples:
<?php
$parser = new FileInputParser();
$output = $parser->hydrateAll($ex_input_1);
echo '<pre>', print_r($output, true);
to see that it also returns unwanted arrays (i.e the ones that have error = 4).
The question
Why it continues to add arrays to output that have error = 4 ?
if you have a better idea on handling this, I'd love to hear it.
Here's a recursive function that will do the filtering you want. When it reaches the bottom of the tree, it checks for error == 4 and if it is, returns an empty array, otherwise it returns the current array. At the next level down any empty values returned are removed by array_filter:
function array_filter_recursive($array) {
if (isset($array['error'])) {
// bottom of tree
return $array['error'] == 4 ? array() : $array;
}
foreach ($array as $key => $value) {
$array[$key] = array_filter_recursive($value);
}
// remove any empty values
return array_filter($array);
}
Output from filtering your two input arrays:
Array (
[files] => Array (
[0] => Array (
[name] => file.jpg
[size] => 244235
[tmp_name] => /usr/tmp/24ffds.tmp
[error] => 0
)
)
[document] => Array (
[name] => file.doc
[size] => 244235
[tmp_name] => /usr/tmp/24ffds.tmp
[error] => 0
)
)
Array (
[0] => Array (
[name] => file.jpg
[size] => 244235
[tmp_name] => /usr/tmp/24ffds.tmp
[error] => 0
)
)
Demo on 3v4l.org

sorting a multi dimensional array in php

I have an array of arrays, as such
$statuses = array(
[0] => array('id'=>10, 'status' => 'active'),
[1] => array('id'=>11, 'status' => 'closed'),
[2] => array('id'=>12, 'status' => 'active'),
[3] => array('id'=>13, 'status' => 'stopped'),
)
I want to be able to make a new array of arrays and each of those sub arrays would contain the elements based on if they had the same status.
The trick here is, I do not want to do a case check based on hard coded status names as they can be random. I want to basically do a dynamic comparison, and say "if you are unique, then create a new array and stick yourself in there, if an array already exists with the same status than stick me in there instead". A sample result could look something like this.
Ive really had a challenge with this because the only way I can think to do it is check every single element against every other single element, and if unique than create a new array. This gets out of control fast if the original array is larger than 100. There must be some built in functions that can make this efficient.
<?php
$sortedArray = array(
['active'] => array(
array(
'id' => 10,
'status' => 'active'
),
array(
'id' => 12,
'status' => 'active'
)
),
['closed'] => array(
array(
'id' => 11,
'status' => 'active'
)
),
['stopped'] => array(
array(
'id' => 13,
'status' => 'active'
)
),
)
$SortedArray = array();
$SortedArray['active'] = array();
$SortedArray['closed'] = array();
$SortedArray['stopped'] = array();
foreach($statuses as $Curr) {
if ($Curr['status'] == 'active') { $SortedArray['active'][] = $Curr; }
if ($Curr['status'] == 'closed') { $SortedArray['closed'][] = $Curr; }
if ($Curr['status'] == 'stopped') { $SortedArray['stopped'][] = $Curr; }
}
You can also do it with functional way though it's pretty the same like Marc said.
$sorted = array_reduce($statuses, function($carry, $status) {
$carry[$status['status']][] = $status;
return $carry;
}, []);

PHP Category Reverse Traversal Algorithm

I'm trying to optimize an e-commerce category system with unlimited category depth (barring system memory limitations). I retrieve all the categories at once and order them as a multi-dimensional array that roughly looks like:
[array] (
[0] (
'CategoryId' => 1,
'ParentCategoryId' => 0,
'Title' => 'Category A',
'SubCategories' => [array] (
[0] (
'CategoryId' => 2,
'ParentCategoryId' => 1,
'Title' => 'Category B',
'SubCategories' => [array] (
[0] (
'CategoryId' => 3,
'ParentCategoryId' => 2,
'Title' => 'Category C'
)
)
)
)
)
)
Each item in the array is actually an object, but for simplicity I wrote it out kind of like an array format.
I'm able to traverse my tree downwards using this function:
/**
* Find Branch using Recursive search by Object Key
* #param String Needle
* #param Array Haystack
* #return Array
*/
public static function findBranchByKey($key, $needle, $haystack)
{
foreach ($haystack as $item)
{
if ( $item->$key == $needle || ( is_object($item) && $item = self::findBranchByKey($key, $needle, $item->SubCategories)) )
{
return $item;
}
}
return false;
}
This finds the object with a matching key and returns it (which may contain more subcategories).
My issue is figuring out how to traverse the other direction. For example, using the data above, let's say I am displaying "Category C" and want to create bread crumbs of it's parents. I can't think of a good way to take my tree array, jump to a specific subcategory, then iterate upwards to get each parent. A resulting array from something like this could be like this so it's easy to spit them out as bread crumbs:
array( 'Category A', 'Category B', 'Category C' )
I could probably do this using SQL in my database but I'd like to retrieve the tree once, cache it, and perform traversal on that object whenever I need to rather than making tons of queries.
TL;DR; How can I traverse upwards in a multidimensional array of categories?
It can be done by recursion.
Let's say, this function should work:
function getPath($id, $tree, &$path = array()) {
foreach ($tree as $item) {
if ($item['CategoryId'] == $id) {
array_push($path, $item['CategoryId']);
return $path;
}
if (!empty($item['SubCategories'])) {
array_push($path, $item['CategoryId']);
if (getPath($id, $item['SubCategories'], $path) === false) {
array_pop($path);
} else {
return $path;
}
}
}
return false;
}
This:
$data = array(
array(
'CategoryId' => 10,
'ParentCategoryId' => 0,
'SubCategories' => array(
array(
'CategoryId' => 12,
'ParentCategoryId' => 1,
'SubCategories' => array()
),
)
),
array(
'CategoryId' => 1,
'ParentCategoryId' => 0,
'SubCategories' => array(
array(
'CategoryId' => 2,
'ParentCategoryId' => 1,
'SubCategories' => array()
),
array(
'CategoryId' => 3,
'ParentCategoryId' => 1,
'SubCategories' => array()
),
array(
'CategoryId' => 4,
'ParentCategoryId' => 1,
'SubCategories' => array(
array(
'CategoryId' => 5,
'ParentCategoryId' => 4,
'SubCategories' => array()
),
)
)
)
)
);
$result = getPath(5, $data);
print_r($result);
will result in:
Array ( [0] => 1 [1] => 4 [2] => 5 )

Hierarchy tree to get parent slug

Array
(
[1] => Array
(
[id] => 1
[parent_id] => 0
[name] => Men
[slug] => men
[status] => 1
)
[2] => Array
(
[id] => 2
[parent_id] => 1
[name] => Shoes
[slug] => shoes
[status] => 1
)
[3] => Array
(
[id] => 3
[parent_id] => 2
[name] => Sports
[slug] => sports
[status] => 1
)
)
Here is my function to make a sidebar menu tree.
function ouTree($array, $currentParent, $currentLevel = 0, $previousLevel = -1)
{
foreach ( $array as $categoryId => $category)
{
if ( $currentParent == $category['parent_id'])
{
if ( $currentLevel > $previousLevel) echo "<ul>";
if ( $currentLevel == $previousLevel) echo "</li>";
echo "<li><a href='/category/{$category['slug']}' title='{$category['name']}'>{$category['name']}</a>";
if ( $currentLevel > $previousLevel)
$previousLevel = $currentLevel;
$currentLevel++;
ouTree ($array, $categoryId, $currentLevel, $previousLevel);
$currentLevel--;
}
}
if ( $currentLevel == $previousLevel) echo "</li></ul>";
}
ouTree($array, 0);
Current function will call only current slug Men. How do I retrieve or repeat the parent slug to current list order? So it will be like this
Men
Shoes
Sports
OK, here's a script to sink your teeth into as been as i had a few minutes. Look at the comments! it explains everything in the script. It to be honest, should work straight away as i've copied your array structure, but please read it and understand it, don't just copy and paste blindly, you wont learn anything that way (i wouldn't normally provide full working code, but this ones pretty hard to explain).
<?php
/**
* Heres your categories array structure, they can be in any order as we will sort them into an hierarchical structure in a moment
*/
$categories = array();
$categories[] = array('id'=>5, 'parent_id' => 4, 'name' => 'Bedroom wear', 'slug' => 'bwear', 'status' => 1);
$categories[] = array('id'=>6, 'parent_id' => 3, 'name' => 'Rolex', 'slug' => 'rolex', 'status' => 1);
$categories[] = array('id'=>1, 'parent_id' => 0, 'name' => 'Men', 'slug' => 'men', 'status' => 1);
$categories[] = array('id'=>2, 'parent_id' => 0, 'name' => 'Women', 'slug' => 'women', 'status' => 1);
$categories[] = array('id'=>3, 'parent_id' => 1, 'name' => 'Watches', 'slug' => 'watches', 'status' => 1);
$categories[] = array('id'=>4, 'parent_id' => 2, 'name' => 'Bras', 'slug' => 'bras', 'status' => 1);
$categories[] = array('id'=>7, 'parent_id' => 2, 'name' => 'Jackets', 'slug' => 'jackets', 'status' => 1);
/**
* This function takes the categories and processes them into a nice tree like array
*/
function preprocess_categories($categories) {
// First of all we sort the categories array by parent id!
// We need the parent to be created before teh children after all?
$parent_ids = array();
foreach($categories as $k => $cat) {
$parent_ids[$k] = $cat['parent_id'];
}
array_multisort($parent_ids, SORT_ASC, $categories);
/* note: at this point, the categories are now sorted by the parent_id key */
// $new contains the new categories array which you will pass into the tree function below (nothign fancy here)
$new = array();
// $refs contain references (aka points) to places in the $new array, this is where the magic happens!
// without references, it would be difficult to have a completely random mess of categories and process them cleanly
// but WITH references, we get simple access to children of children of chilren at any point of the loop
// each key in this array is teh category id, and the value is the "children" array of that category
// we set up a default reference for top level categories (parent id = 0)
$refs = array(0=>&$new);
// Loop teh categories (easy peasy)
foreach($categories as $c) {
// We need the children array so we can make a pointer too it, should any children categories popup
$c['children'] = array();
// Create the new entry in the $new array, using the pointer from $ref (remember, it may be 10 levels down, not a top level category) hence we need to use the reference/pointer
$refs[$c['parent_id']][$c['id']] = $c;
// Create a new reference record for this category id
$refs[$c['id']] = &$refs[$c['parent_id']][$c['id']]['children'];
}
return $new;
}
/**
* This function generates our HTML from the categories array we have pre-processed
*/
function tree($categories, $baseurl = '/category/') {
$tree = "<ul>";
foreach($categories as $category) {
$tree .= "<li>";
$tree .= "<a href='".$baseurl.$category['slug']."'>".$category['name']."</a>";
// This is the magci bit, if there are children categories, the function loops back on itself
// and processes the children as if they were top level categories
// we append the children to the main tree string rather tha echoing for this reason
// we also pass the base url PLUS our category slug as the "new base url" so it can build the URL correctly
if(!empty($category['children'])) {
$tree .= tree($category['children'], $baseurl.$category['slug'].'/');
}
$tree .= "</li>";
}
$tree .= "</ul>";
return $tree;
}
///echo "<pre>"; print_r(preprocess_categories($categories)); die();
echo tree( preprocess_categories( $categories ) );
?>
Heres a pastebin link if you like pretty coloured code: http://pastebin.com/KVhCuvs3

Matching an array value by key in PHP

I have an array of items:
array(
[0] => array(
'item_no' => 1
'item_name' => 'foo
)
[1] => array(
'item_no' => 2
'item_name' => 'bar'
)
) etc. etc.
I am getting another array from a third party source and need to remove items that are not in my first array.
array(
[0] => array(
'item_no' => 1
)
[1] => array(
'item_no' => 100
) # should be removed as not in 1st array
How would I search the first array using each item in the second array like (in pseudo code):
if 'item_no' == x is in 1st array continue else remove it from 2nd array.
// Returns the item_no of an element
function get_item_no($arr) { return $arr['item_no']; }
// Arrays of the form "item_no => position in the array"
$myKeys = array_flip(array_map('get_item_no', $myArray));
$theirKeys = array_flip(array_map('get_item_no', $theirArray));
// the part of $theirKeys that has an item_no that's also in $myKeys
$validKeys = array_key_intersect($theirKeys, $myKeys);
// Array of the form "position in the array => item_no"
$validPos = array_flip($validKeys);
// The part of $theirArray that matches the positions in $validPos
$keptData = array_key_intersect($theirArray, $validPos);
// Reindex the remaining values from 0 to count() - 1
return array_values($keptData);
All of this would be easier if, instead of storing the key in the elements, you stored it as the array key (that is, you'd be using arrays of the form "item_no => item_data") :
// That's all there is to it
return array_key_intersect($theirArray, $myArray);
You can also do:
$my_array =array(
0 => array( 'item_no' => 1,'item_name' => 'foo'),
1 => array( 'item_no' => 2,'item_name' => 'bar')
);
$thrid_party_array = array(
0 => array( 'item_no' => 1),
1 => array( 'item_no' => 100),
);
$temp = array(); // create a temp array to hold just the item_no
foreach($my_array as $key => $val) {
$temp[] = $val['item_no'];
}
// now delete those entries which are not in temp array.
foreach($thrid_party_array as $key => $val) {
if(!in_array($val['item_no'],$temp)) {
unset($thrid_party_array[$key]);
}
}
Working link
If your key is not actually a key of your array but a value, you will probably need to do a linear search:
foreach ($itemsToRemove as $itemToRemove) {
foreach ($availableItems as $key => $availableItem) {
if ($itemToRemove['item_no'] === $availableItem['item_no']) {
unset($availableItems[$key]);
}
}
}
It would certainly be easier if item_no is also the key of the array items like:
$availableItems = array(
123 => array(
'item_no' => 123,
'item_name' => 'foo'
),
456 => array(
'item_no' => 456,
'item_name' => 'bar'
)
);
With this you could use a single foreach and delete the items by their keys:
foreach ($itemsToRemove as $itemToRemove) {
unset($availableItems[$itemToRemove['item_no']]);
}
You could use the following to build an mapping of item_no to your actual array keys:
$map = array();
foreach ($availableItems as $key => $availableItem) {
$map[$availableItems['item_no']] = $key;
}
Then you can use the following to use the mapping to delete the corresponding array item:
foreach ($itemsToRemove as $itemToRemove) {
unset($availableItems[$map[$itemToRemove['item_no']]]);
}

Categories