reducing an array into another array - php

Inside a laravel blade template, I am trying to reduce an array like this:
$longList = [['box' => 1, 'kg' => 2], ['box' => 2, 'kg' => 2], ['box' => 3, 'kg' => 3]];
into something like this:
$reducedList = [['count' => 2, 'kg' => 2], ['count' => 1, 'kg' => 3]];
This is what I have so far:
#php
$variableWeights = isset($sale->variable_weight_json) ? collect(json_decode($sale->variable_weight_json, true)) : null;
$groups = array();
if (isset($variableWeights)) {
$groups = $variableWeights->reduce(function($carry, $item) {
$index = array_search($item['kg'], array_column($carry, 'weight'));
if (isset($index)) {
$existing = $carry[$index];
array_splice($carry, $index, 1, [
'count' => $existing['count'] + 1,
'weight' => $item['kg']
]);
} else {
array_push($carry, [
'count' => 1,
'weight' => $item['kg'],
]);
}
return $carry;
}, array());
}
#endphp
But it is giving me the error Undefined offset: 0
I am new to php. How should the code be corrected or is there a better approach to achieve the desired result?

Why dont you reduce it to something simpler like this
$reducedList = [2 => 2, 3 => 1]
where the weight is the index and the value is the count.
$reducedList = [];
foreach ($longList as $box) {
if (isset($reducedList[$box['kg']]) {
$reducedList[$box['kg']]++;
} else {
$reducedList[$box['kg']] = 1;
}
}
This way you avoid complexity but you still get the same amount of information.

I guess you can achieve it with code like:
$longList = [['box' => 1, 'kg' => 2], ['box' => 2, 'kg' => 2], ['box' => 3, 'kg' => 3]];
$reducedList = array_values(array_reduce(
$longList,
function($carry, $item) {
if (isset($carry[$item['kg']])) {
++$carry[$item['kg']]['count'];
} else {
$carry[$item['kg']] = ['count' => 1, 'kg' => $item['kg']];
}
return $carry;
},
[]
));
print_r($reducedList);
Here is a working example.

don't use isset() function. it checks only variable existing. use empty() or other condition, it will check variable existing and value. try this.
#php
$variableWeights = isset($sale->variable_weight_json) ? collect(json_decode($sale->variable_weight_json, true)) : null;
$groups = array();
if ($variableWeights->isNotEmpty()) {
$groups = $variableWeights->reduce(function($carry, $item) {
$index = array_search($item['kg'], array_column($carry, 'weight'));
if ($index != false ) {
$existing = $carry[$index]?: false;
if ($existing) {
array_splice($carry, $index, 1, [
'count' => $existing['count'] + 1,
'weight' => $item['kg']
]);
}
} else {
array_push($carry, [
'count' => 1,
'weight' => $item['kg'],
]);
}
return $carry;
}, array());
}
#endphp

You can use the sexy countBy() method (combined with the famous map() one) of the Collection class.
Try this:
$longList = [['box' => 1, 'kg' => 2], ['box' => 2, 'kg' => 2], ['box' => 3, 'kg' => 3]];
$shortList = collect($longList)
->countBy('kg')
->map(function ($count, $kg) {
return [
'kg' => $kg,
'count' => $count,
];
});
With that, you'll get this:
dd($shortList);
=> Illuminate\Support\Collection {#3380
all: [
2 => [
"kg" => 2,
"count" => 2,
],
3 => [
"kg" => 3,
"count" => 1,
],
],
}
Here you have a working demo.

isset checks if variable is set (i.e it exists and not NULL). As I (and core developers) can hardly imagine in which cases array_search can return NULL, there's a manual which says:
Returns the key for needle if it is found in the array, FALSE otherwise.
So, you need to check if $index is not false:
$index = array_search($item['kg'], array_column($carry, 'weight'));
// Note that I use `!==` because using `!=` will convert
// `0` index to false, which is not correct in your case
if (false !== $index) {

Related

Filter array with array_walk_recursive but deny specific values

I am trying to filter an array
$array = [
[
'id' => 1,
'some_key' => 'some_value',
'attribute' => [
'id' => 45,
'cat_id' => 1
],
'sub' => [
'id' => 17,
'some_key' => 'some_value',
'attribute' => [
'id' => 47,
'cat_id' => 17
],
],
],
[
'id' => 2,
'some_key' => 'some_value',
'sub' => [
'id' => 19,
'some_key' => 'some_value',
],
]
];
$childArray = [];
array_walk_recursive($array, static function($value, $key) use(&$childArray){
if($key === 'id') {
$childArray[] = $value;
}
});
This returns me an array of all array-fields having id as key.
[1,45,17,47,2,19]
But there is a small problem, some of the array have an key called attribute containing an idkey field that I dont want to have.
[1,17,2,19] //this is what I want
Is there a way to say "don't take the id inside attribute" ?
My current solution, I added a filter before my filter :D
/**
* #param array $array
* #param string $unwanted_key
*/
private function recursive_unset(&$array, $unwanted_key)
{
unset($array[$unwanted_key]);
foreach ($array as &$value) {
if (is_array($value)) {
$this->recursive_unset($value, $unwanted_key);
}
}
}
but this seems like this is not the best practice ^^
You can traverse recursively manually instead of array_walk_recursive and avoid all under attribute key.
<?php
$childArray = [];
function collectIDs($arr,&$childArray){
foreach($arr as $key => $value){
if($key === 'attribute') continue;
if(is_array($value)) collectIDs($value,$childArray);
else if($key === 'id') $childArray[] = $value;
}
}
collectIDs($array,$childArray);
print_r($childArray);
Demo: https://3v4l.org/V6uFf
Find a function that will flatten your array. The result should look like this (I have a class for this):
array (
0 =>
array (
'id' => 1,
'some_key' => "some_value",
'attribute.id' => 45,
'attribute.cat_id' => 1,
'sub.id' => 17,
'sub.some_key' => "some_value",
'sub.attribute.id' => 47,
'sub.attribute.cat_id' => 17,
),
1 =>
array (
'id' => 2,
'some_key' => "some_value",
'sub.id' => 19,
'sub.some_key' => "some_value",
),
)
So you have all keys available and can work with a modified array_walk.
$childArray = [];
array_walk_recursive($data, static function($value, $key) use(&$childArray){
$keys = array_reverse(explode(".",$key));
if($keys[0] === 'id' AND (!isset($keys[1]) OR $keys[1] != 'attribute')) {
$childArray[] = $value;
}
});
The RecursiveArrayIterator class is also very suitable when array values ​​are to be collected depending on keys and values ​​on different levels.
$result = [];
$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
foreach($it as $key => $value) {
$parentLevel = $it->getDepth()-1;
$parentKey = $it->getSubIterator($parentLevel)->key();
if($key === 'id' AND $parentKey !== 'attribute'){
$result[] = $value;
}
}
var_export($result);
//array ( 0 => 1, 1 => 17, 2 => 2, 3 => 19, )
To return the qualifying ids instead of creating a reference variable, merge recursive calls of the function as you iterate.
Code: (Demo)
function getIds($array) {
$ids = [];
foreach ($array as $key => $value) {
if ($key === 'id') {
$ids[] = $value;
} elseif ($key !== 'attribute' && is_array($value)) {
$ids = array_merge($ids, getIds($value));
}
}
return $ids;
}
var_export(getIds($array));
Output:
array (
0 => 1,
1 => 17,
2 => 2,
3 => 19,
)

Combine array of objects using keys

I want to combine two arrays of objects. Let me give you an example:
Example:
// First array:
$array1 = [
{ name => 'Joe', p_id => 1 },
{ name => 'Bob', p_id => 2 },
{ name => 'Sam', p_id => 4 }
]
// Second array:
$array2 = [
{ id => 1, name => 'X' },
{ id => 2, name => 'Y' },
{ id => 4, name => 'Z' }
]
Expected output:
$output = [
{ name => 'Joe + X', id => 1 },
{ name => 'Bob + Y', id => 2 },
{ name => 'Sam + Z', id => 4 }
]
Goal:
I want the fastest possible way to combine the name property in the second array with the name property in the first array.
Note: The p_id property in the first array is the same as the id property in the second array.
What i try:
I've used nested loops that have a very low speed.
array_map is the solution!
Given:
$first = [
{ name => 'Joe', p_id => 1 },
{ name => 'Bob', p_id => 2 },
{ name => 'Sam', p_id => 4 },
];
$second = [
{ id => 1, name => 'X' },
{ id => 2, name => 'Y' },
{ id => 4, name => 'Z' },
];
The solution is just simply:
$result = array_map(
static function (\stdClass $first, \stdClass $second): array {
return [
'name' => $first->name . ' + ' . $second->name,
'id' => $first->p_id,
];
},
$first, $second
);
PS: I assume the objects are \stdClass, replace it by the correct one.
Here is a solution for when the ids of the elements inside the array are not in order. Notice that I have changed the order of $array1. Just a bit better than the regular nested loops, on the loop of the $array2 it will remove the "found" elements to improve speed of the next loop.
From:
// First array:
$array1 = [
(object) ['name' => 'Joe', 'p_id' => 1],
(object) ['name' => 'Sam', 'p_id' => 4],
(object) ['name' => 'Bob', 'p_id' => 2],
];
// Second array:
$array2 = [
(object) ['id' => 1, 'name' => 'X'],
(object) ['id' => 2, 'name' => 'Y'],
(object) ['id' => 4, 'name' => 'Z'],
];
Solution:
$result = [];
foreach ($array1 as $array1Element) {
for ($i=0;$i<count($array2);$i++) {
if ($array1Element->p_id === $array2[$i]->id) {
$array2[$i]->name = $array1Element->name . ' + ' . $array2[$i]->name;
$result[] = $array2[$i];
unset($array2[$i]);
$array2 = array_values($array2);
break;
}
}
}

need a cleaner way to break a tie in php

I have a poll which has 4 choices, sometimes people vote for the same things and choices end up in a tie. I'm looking for a way to break this tie.
The poll variables are always different the below is just an example of how they come. I'm currently doing this with messy if statements and randomizing results for each scenario or one === the other.
<?php
$choice1=4;
$choice2=4;
$choice3=2;
$choice4=4;
if ($choice1==$choice2) {
$a = ['$choice1','$choice2'];
$breaker = $a[mt_rand(0, count($a)-1)];
echo $breaker;
}elseif ($choice1==$choice3) {
$a = ['$choice1','$choice3'];
$breaker = $a[mt_rand(0, count($a)-1)];
echo $breaker;
}elseif ($choice1==$choice4) {
$a = ['$choice1','$choice4'];
$breaker = $a[mt_rand(0, count($a)-1)];
echo $breaker;
}elseif ($choice2==$choice1) {
$a = ['$choice2','$choice1'];
$breaker = $a[mt_rand(0, count($a)-1)];
echo $breaker;
//etc...
// This goes on and on also goes up to 3 way ties and 4 way ties.
This method seems extremely inelegant, also the number of votes is going to increase as more and more users register to vote so it is not scalable at this point any suggestions?
This approach uses an array with two set of sorts.
// As many choices as you would like
$choices = [
['id' => 1, 'name' => 'choice1', 'count' => 4],
['id' => 2, 'name' => 'choice2', 'count' => 4],
['id' => 3, 'name' => 'choice3', 'count' => 2],
['id' => 4, 'name' => 'choice4', 'count' => 4],
['id' => 5, 'name' => 'choice5', 'count' => 4],
['id' => 6, 'name' => 'choice6', 'count' => 4],
['id' => 7, 'name' => 'choice7', 'count' => 4],
['id' => 8, 'name' => 'choice8', 'count' => 6],
['id' => 9, 'name' => 'choice9', 'count' => 7],
['id' => 10, 'name' => 'choice10', 'count' => 4],
['id' => 11, 'name' => 'choice11', 'count' => 6],
['id' => 12, 'name' => 'choice12', 'count' => 6],
['id' => 13, 'name' => 'choice13', 'count' => 7],
['id' => 14, 'name' => 'choice14', 'count' => 3],
['id' => 15, 'name' => 'choice15', 'count' => 4],
];
// First, sort by count
usort($choices, function($a, $b) {
if ( $a['count'] < $b['count'] ) return -1; // a < b
elseif ( $a['count'] > $b['count'] ) return 1; // a > b
return 0;
});
// Now, all are sorted by count.
// Walk through and deal with items that have the same count.
$buf = []; // A temp buffer to keep all items with a certain count
$prevCcount = null;
$output = [];
foreach($choices as $choice) {
$count = $choice['count'];
echo sprintf('id %d count %d', $choice['id'], $choice['count']) . "\n";
if ( $prevCount != $count ) {
// Possible new group of items with the same count
// Does the buffer have more than 1. If so, randomize.
if ( count($buf) > 1 ) {
echo "Shuffling " . count($buf) . "items\n";
$shuffled = shuffle($buf);
if ( ! $shuffled ) {
throw new Exception('Failed to shuffle');
}
}
if ( count($buf) > 0 ) {
$output = array_merge($output, $buf);
$buf = [];
}
}
$prevCount = $count; // Keep track of count of previous item
$buf[] = $choice; // add current item to buffer
}
// Deal with the tail
// There will be 1 or more items still in the buffer that must be dealt with
echo "Final buf has " . count($buf) . " items\n";
if ( count($buf) > 1 ) {
echo "Shuffling " . count($buf) . " items\n";
$shuffled = shuffle($buf);
if ( ! $shuffled ) {
throw new Exception('Failed to shuffle');
}
}
$output = array_merge($output, $buf);
print_r($output);
echo "\n";
You could use array_keys and max to get back which of them has the max score then chose one in that array in your liking
$choice1=4;
$choice2=4;
$choice3=2;
$choice4=4;
$arr = ["choice1" => $choice1,"choice2" => $choice2,"choice3" => $choice3,"choice4" => $choice4];
$winners = array_keys($arr, max($arr));
var_dump($winners);
Output:
array(3) {
[0]=>
string(7) "choice1"
[1]=>
string(7) "choice2"
[2]=>
string(7) "choice4"
}
You can then use your existing method to choose one of the three (in this case).
$breaker = $winners[mt_rand(0, count($winners)-1)];
echo $breaker;

Get Count of particular entity in array inside array?

I have an array named hello as follows,
hello(
0 => ([name => abc, add=>def, city=>ny,phone=>12345]);
1 => ([name => pqr, add=>mno, city=>qw,phone=>67890]);
2 => ([name => abc, add=>def, city=>ny,phone=>14785]);
3 => ([name => ghi, add=>foo, city=>yu,phone=>258]);
4 => ([name => jkl, add=>exy, city=>ny,phone=>95145]);
);
It has few elements and that elements are also array itself, like while retring query result.
I want to count a perticular name that repeats. Like in hello[0] and hello[2], so my ans should be abc=2.
How can I do that?
Thanks.
Edit 2:
Now you want a way to count all of the objects:
$arr = [
0 => (object) ['name' => 'abc', 'add'=>'def', 'city'=>'ny', 'phone'=>'12345'],
1 => (object) ['name' => 'pqr', 'add'=>'mno', 'city'=>'qw', 'phone'=>'67890'],
2 => (object) ['name' => 'abc', 'add'=>'def', 'city'=>'ny', 'phone'=>'14785'],
3 => (object) ['name' => 'ghi', 'add'=>'foo', 'city'=>'yu', 'phone'=>'258'],
4 => (object) ['name' => 'jkl', 'add'=>'exy', 'city'=>'ny', 'phone'=>'95145'],
];
function countBy (callable $f, $xs) {
return array_reduce($xs, function ($acc, $x) use ($f) {
$y = call_user_func($f, $x);
$acc[$y] = isset($acc[$y]) ? $acc[$y] + 1 : 1;
return $acc;
}, []);
}
json_encode(countBy (function ($x) { return $x->name; }, $arr));
// => { "abc": 2, "pqr": 1, "ghi": 1, "jkl": 1 }
Edit:
You lied about the data types so I changed $x['name'] to $x->name and $x[$attr] to $x->{$name}
You can use array_reduce
array_reduce($arr, function ($acc, $x) {
return $x->name === 'abc' ? $acc + 1 : $acc;
}, 0);
// => 2
Or write it as a function
function countAttr($attr, $match, $xs) {
return array_reduce($xs, function ($acc, $x) use ($attr, $match) {
return $x->{$attr} === $match ? $acc + 1 : $acc;
}, 0);
}
countAttr('name', 'abc', $arr)(
// => 2
In this case defining a function would be best so that count for any name cen be get. You can try this -
$array = [
0 => ['name' => 'abc', 'add'=>'def', 'city'=>'ny','phone'=>'12345'],
1 => ['name' => 'pqr', 'add'=>'mno', 'city'=>'qw','phone'=>'67890'],
2 => ['name' => 'abc', 'add'=>'def', 'city'=>'ny','phone'=>'14785'],
3 => ['name' => 'ghi', 'add'=>'foo', 'city'=>'yu','phone'=>'258'],
4 => ['name' => 'jkl', 'add'=>'exy', 'city'=>'ny','phone'=>'95145'],
];
function get_name_count($array, $name)
{
// filter array for name
$temp = array_filter($array, function($a) use($name) {
return ($a['name'] === $name);
});
// return count
return count($temp);
}
var_dump(get_name_count($array, 'abc'));
Output
int(2)
// hello , please use some standard name for array
// code not tested
// define variable if required
$field_name="name";
$value="abc";
$count=0;
echo count_field($field_name,$value);
function count_field($field_name,$value)
{
foreach($hello as $val)
{
if($val[$field_name]==$value)
{
$count++;
}
}
return $count;
}

PHP : multidimensional array merge recursive

I need to merge those two arrays:
$ar1 = array("color" => array("red", "green"), "aa");
$ar2 = array("color" => array( "green", "blue"), "bb");
$result = array_merge_recursive($ar1, $ar2);
Expected output:
[
'color' => [
(int) 0 => 'red',
(int) 1 => 'green',
(int) 3 => 'blue'
],
(int) 0 => 'aa',
(int) 1 => 'bb'
]
But it outputs:
[
'color' => [
(int) 0 => 'red',
(int) 1 => 'green',
(int) 2 => 'green', (!)
(int) 3 => 'blue'
],
(int) 0 => 'aa',
(int) 1 => 'bb'
]
I'm looking for the simplest way to do this, my array inputs won't be deeper than those examples.
Here it is.
function array_merge_recursive_ex(array $array1, array $array2)
{
$merged = $array1;
foreach ($array2 as $key => & $value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = array_merge_recursive_ex($merged[$key], $value);
} else if (is_numeric($key)) {
if (!in_array($value, $merged)) {
$merged[] = $value;
}
} else {
$merged[$key] = $value;
}
}
return $merged;
}
Thanks to Meglio comment, you can use these functions for any number of arrays :
Functions
function drupal_array_merge_deep() {
$args = func_get_args();
return drupal_array_merge_deep_array($args);
}
// source : https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/drupal_array_merge_deep_array/7.x
function drupal_array_merge_deep_array($arrays) {
$result = array();
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// Renumber integer keys as array_merge_recursive() does. Note that PHP
// automatically converts array keys that are integer strings (e.g., '1')
// to integers.
if (is_integer($key)) {
$result[] = $value;
}
elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
$result[$key] = drupal_array_merge_deep_array(array(
$result[$key],
$value,
));
}
else {
$result[$key] = $value;
}
}
}
return $result;
}
Usage
$merged = drupal_array_merge_deep($ar_1, $ar_2);
var_dump($merged);
$merged = drupal_array_merge_deep_array([$ar_1, $ar_2]);
var_dump($merged);
Usage (test data)
$ar_1 = [
"item1" => false,
"item2" => true,
"item_list" => [
"sub_item1" => 5,
"sub_itemlist" => [
"sub_sub_item1" => 27,
],
]
];
$ar_2 = [
"item_list" => [
"sub_item2" => 5,
"sub_itemlist" => [
"sub_sub_item2" => 27,
],
],
"item3" => true,
];
Usage output (same for both functions)
array (size=4)
'item1' => boolean false
'item2' => boolean true
'item_list' =>
array (size=3)
'sub_item1' => int 5
'sub_itemlist' =>
array (size=2)
'sub_sub_item1' => int 27
'sub_sub_item2' => int 27
'sub_item2' => int 5
'item3' => boolean true

Categories