php convert array into a hierarchical nested set for database - php

So I read this:
http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/
And i'm using mysql and php
What I need is script that convert only for one time the old data into data with the left/right values.
I don't need to add/update things.
The php data
$in_array = array (
array(
'id' => 400,
'n' => 'Sub 1a',
's' => array (
array (
'n' => 'Sub 1b',
'id' => 421,
's' => array (
array (
'n' => 'Sub 1c',
'id' => 422,
)
)
)
)
),
array(
'id' => 500,
'n' => 'Sub 2a',
's' => array (
array (
'n' => 'Sub 2b',
'id' => 521,
's' => array (
array (
'n' => 'Sub 3b',
'id' => 522,
)
)
)
)
)
);
At the moment i'm trying to solve this with this script but that don't work like it should.
$a_newTree = array();
function rebuild_tree($parent, $left) {
global $a_newTree;
$indexed = array();
// the right value of this node is the left value + 1
$right = $left+1;
// get all children of this node
foreach($parent as $cat){
// recursive execution of this function for each
// child of this node
// $right is the current right value, which is
// incremented by the rebuild_tree function
if(is_array($cat) && array_key_exists('s',$cat)){
$indexed['n'] = $cat['n'];
$right = rebuild_tree($cat, $right);
}
}
// we've got the left value, and now that we've processed
// the children of this node we also know the right value
array_push($a_newTree,array(':lft'=>$left,':rgt'=>$right,':name'=>'bla'));
// return the right value of this node + 1
return $right+1;
}
rebuild_tree($in_array, 1);
I can access mysql and query so the output is like this
Sub 1a | Sub 1b | Sub 1c
Sub 1a | Sub 1b | Sub 1d
Sub 1a | Sub 1b | Sub 1e
Sub 1a | Sub 1f | Null
Sub 1a | Sub 1g | Null
Sub 2a | Sub 2b | Sub 2c
With that data I made the array above.

The array was not oke.
This is working
$in_array = array (
// array(
'id' => 400,
'n' => 'Sub 1a',
's' => array (
// array (
'n' => 'Sub 1b',
'id' => 421,
's' => array (
// array (
'n' => 'Sub 1c',
'id' => 422,
// )
)
// )
// )
),
array(
'id' => 500,
'n' => 'Sub 2a',
's' => array (
// array (
'n' => 'Sub 2b',
'id' => 521,
's' => array (
// array (
'n' => 'Sub 3b',
'id' => 522,
// )
)
// )
)
)
);
$a_newTree = array();
function rebuild_tree($parent, $left) {
global $a_newTree;
$indexed = array();
$right = $left+1;
if(array_key_exists('n',$parent)){
$indexed['n'] = $parent['n'];
}else{
$indexed['n'] = '?';
}
foreach($parent as $cat){
if(is_array($cat)){
$right = rebuild_tree($cat, $right);
}
}
array_push($a_newTree,array(':lft'=>$left,':rgt'=>$right,':name'=>$indexed['n']));
return $right+1;
}
rebuild_tree($in_array, 1);

Related

Loop into multidimensional array from top to bottom

