I am trying to calculate and store the "Sum values from children to their parent in a variable dimension json/array".
We should start with the children with the lowest level, sum their values and store it in their parent. Move up one level, repeat the calculation, and so on.
Here is an example of given array (the "value" is "weight"):
[
"weight" => 0,
"children" => [
[
"weight" => 10
],
[
"weight" => 0,
"children" => [
[
"weight" => 60,
"children" => [
"weight" => 100
]
]
]
]
]
]
I would like to be able to dynamically calculate it like this :
[
"weight" => 110,
"children" => [
[
"weight" => 10
],
[
"weight" => 100,
"children" => [
[
"weight" => 100,
"children" => [
"weight" => 100
]
]
]
]
]
]
Do you have an idea ?
Thanks !!
If you want to update parent nodes with child nodes, then you must use a OOP concept. Because objects can store the pointer of parent node. Then you can easily update the parent node by using this pointer.
Define a two classes for Nodes and Node groups. Then you can implement your logics in these classes.
<?php
$arr = [
"weight" => 0,
"children" => [
[
"weight" => 10
],
[
"weight" => 0,
"children" => [
[
"weight" => 60,
"children" => [
"weight" => 100
]
]
]
]
]
];
interface CalculateWeight {
function calculateWeight();
}
class Node implements CalculateWeight {
public $weight = 0;
/** #var Group **/
public $parent = null;
public function __construct(?self $parent){
$this->parent = $parent;
}
function calculateWeight() {
if($this->parent){
$this->parent->plusWeight($this->weight);
}
}
public function setWeight(int $weight){
$this->weight = $weight;
}
// Creating nodes from an array
public static function fromArray(array $node, ?Node $parent=null): Node{
$weight = $node["weight"];
if(isset($node["children"])){
$group = new Group($parent);
// Checking the weather it has one child or multiple children
if(isset($node["children"][0])){
foreach($node["children"] as $node){
$group->addChild( Node::fromArray($node, $group));
}
} else {
$group->addChild(Node::fromArray($node["children"], $group));
}
return $group;
} else {
$node = new Node($parent);
$node->setWeight($weight);
return $node;
}
}
public function toArray(){
return ["weight"=> $this->weight];
}
}
class Group extends Node implements CalculateWeight {
public $childs = [];
/** #var Group **/
public $parent = null;
public function __construct(?self $parent){
$this->parent = $parent;
}
public function plusWeight(int $weight){
$this->weight += $weight;
}
public function addChild(Node $child){
$this->childs[] = $child;
}
public function calculateWeight(){
// Calculate children weights
foreach($this->childs as $child){
$child->calculateWeight();
}
// Updating calculated weight to parent node
if($this->parent){
$this->parent->plusWeight($this->weight);
}
}
public function toArray(){
return [
"weight"=> $this->weight,
"children"=> array_map(function($node){return $node->toArray();}, $this->childs)
];
}
}
$parentNode = Node::fromArray($arr,null);
$parentNode->calculateWeight();
var_dump($parentNode->toArray());
Related
I want to convert all of my static data to collection in Laravel.
This is my data:
static $menu_list = [
[
'path' => 'admin/report/transaction',
'active' => 'admin/report',
'name' => 'Report',
'icon' => 'file-text',
'children' => [
'path' => 'admin/report/transaction',
'active' => 'admin/report/transaction',
'name' => 'Transaction',
],
],
];
This function converts my data to array:
public static function menuList()
{
$menu_list = collect(self::$menu_list)->map(function ($voucher) {
return (object) $voucher;
});
}
but function above can only convert main of array, it can't convert children => [...] to collection.
You need a recursive call.
public static function convertToCollection()
{
$menu_list = self::menuList(self::$menu_list);
}
public static function menuList($list)
{
return collect($list)->map(function ($voucher) {
if(is_array($voucher)) {
return self::menuList($voucher)
}
return $voucher;
});
}
You need to use collect() inside map() again:
public static function menuList()
{
$menu_list = collect(self::$menu_list)->map(function ($voucher) {
return (object) array_merge($voucher, [
'children' => collect($voucher['children'])
]);
});
}
Just add a small code peace to your approach.
$menu_list = collect(self::$menu_list)->map(function ($voucher) {
$voucher['children'] = (object) $voucher['children'];
return (object) $voucher;
});
Output
Illuminate\Support\Collection {#574 ▼
#items: array:1 [▼
0 => {#573 ▼
+"path": "admin/report/transaction"
+"active": "admin/report"
+"name": "Report"
+"icon": "file-text"
+"children": {#567 ▼
+"path": "admin/report/transaction"
+"active": "admin/report/transaction"
+"name": "Transaction"
}
}
]
}
I'm trying to test a function which walks through a class, takes the public properties and makes an object with it.
The non public properties are ignored in the output.
So, I mock the class that will be processed and add it some properties.
This is my code:
class GetSettingsTest extends TestCase
{
public function getExpected() {
return (object) [
"one" => (object) [
"oneOne" => "1.1",
"oneTwo" => "1.2",
"oneThree" => (object) [
"oneThreeOne" => "1.3.1",
"oneThreeTwo" => "1.3.2",
]
],
"two" => (object) [
"twoOne" => "2.1",
"twoTwo" => "2.2",
"twoThree" => (object) [
"twoThreeOne" => "1.3.1",
"twoThreeTwo" => "1.3.2",
]
],
"three" => (object) [
"threeOne" => "3.1",
"threeTwo" => "3.2"
]
// four is not here : it is protected or private.
];
}
public function getSettingsMock() {
$stub = $this->getMockBuilder('FakeSettingsClass')
->disableOriginalConstructor()
->getMock();
$stub->one = (array) [
"oneOne" => "1.1",
"oneTwo" => "1.2",
"oneThree" => (array) [
"oneThreeOne" => "1.3.1",
"oneThreeTwo" => "1.3.2",
]
];
$stub->two = (array) [// provide an array, must return an object
"twoOne" => "2.1",
"twoTwo" => "2.2",
"twoThree" => (object) [// provide an object, must return an object
"twoThreeOne" => "1.3.1",
"twoThreeTwo" => "1.3.2",
]
];
$stub->three = (array) [
"threeOne" => "3.1",
"threeTwo" => "3.2"
];
$stub->four = (array) [
// I want this to be protected or private to be not present in the output.
"fourOne" => "4.1",
"fourTwo" => "4.2"
];
return $stub;
}
public function testGetSettings() {
$expected = $this->getExpected();
$getSettings = new GetSettings($this->getSettingsMock());
$value = $getSettings->getSettings();
$this->assertEquals($expected, $value);
}
}
The function works well with a var_dump, it ignores non-public values as expected.
The test works without the non-public part, but I want to test it with the non-public part.
I can't figure how to test the non-public part in PhHPUnit.
Probably by setting a protected value in the getSettingMock function but how can I do that?
Here is a solution based on xmike's comment and with the Phpunit doc here : https://phpunit.readthedocs.io/en/9.0/fixtures.html.
make a fixture class like this :
class GetSettingsFixture
{
public array $one = [
"oneOne" => "1.1",
"oneTwo" => "1.2",
"oneThree" => [
"oneThreeOne" => "1.3.1",
"oneThreeTwo" => "1.3.2",
]
];
public array $two = [
"twoOne" => "2.1",
"twoTwo" => "2.2",
"twoThree" => [
"twoThreeOne" => "1.3.1",
"twoThreeTwo" => "1.3.2",
]
];
public array $three = [
"threeOne" => "3.1",
"threeTwo" => "3.2"
];
public string $four = "a string";
private array $five = [ // this should be ignored in the output.
"fiveOne" => "5.1",
"fiveTwo" => "5.2"
];
protected array $six = [ // this should be ignored in the output.
"sixOne" => "6.1",
"sixTwo" => "6.2"
];
public function testFunction() { // this should be ignored in the output.
return "something";
}
}
And this test pass :
class GetSettingsTest extends TestCase
{
private GetSettingsFixture $given;
public function setUp(): void {
// this function is executed before test.
$this->given = new GetSettingsFixture(); // this call the fixture class.
}
public function tearDown(): void {
// this function is executed after the test.
unset($this->given);
}
public function getExpected() {
return (object) [
"one" => (object) [
"oneOne" => "1.1",
"oneTwo" => "1.2",
"oneThree" => (object) [
"oneThreeOne" => "1.3.1",
"oneThreeTwo" => "1.3.2",
]
],
"two" => (object) [
"twoOne" => "2.1",
"twoTwo" => "2.2",
"twoThree" => (object) [
"twoThreeOne" => "1.3.1",
"twoThreeTwo" => "1.3.2",
]
],
"three" => (object) [
"threeOne" => "3.1",
"threeTwo" => "3.2"
],
"four" => "a string"
// five, six are not here : it is protected or private.
// testFunction is hot here too, it's not a property.
];
}
public function testGetSettings() {
$expected = $this->getExpected();
$getSettings = new GetSettings($this->given);
$value = $getSettings->getSettings();
$this->assertEquals($expected, $value);
}
}
So, basically what i'm trying to do is getting combinations of the array children, for example i got the array:
[
"name" => "Item1",
"children" =>
[
"name" => "Item2",
"children" => [
["name" => "Item3"],
["name" => "Item4"]
]
],
["name" => "Item5"]
];
I tried to work with some functions i got on the stackoverflow, but i only got it to work with all of them at once, i was getting just
[
"Item4" => "Item1/Item2/Item4",
"Item5" => "Item1/Item5"
];
The output should be
[
"Item1" => "Item1",
"Item2" => "Item1/Item2",
"Item3" => "Item1/Item2/Item3"
"Item4" => "Item1/Item2/Item4"
"Item5" => "Item1/Item5"
];
As asked, the function i was working with before:
function flatten($arr) {
$lst = [];
/* Iterate over each item at the current level */
foreach ($arr as $key => $item) {
/* Get the "prefix" of the URL */
$prefix = $item['slug'];
/* Check if it has children */
if (array_key_exists('children', $item) and sizeof($item['children'])) {
/* Get the suffixes recursively */
$suffixes = flatten($item['children']);
/* Add it to the current prefix */
foreach($suffixes as $suffix) {
$url = $prefix . '/' . $suffix;
$lst[$item['id']] = $url;
}
} else {
/* If there are no children, just add the
* current prefix to the list */
$lst[$item['id']] = $prefix;
}
}
return $lst;
}
I've had to fix the data as the levels of data don't match up. The rest of the code is new as I found so many errors from your existing code.
Comments in code...
$data = [
"name" => "Item1",
"children" =>
[[
"name" => "Item2",
"children" =>[
["name" => "Item3"],
["name" => "Item4"]]
],
["name" => "Item5"]]
];
print_r(flatten($data));
function flatten($arr, $pathSoFar = '') {
$lst = [];
$path = $pathSoFar."/";
foreach ( $arr as $key => $value ) {
if ( $key === 'name' ) {
// Add name of current level onto path
$path .= $value;
$lst[$value] = $path;
}
else if ( $key === 'children' ) {
//Process child elements recursively and add into current array
$lst = array_merge($lst, flatten($value, $path));
}
else {
// This is for sub-elements which probably are (for example) 0, 1
// (removing trailing / to stop multiples)
$lst = array_merge($lst, flatten($value, rtrim($path,"/")));
}
}
return $lst;
}
I'd like to sort the following associative array:
$tree = [
"id" => 245974,
"children" => [
[
"id" => 111
],
[
"id" => 245982,
"children" => [
[
"id" => 246093,
"children" => [
[
"id" => 225892
],
[
"id" => 225893
],
[
"id" => 225902
]
]
]
]
]
]
];
Desired sort order after the "search value" of id => 225902:
[
"id" => 245974,
"children" => [
[
"id" => 245982, // <-- this is moved up
"children" => [
[
"id" => 246093,
"children" => [
[
"id" => 225902 // <-- this is moved up
],
[
"id" => 225892
],
[
"id" => 225893
]
]
]
]
],
[
"id" => 111
]
]
];
What I've tried:
<?php
$category_id = 225902;
function custom_sort(&$a, &$b) {
global $category_id;
if ($a['id'] === $category_id) {
return -1;
}
if ($b['id'] === $category_id) {
return 1;
}
if (array_key_exists('children', $a)) {
if (usort($a['children'], "custom_sort")) {
return -1;
}
}
if (array_key_exists('children', $b)) {
if (usort($b['children'], "custom_sort")) {
return 1;
}
}
return 0;
}
function reorder_tree($tree) {
usort($tree['children'], "custom_sort");
return $tree;
}
echo "<pre>";
var_dump(reorder_tree($tree));
echo "</pre>";
However, that returns:
[
"id" => 245974,
"children" => [
[
"id" => 245982, // <- this is moved up
"children" => [
[
"id" => 246093,
"children" => [
[
"id" => 225892
],
[
"id" => 225893
],
[
"id" => 225902 // <- this is *not* moved up
]
]
]
]
],
[
"id" => 111
],
]
];
How would I be able to also sort the children arrays?
Great attempt and very much on the right track. The problem with recursion in the comparator is that usort will not call the comparator function when the array length is 1, so whether or not you explore the whole tree is at the whim of usort. This will abandon id => 245982's branch of the tree.
The solution is to avoid recursing in the usort's comparator function directly. Rather, use a regular recursive function that calls usort as needed, namely, the current array or a child array contains the target id. I use a separate array to keep track of which elements should be moved forward, but you can break out of the loop and splice/unshift a single element to the front if you prefer.
We can also make $category_id a parameter to the function.
Here's one approach:
function reorder_tree_r(&$children, $target) {
$order = [];
$should_sort = false;
foreach ($children as $i => &$child) {
$order[$i] = false;
if (array_key_exists("children", $child) &&
reorder_tree_r($child["children"], $target) ||
$child["id"] === $target) {
$order[$i] = true;
$should_sort = true;
}
}
if ($should_sort) {
$priority = [];
$non_priority = [];
for ($i = 0; $i < count($children); $i++) {
if ($order[$i]) {
$priority[]= $children[$i];
}
else {
$non_priority[]= $children[$i];
}
}
$children = array_merge($priority, $non_priority);
}
return $should_sort;
}
function reorder_tree($tree, $target) {
if (!$tree || !array_key_exists("children", $tree)) {
return $tree;
}
reorder_tree_r($tree["children"], $target);
return $tree;
}
var_export(reorder_tree($tree, 225902));
Output:
array (
'id' => 245974,
'children' =>
array (
0 =>
array (
'id' => 245982,
'children' =>
array (
0 =>
array (
'id' => 246093,
'children' =>
array (
0 =>
array (
'id' => 225902,
),
1 =>
array (
'id' => 225892,
),
2 =>
array (
'id' => 225893,
),
),
),
),
),
1 =>
array (
'id' => 111,
),
),
I have this array where I check if the child as a particular key ("hidden") is true. When it is, I need to append the node's id to the parent's key "contentId".
I have this which does the job but only for the first item in an array. I suspect the recursion is somehow broken by the return statement. Any ideas? Perhaps I'm missing out on a array_walk_recursive solution?
function bubbleUp(&$tree){
for ($i=0; $i < count($tree); $i++){
if ( isset($tree[$i]['children']) && is_array($tree[$i]['children']) ) {
$tree[$i]['contentId'] = [ $tree[$i]['id'] ];
array_push($tree[$i]['contentId'], bubbleUp($tree[$i]['children']));
} else {
return reportBack($tree[$i]);
}
}
return $tree;
}
function reportBack($node){
if ( $node['hidden'] ) {
return $node['id'];
} else {
return '';
}
}
$tree = [
[
"name" => "Intro",
"id" => 123,
"hidden" => false,
"children" => [[
"name" => "foo",
"id" => 452,
"hidden" => true,
"children" => [
[
"name" => "bar",
"id" => 982,
"hidden" => true,
],
[
"name" => "gru",
"id" => 239,
"hidden" => true,
]
]
]]
]
];
bubbleUp($tree);
echo '<pre><small>'; print_r($tree); echo '</small></pre>';
The end result is meant to be:
$tree = [
[
"name" => "Intro",
"id" => 123,
"hidden" => false,
"children" => [[
"name" => "foo",
"id" => 452,
"hidden" => true,
"children" => [
[
"name" => "bar",
"id" => 982,
"hidden" => true,
],
[
"name" => "gru",
"id" => 239,
"hidden" => true,
]
],
"contentId" => [452, 982, 239]
]],
"contentId" => [123, 452, 982, 239]
]
];
About to head home from work, so i'll try and return to this when I get home, but I tried an array_walk solution, had a fair bit of fun trying to do it in the process. Although I don't believe it's working exactly as you want it just yet, but it may act as a guide for anyone else who looks over the question prior to me getting home.
function recursiveSearch(&$value) {
if (isset($value['children'])) {
foreach ($value['children'] as &$child) {
$id = recursiveSearch($child);
$value['contentID'][] = $id;
}
} else {
if (isset($value['hidden']) && $value['hidden'] == true) {
return $value['id'];
}
}
}
array_walk($tree, 'recursiveSearch');
To re-iterate, this solution is not yet complete, but I have to travel home from work and this may help someone else answer, or if not remind me when I get home to come back to this :p
Here's a link to it in action so far: https://ideone.com/RHql33
Main part that makes this difficult is that you want the content ID to be appended to different places dependent on the parents access level.
I.e. if the parent is false, then the immediate node should append the content ID in that branch, but all sub-nodes below it should also append to that location. (Hopefully that makes sense, i'm terrible at talking about this sort of stuff haha o_o)
Thanks to Virtual Pigeon's answer, which led to this:
function recursiveSearch(&$value) {
$value['contentID'][] = $value['id'];
if (isset($value['children'])) {
foreach ($value['children'] as &$child) {
$id = recursiveSearch($child);
if ( is_array($id) ) {
$value['contentID'] = array_merge($value['contentID'], $id);
} else {
$value['contentID'][] = $id;
}
}
return $value['contentID'];
} else {
if (isset($value['hidden']) && $value['hidden'] == true) {
return $value['id'];
}
}
}