I have this tree :
Array
(
[0] => Array
(
[id] => 1
[parent_id] => 0
[title] => Parent Page
[children] => Array
(
[0] => Array
(
[id] => 2
[parent_id] => 1
[title] => Sub Page
),
[1] => Array
(
[id] => 5
[parent_id] => 1
[title] => Sub Page 2
)
)
)
[1] => Array
(
[id] => 4
[parent_id] => 0
[title] => Another Parent Page
)
)
And I'm looking for a display from top to bottom.
And display something like this :
1
1.2
1.5
4
But if I have id 3 which is a leaf from 5 I would like this :
1
1.2
1.5
1.5.3
4
I have search a lot and my brain is limited when i'm using recursivity..
I have tried this :
function printAll($a){
foreach ($a as $v){
if (!array_key_exists('children', $v)){
debugLog($v['id']);
return;
}
else{
$arrayChildrens = $v['children'];
foreach($arrayChildrens as $c){
$arrayChildrens = $c['children'];
$this->printAll($arrayChildrens);
}
}
}
}
But doesn't work..
I tried to begin just to display
1
2
5
4
But my goal is to display id parents before id ( like Ishowed you before)
Thanks a lot !
This function should give you your expected output.
function printAll($a, $prefix = '') {
//loop through $a
foreach($a as $v) {
//echo current level `id` with previous `$prefix`
echo "{$prefix}{$v['id']}\n";
//check if current level contains children
if(!empty($v['children'])) {
//clean up prefix to remove extra `.` at the end of prefixes
$prev_prefix = rtrim($prefix, '.');
//recurse printAll again passing the children as `$a` and a `$prefix` being the previous levels combined e.g `1.5`
//also clean up extra periods at the start of the prefix
printAll($v['children'], ltrim("{$prev_prefix}.{$v['id']}.", "."));
}
}
}
Output:
1
1.2
1.5
1.5.3
4
Using a proper return
Usually with a function you actually want the function to return values instead of echoing them automatically to your page. If you want this function to return an array of values instead of echoing them, you could do this:
function printAll($a, $level = '', $values = []) {
foreach($a as $v) {
$values[] = $value = "{$level}{$v['id']}";
if(!empty($v['children'])) {
$values = printAll($v['children'], "{$value}.", $values);
}
}
return $values;
}
Which will have a result like this:
Array
(
[0] => 1
[1] => 1.2
[2] => 1.5
[3] => 1.5.3
[4] => 4
)
This should do the job.
$arr = array(
array(
'id' => 1,
'parent_id' => 0,
'title' => 'Parent Page',
'children' => array(
array(
'id' => 2,
'parent_id' => 1,
'title' => 'Sub Page',
),
array(
'id' => 5,
'parent_id' => 1,
'title' => 'Sub Page 2',
'children' => array(
array(
'id' => 7,
'parent_id' => 5,
'title' => 'Sub Page',
),
array(
'id' => 8,
'parent_id' => 5,
'title' => 'Sub Page 2',
)
)
)
)
),
array(
'id' => 4,
'parent_id' => 0,
'title' => 'Another Parent Page',
)
);
function printAll($arr, $parent = [])
{
if (is_array($arr)) {
foreach ($arr as $k => $v) {
if (isset($v['id'])) {
$parent[] = $v['id'];
echo implode('.', $parent) . PHP_EOL;
}
if (isset($v['children'])) {
printAll($v['children'], $parent);
}
array_pop($parent);
}
}
}
printAll($arr);
Output
1
1.2
1.5
1.5.7
1.5.8
4
Working demo.

Group data sets by specific column where column names are the first row in each set

I have an array of arrays like this:
$data = array (
'data1' => array (
0 =>
array (
0 => 'ID',
1 => 'PinCode',
2 => 'Date',
),
1 =>
array (
0 => '101',
1 => '454075',
2 => '2012-03-03',
),
2 =>
array (
0 => '103',
1 => '786075',
2 => '2012-09-05',
),
),
'data2' => array (
0 =>
array (
0 => 'Balance',
1 => 'ID',
),
1 =>
array (
0 => '4533',
1 => '101',
)
),
'data3' => array (
0 =>
array (
0 => 'Active',
1 => 'ID',
),
1 =>
array (
0 => 'Yes',
1 => '101',
),
2 =>
array (
0 => 'No',
1 => '103',
)
),
);
In the $data array there are three arrays named data1, data2 and data3 respectively.
In each array, the first row is the name of the columns and the remaining rows are the values for those columns (think of it like a table).
In each data1,data2 and data3 the first row contains a column called ID.
I want to group the data from all three arrays based on the matching ID field such that the final output array is like this:
Desired Output:
$output =
array (
'output' =>
array (
0 =>
array (
0 => 'ID',
1 => 'PinCode',
2 => 'Date',
3 => 'Balance',
4 => 'Active',
),
1 =>
array (
0 => '101',
1 => '454075',
2 => '2012-03-03',
3 => '4533',
4 => 'Yes',
),
2 =>
array (
0 => '103',
1 => '786075',
2 => '2012-09-05',
3 => 'null',
4 => 'No',
),
)
);
What I tried(just a attempt to combine data1 and data2) :
$d1=$data['data1'];
$d2=$data['data2'];
if(count($d1)>count($d2))
{
$arr1=array();
$arr2=array();
$arr3=array();
$column1=$d1[0];
$column2=$d2[0];
for($i=1;$i<=(count($d1)-1);$i++)
{
if($i<count($d2))
$arr2[]=array_combine($column2,$d2[$i]);
else
$arr2[]=array_combine($column2,array('0'=>'','1'=>''));
}
for($i=1;$i<=(count($d1)-1);$i++)
{
$arr1[]=array_combine($column1,$d1[$i]);
}
for($i=0;$i<=(count($arr1)-1);$i++)
{
$arr3[]=array_merge($arr1[$i],$arr2[$i]);
}
print_r($arr3);
}
I need help regarding a neat code to combine any number of arrays.
Notice that missing elements should receive a null value.
How do I get the output I have mentioned above?
This splits it into 2 steps, first accumulate all the data by the ID, also all the header columns are collected. Then use this data to create output array with blanks where the data is missing.
Comments in code...
$store = [];
$headers = [];
foreach ( $data as $set ) {
$headerRow = array_shift($set);
// Collect all header columns
$headers = array_merge($headers, $headerRow);
foreach ( $set as $index => $list ){
// Create associative list of data so they can be combined (i.e. ID fields)
$list = array_combine($headerRow, $list);
// Use ID value as key and create if needed
if ( !isset($store[$list["ID"]]) ) {
$store[$list["ID"]] = $list;
}
else {
$store[$list["ID"]] = array_merge($store[$list["ID"]], $list);
}
}
}
$headers = array_unique($headers);
$output = [ 'output' => [$headers]];
// Create template array, so that missing fields will be set to null
$blank = array_fill_keys($headers, null);
foreach ( $store as $dataRow ) {
// Fill in the fields for this ID and then change to numeric keys
$output['output'][] = array_values(array_merge($blank, $dataRow));
}
Having the keys as the first element of array is not good practice - that why keys are for.
I recommend different approach - use array-combine for connect them and use the ID as key:
foreach($data as $v) {
$keys = array_shift($v); // take the keys
foreach($v as &$e) {
$e = array_combine($keys, $e); // combine the keys and the value
// add or append them according the ID
if (!isset($res[$e['ID']])) $res[$e['ID']] = $e;
else $res[$e['ID']] = array_merge($res[$e['ID']], $e);
}
}
Now you can take this - and if you must convert it back to your structure.
Live example: 3v4l
Largely resembling NigelRen's answer, my snippet will group by ID value, apply default null values where appropriate and prepend a row of column names after all other array building is finished.
Code: (Demo)
$result = [];
$uniqueKeys = [];
// group data by ID
foreach ($data as $rows) {
$keyRow = array_shift($rows);
$uniqueKeys += array_combine($keyRow, $keyRow);
foreach ($rows as $row) {
$assoc = array_combine($keyRow, $row);
$result[$assoc['ID']] = ($result[$assoc['ID']] ?? []) + $assoc;
}
}
// apply default null values where missing element and re-index rows
foreach ($result as &$row) {
$row = array_values(
array_replace(
array_fill_keys($uniqueKeys, null),
$row
)
);
}
// prepend row of column names
array_unshift($result, array_values($uniqueKeys));
var_export($result);
Output:
array (
0 =>
array (
0 => 'ID',
1 => 'PinCode',
2 => 'Date',
3 => 'Balance',
4 => 'Active',
),
1 =>
array (
0 => '101',
1 => '454075',
2 => '2012-03-03',
3 => '4533',
4 => 'Yes',
),
2 =>
array (
0 => '103',
1 => '786075',
2 => '2012-09-05',
3 => NULL,
4 => 'No',
),
)

Best way to calculate overall records based on associative data

I'm processing the final results of competitions and its general report on the best trainer and which place the trainer should get.
I have already prepared associative arrays below. The key represents trainer's id and the value represents the number of medals in a category (gold, silver, bronze) that his/her athletes got.
[gold] => Array
(
[777777] => 4
[333333] => 2
[555555] => 1
[999999] => 1
)
[silver] => Array
(
[999999] => 3
[777777] => 3
[333333] => 2
)
[bronze] => Array
(
[333333] => 6
[777777] => 4
[999999] => 2
)
Next array associates trainer's id with its name:
[trainers] => Array
(
[333333] => Trainer 4
[777777] => Trainer 1
[999999] => Trainer 2
[555555] => Trainer 3
)
I have stuck processing the data above into final results like this. Any ideas on how it could be done elegantly? The problem is that the data is never constant and the size of the array is always different.
Any help would be greatly appreciated.
Here is code sample:
$gold, $silver, $bronze, $trainers are arrays with information you provided.
$out = [];
foreach($trainers as $trainerId=> $trainerName){
$out[] = array(
'id'=>$trainerId,
'name'=>$trainerName,
'gold'=>isset($gold[$trainerId])?$gold[$trainerId]:0,
'silver'=>isset($silver[$trainerId])?$silver[$trainerId]:0,
'bronze'=>isset($bronze[$trainerId])?$bronze[$trainerId]:0,
);
}
uasort($out, function($a, $b){
// Here: sort by your algorithm. Here is example:
if($a['gold'] != $b['gold']){
return $b['gold'] - $a['gold'];
}
if($a['silver'] != $b['silver']){
return $b['silver'] - $a['silver'];
}
return $b['bronze'] - $a['bronze'];
});
$placeId = 1;
foreach($out as &$info){
$info['place'] = $placeId++;
}
unset($info);
foreach($out as $info){
echo "{$info['place']} place goes to - {$info['name']} ({$info['id']}) as he/she got {$info['gold']} gold medals, {$info['silver']} silver and {$info['bronze']} bronze";
}
Here is another way to do it with metrics:
$gold = array
(
'777777' => 4,
'333333' => 2,
'555555' => 1,
'999999' => 1
);
$silver = array
(
'999999' => 3,
'777777' => 3,
'333333' => 2
);
$bronze = array
(
'333333' => 6,
'777777' => 4,
'999999' => 2
);
$trainers = array
(
'333333' => 'Trainer 4',
'777777' => 'Trainer 1',
'999999' => 'Trainer 2',
'555555' => 'Trainer 3'
);
$metrics = [
'gold' => 3,
'silver'=> 2,
'bronze' => 1];
$results = [];
foreach ($metrics as $arrName => $metric)
{
foreach (${$arrName} as $trainerId => $medals)
{
$results[$trainerId] = ( isset($results[$trainerId]) ) ? $results[$trainerId]+$medals * $metric : $medals * $metric;
}
}
// sorting scores (by value)
arsort($results);
// print scores
var_dump($results);
// print final results
$placeOut = '';
foreach ($results as $trainerId => $score) {
$placeOut .= $trainers[$trainerId].": he/she has ";
foreach ($metrics as $medalName => $metric) {
$placeOut .= (${$medalName}[$trainerId] > 0 ? ${$medalName}[$trainerId] : 0)." ".$medalName.", ";
}
$placeOut .= "\n";
}
echo "<pre>".$placeOut."</pre>";
?>

multidimensional array from nested array

I have a script that goes through a CSV file and puts each row as an array into another array (nested array?). Each row has 2-3 fields for the category of the item in that row. I'm trying to work through how to create a multidimensional array out of these categories. Here is the source I currently have:
$csv = new File_CSV_DataSource;
if ($csv->load($file)) {
$items = $csv->getHeaders();
$csv->getColumn($items[2]);
if ($csv->isSymmetric()) {
$items = $csv->connect();
} else {
$items = $csv->getAsymmetricRows();
}
$items = $csv->getrawArray();
}
$mainCats = array();
$subCats = array();
$subSubs = array();
foreach($items as $item){
if(!in_array($item[10], $mainCats)){
$mainCats[] = $item[10];
}
}
foreach($items as $item){
if(!array_key_exists($item[11], $subCats)){
$parent = array_search($item[10], $mainCats);
$subCats[$item[11]] = $parent;
}
}
foreach($items as $item){
if(!array_key_exists($item[12], $subSubs)){
$parent = array_search($item[11], array_keys($subCats));
$subSubs[$item[12]] = $parent;
}
}
What this does so far is create 3 arrays with the format of:
$mainCats = Array(
[0] => Main Cat 1,
[1] => Main Cat 2,
[2] => Main Cat 3
);
$subCats = Array(
[Sub Cat 1] => 0,
[Sub Cat 2] => 1,
[Sub Cat 3] => 2
);
$subSubs = Array(
[Sub Sub 1] => 0,
[Sub Sub 2] => 1,
[Sub Sub 3] => 2
);
The numeric values of each of the last 2 arrays are the index of their parent category in the previous array. What I would like to do is to merge them all into one large array in the format of:
$cats = Array(
[0] => Array(
'name' => Main Cat 1,
'subs' => Array(
[0] => Array(
'name' => Sub Cat 1,
'subs' => Array(
'name' => Sub Sub 1
)
)
)
),
[1] => Array(
'name' => Main Cat 2,
'subs' => Array(
[0] => Array(
'name' => Sub Cat 2,
'subs' => Array(
'name' => Sub Sub 2
)
)
)
),
[2] => Array(
'name' => Main Cat 3,
'subs' => Array(
[0] => Array(
'name' => Sub Cat 3,
'subs' => Array(
'name' => Sub Sub 3
)
)
)
),
);
I know there has to be a far more efficient way of doing this, but I can't figure it out.
EDIT - I should also mention that not all rows have a 3rd category field value.
I prefer to index them by name:
$cats=array();
//--------
foreach($items as $item){
$main=$item[10];
$subCat=$item[11];
$subSub[$item[12]];
$cats[$main]['subs'][$subCat]['subsubs'][$subSub]['name']=$subSub;
$cats[$main]['subs'][$subCat]['name']=$subCat;
$cats[$main]['name']=$main;
}

PHP - Convert flat array of data to layered multidimensional array

Here is my data returned from a CMS function:
array (
0 =>
stdClass::__set_state(array(
'term_id' => '31',
'parent' => '26'
)),
1 =>
stdClass::__set_state(array(
'term_id' => '3',
'parent' => '0'
)),
2 =>
stdClass::__set_state(array(
'term_id' => '32',
'parent' => '26'
)),
3 =>
stdClass::__set_state(array(
'term_id' => '33',
'parent' => '26'
)),
4 =>
stdClass::__set_state(array(
'term_id' => '34',
'parent' => '26'
)),
5 =>
stdClass::__set_state(array(
'term_id' => '26',
'parent' => '3'
)),
I need to convert the above to the following format:
Array
(
[0] => Array
(
3
)
[1] => Array
(
26
)
[2] => Array
(
31
32
33
34
)
)
So let me explain. Each item in the source is a term. Each term has an ID (term_id) and a parent (parent). If the parent == 0 it doesn't have a parent and is level 0. Any child items of level 0 are level 1 and so on.
What I want to return is an array that holds all the levels and its ID's at that level.
Be aware that this is just sample data and there could be many more levels and any number of ID's on each level.
So using PHP how do I achieve what I'm after?
Assuming valid structure (no orphans and such), can loop through data and pluck each next level, until no more data left:
$output = array();
$current = array( 0 );
$index = 0;
while ( ! empty( $data ) ) {
$parents = $current;
$current = array();
$output[$index] = array();
foreach ( $data as $key => $term ) {
if ( in_array( $term->parent, $parents ) ) {
$output[$index][] = $term->term_id;
$current[] = $term->term_id;
unset( $data[$key] );
}
}
$index ++;
}
var_dump( $output );
Whipped this together real quick, kind of dirty but I think it's what you need.
http://snipt.org/vagU0
I think you need more data. For example objects with parent = 26 are at level 2. How do you now that? You need to have another array where you will have parent_id and its level. Then you can iterate through array and build what you want. If this is multidemnsional array you can use array_walk_recursive

Categories