I have an array that contains some other arrays, I will make a sample of the original structure below in order to understand my issue.
$foo = array(
[0] => array('name' => foo, 'offered' => 1)
[1] => array('name' => foo)
[2] => array('name' => foo, 'offered' => 1)
[3] => array('name' => foo, 'offered' => 1)
);
What i want to do is, to sort my array in order to get first every array that contains the key 'offered'. In the example above i need do get this type of order. [0],[2],[3],[1].
You could use uasort() with a custom comparison function.
$foo = array(
array('name' => 'mark', 'offered' => 1),
array('name' => 'joe'),
array('name' => 'bill', 'offered' => 1),
array('name' => 'hugo', 'offered' => 1)
);
uasort($foo, 'customSort');
function customSort($a, $b) {
$a = isset($a['offered']);
$b = isset($b['offered']);
if (($a && $b) || (!$a && !$b)) return 0;
else if ($a && !$b) return -1;
else return 1;
}
print_r($foo);
Please check:
http://php.net/manual/it/function.usort.php
http://php.net/manual/it/function.uasort.php
http://php.net/manual/it/function.uksort.php
Explanation: (a bit simplified for newcomers) When you sort an array with these special sorting functions, you can tell PHP to use a custom function to let it decide whether a value is "less than another" (and return 1) or "greater than" another (and return -1) or "equal to another" (and return 0).
This functions has as parameters ($a, $b) the two array items to compare.
In this case we decide that the ones where "offered" exists are "less than" the others, so they will be sorted first.
I would iterate and check if the key exists, depending on that, rebuild the array.
$final = [];
foreach ($foo as $r) {
if (isset($r['offered']))
array_unshift($final, $r);
else
array_push($final, $r);
}
var_dump($final);
I find the spaceship operator (php7+) to provide a very clean syntax.
Evaluate the offered column from both $a and $b to check if the column is set. If not the isset() false will be treated as 0, otherwise true will be treated as 1.
By writing $b on the left of the operator and $a on the right, DESC sorting is performed -- this positions all 1 (true) evaluations before 0 (false) evaluations.
Code: (Demo)
$array = [
['name' => 'a', 'offered' => 1],
['name' => 'b'],
['name' => 'c', 'offered' => 1],
['name' => 'd', 'offered' => 1],
['name' => 'e']
];
usort($array, function($a, $b) {
return isset($b['offered']) <=> isset($a['offered']);
});
var_export($array);
Output:
array (
0 =>
array (
'name' => 'a',
'offered' => 1,
),
1 =>
array (
'name' => 'c',
'offered' => 1,
),
2 =>
array (
'name' => 'd',
'offered' => 1,
),
3 =>
array (
'name' => 'b',
),
4 =>
array (
'name' => 'e',
),
)
Related
I have the following code:
<?php
$a = [
[
'id' => 20,
'created_at' => '2020-11-22',
'updated_at' => '2020-11-22 11:16:22',
'name' => 'AA',
],
[
'id' => 19,
'created_at' => '2020-11-27 11:16:22',
'updated_at' => null,
'name' => 'BB',
]
];
$b = [
[
'id' => 20,
'created_at' => '2020-11-22 11:16:11',
'updated_at' => '2020-11-22 11:16:22',
'name' => 'AA',
],
[
'id' => 19,
'created_at' => '2020-11-27 11:16:22',
'updated_at' => null,
'name' => 'BB',
]
];
function array_diff_by_keys(array $a, array $b)
{
$compare = function ($x, $y) {
return $x['id'] <=> $y['id'] ?:
$x['created_at'] <=> $y['created_at'] ?:
$x['updated_at'] <=> $y['updated_at']; // how to build dynamic?
};
return array_udiff($a, $b, $compare);
}
array_diff_by_keys($a, $b); // what i've got
array_diff_by_keys($a, $b, ['id', 'created_at', ...]); // what i want
I would like in the $compare function create dynamic condition based on arguments passed to the function. Currently I have predefined keys like (id, created_at etc.), and I would like to be able to decide what arguments are to be included in the Elvis operator
You can iterate over the keys you want to compare on, taking the diff of the corresponding values in $x and $y, and returning that diff if it's non-zero; otherwise moving on to the next key. This function allows you to not specify the desired keys to compare on, if you don't they default to all the keys in each element:
function array_diff_by_keys(array $a, array $b, array $keys = null)
{
if (empty($keys)) $keys = array_keys(reset($a));
$compare = function ($x, $y) use ($keys) {
foreach ($keys as $key) {
$diff = $x[$key] <=> $y[$key];
if ($diff) return $diff;
}
return $diff;
};
return array_udiff($a, $b, $compare);
}
Output (for your sample data):
Array
(
[0] => Array
(
[id] => 20
[created_at] => 2020-11-22
[updated_at] => 2020-11-22 11:16:22
[name] => AA
)
)
Demo on 3v4l.org
This question already has answers here:
Group multidimensional array data based on two column values and sum values of one column in each group
(5 answers)
Closed 7 months ago.
I have an array of associative arrays and I would like to group them by the rows by the values for A, B, and C and sum the count value for each group.
$array = [
['A' => 'O', 'B' => 'O', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'Test', 'C' => 1, 'count' => 1],
['A' => 'O', 'B' => 'O', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'Test', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'test1', 'C' => 2, 'count' => 1],
];
I need a result like this:
[
["A" => "O", "B" => "O", "C" => 1, "count" => 2],
["A" => "Test", "B" => "Test", "C" => 1, "count" => 2],
["A" => "Test", "B" => "test1", "C" => 2, "count" => 1]
]
In order to make this you need to loop through the array and check where the attributes "A", "B", "C" are equal. I tried doing this, but I couldn't fix it.
$countedArray[0] = $array[0];
foreach ($array as $item) {
$occKey = array_filter(
$countedArray,
function ($countedItem, $key) use ($array) {
if ($countedItem['A'] == $item['A']
&& $countedItem['B'] == $item['B']
&& $countedItem['C'] == $item['C']
) {
$countedItem[$key]['count'] = countedItem[$key]['count'] + 1
} else {
array_push(
$countedArray,
[
'A' => $item['A'],
'B' => $item['B'],
'C' => $item['C'],
'count' => 1
]
);
}
},
ARRAY_FILTER_USE_BOTH
);
}
I've done my best to make it less verbose. I welcome any suggestions. Here's my proposed solution:
function sumOccurrences(array $original): array
{
$summed = [];
foreach ($original as $value) {
// here we get the array without the 'count' key - everything we need to compare
$comparisonElement = array_filter($value, function ($key) {
return $key !== 'count';
}, ARRAY_FILTER_USE_KEY);
// we search with strict comparison (third param - true) - see reasoning below
$foundAt = array_search($comparisonElement, array_column($summed, 'element'), true);
if ($foundAt === false) {
// we separate the values we compare and the count for easier handling
$summed[] = ['element' => $comparisonElement, 'count' => $value['count']];
} else {
// if we've run into an existing element, just increase the count
$summed[$foundAt]['count'] += $value['count'];
}
}
// since we separated count from the values for comparison, we have to merge them now
return array_map(function ($a) {
// $a['count'] is wrapped in an array as it's just an integer
return array_merge($a['element'], ['count' => $a['count']]);
}, $summed);
}
In order to make it less verbose, I've opted to compare arrays directly. Other than being less verbose, another benefit is that this will work if additional key => value pairs are introduced to the array without any addition to logic. Everything that is not count gets compared, no matter how many pairs exist. It will also cover any nested arrays (for example 'C' => ['D' => 1]).
But, this comes at a price - we must use strict comparison because loose can give undesired results (for example, ['a'] == [0] will return true). Strict comparison also means that it won't work if any values are objects (strict comparison means it's checking for the same instance) and that arrays will only be matched if they have the same key => value pairs in the same order. This solution assumes that your array (and any nested ones) are already sorted.
If that is not the case, we'd have to sort it before comparing. Normally, ksort would do the job, but to support nested arrays, we'd have to devise a recursive sort by key:
function ksortRecursive(array &$array): void
{
ksort($array);
foreach ($array as &$value) {
if (is_array($value)) {
ksortRecursive($value);
}
}
}
and call it before we do array_search.
Now if we assume a starting array like in your example, the following should give you the desired result:
$original = [
['A' => 'O', 'B' => 'O', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'Test', 'C' => 1, 'count' => 1],
['A' => 'O', 'B' => 'O', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'Test', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'test1', 'C' => 2, 'count' => 1],
];
var_dump(sumOccurrences($original));
The accepted answer is working waaaaay too hard for what is a basic task.
You merely need to use temporary, composite keys (based on the first three elements' values) to form grouped results. When a new row matches a pre-existing group, simply add its count to the stored count for the group. When the loop finishes, call array_values() to re-index the first level of the result array.
Code: (Demo)
$array = [
['A' => 'O', 'B' => 'O', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'Test', 'C' => 1, 'count' => 1],
['A' => 'O', 'B' => 'O', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'Test', 'C' => 1, 'count' => 1],
['A' => 'Test', 'B' => 'test1', 'C' => 2, 'count' => 1],
];
$result = [];
foreach ($array as $row) {
$key = implode('~', array_slice($row, 0, 3));
if (!isset($result[$key])) {
$result[$key] = $row;
} else {
$result[$key]['count'] += $row['count'];
}
}
var_export(array_values($result));
I need a custom array_replace_recursive($array, $array1) method which does what the original array_replace_recursive($array, $array1) method does except that for indexed array it should use the array in the second array and overwrite the first array recursively.
example:
$a = array (
'a' => array(1,2,3),
'b' => array('a' => 1, 'b' => 2, 'c' => 3)
);
$b = array (
'a' => array(4),
'b' => array('d' => 1, 'e' => 2, 'f' => 3)
);
$c = array_replace_recursive($a, $b);
current behaviour:
$c = array (
'a' => array(4,2,3),
'b' => array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 1, 'e' => 2, 'f' => 3)
);
desired behaviour:
$c = array (
'a' => array(4),
'b' => array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 1, 'e' => 2, 'f' => 3)
);
as you can see element 'a' is an indexed array so the element in the second array has overwritten the element in the first array. element 'b' is an associative array so it maintains the original behaviour.
Below worked for me:
<?php
/**
* This method finds all the index arrays in array2 and replaces array in array1. it checks for indexed arrays recursively only within associative arrays.
* #param $array1
* #param $array2
*/
function customMerge(&$array1, &$array2) {
foreach ($array2 as $key => $val) {
if(is_array($val)) {
if(!isAssoc($val)) {
if($array1[$key] != $val) {
$array1[$key] = $val;
}
} else {
$array1_ = &$array1[$key];
$array2_ = &$array2[$key];
customMerge($array1_, $array2_);
}
}
}
}
function isAssoc($arr)
{
return array_keys($arr) !== range(0, count($arr) - 1);
}
$a = array (
'a' => array(1,2,3),
'b' => array('a' => 1, 'b' => 2, 'c' => 3, 'g' => array(
4,5,6
))
);
$b = array (
'a' => array(4),
'b' => array('d' => 1, 'e' => 2, 'f' => 3, 'g' => array(
7
))
);
$c = array_replace_recursive($a, $b); // first apply the original method
$expected = array (
'a' => array(4),
'b' => array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 1, 'e' => 2, 'f' => 3, 'g'=> array(
7
)),
);
$d = $c; // create copy
customMerge($d, $b); // do the custom merge
echo $d == $expected;
The isAssoc() method in the first answer of this post is what your looking for:
How to check if PHP array is associative or sequential?
That method will check if the array is index and return true if it's the case.
$a = array(
0 => array( 'one' => 1, 'two' => 2 ),
1 => array( 'one' => 3, 'two' => 4 ),
2 => array( 'one' => 5, 'two' => 2 )
);
$c = count( $a );
$r = array();
for ( $i = 0; $i < $c; $i++ )
{
if ( $a[$i]['two'] == 2 )
$r[] = $a[$i];
}
Is there a cleaner way then to do all of the above?
Have you tried using array_filter()?
$r = array_filter($a, function($var) {
return ($var['two'] === 2);
});
The output of the above is slightly different than your original code:
Yours:
array(
0 => array('one' => 1, 'two' => 2),
1 => array('one' => 5, 'two' => 2)
)
Using array_filter:
array(
0 => array('one' => 1, 'two' => 2),
2 => array('one' => 5, 'two' => 2) // Note the key is 2, not 1
)
If you need the keys collapsed, you can follow up the array_filter() with array_values() or array_multisort()
You could write a function to do just this and then use array_walk or array_filter but that's about it.
Only way I can see to clean it up more would be to change the original datastructure.
I want to sort arrays by key in php, but the alphabet that I'm using is not the normal English alphabet -- it's a self-created alphabet. Is this possible?
My alphabet is:
$alphabet = "AjawbpfmnrhHxXsSqkgtTdD =";
The array is like this:
Array (
[=k_0] => Array(
[0] => DI.3,2 &dwA-nTr& #Hrw#
[1] => mA
[2] => =k
[3] => Sfj,t
[4] => =k
[5] => pXr
)
[aA_2] => Array(
[0] => DI.7,4 &dwA-nTr& #Hrw-smA-tA,wj#
[1] => snD
[2] => aA
[3] => Sfj,t
[4] => jt
[5] => jt,w
)
[sqA_1] => Array(
[0] => DI.6,18 &dwA-nTr& #nswt#
[1] => ra
[2] => sqA
[3] => Sfj,t
[4] => =s
[5] => r
)
);
So if I sort this array following my alphabet then the array with the key [=k_0] should be at the end.
You can use the usort() function and provide your own sorting logic.
See php.net for an example.
Edit: use uksort, not usort. See http://www.php.net/manual/en/function.uksort.php. Thanks #Darien!
A slightly modified example from php.net - the original code with an $alphabet mapping added:
function cmp($a, $b)
{
// custom sort order - just swapps 2 and 3.
$alphabet = array (1 => 1, 2 => 3, 3 => 2, 4 => 4, 5 => 5, 6=> 6);
if ($alphabet[$a] == $alphabet[$b]) {
return 0;
}
return ($alphabet[$a] < $alphabet[$b]) ? -1 : 1;
}
$a = array(3 => 'c' , 2 => 'b', 5 => 'e', 6 => 'f', 1=>'a');
uksort($a, "cmp");
foreach ($a as $key => $value) {
echo "$key: $value\n";
}
Given your $alphabet = "AjawbpfmnrhHxXsSqkgtTdD";, and assuming that A<j<a, etc, per your comment, transform each key in the alternative alphabet to a series in the known alphabet, e.g. use a mapping like:
your alphabet: AjawbpfmnrhHxXsSqkgtTdD
'real'alphabet: abcdefghijklmnopqrstuvw
So the key 'Ajaw' => 'abcd', and 'fmnr' => 'ghij', etc. This then turns your keys into something you can sort using conventional php functions. You'd need some way to handle characeters not present in your original alphabet though.
Something like that might work - you'd need two transform functions (from your alphabet to 'real' alphabet and vice versa), and then a comparator for e.g. uksort.
My two cents - thanks for clarifying the original question.
Fortunately, your custom alphabet does not have more characters than the list of single-byte latin letters, so translating is a very simple and readable process.
I recommend that you set up a translation array before you begin sorting, then translate/normalize the keys for the purpose of sorting.
Code: (Demo)
$array = [
'=k_0' => ['test1'],
'aA_2' => ['test2'],
'sqA_1' => ['test3'],
'=kj_0' => ['test4'],
'awA_2' => ['test5'],
'= D_1' => ['test6'],
'sq A_1' => ['test7'],
'sqA_2' => ['test8'],
];
$trans = ['AjawbpfmnrhHxXsSqkgtTdD =', 'abcdefghijklmnopqrstuvwxy'];
uksort(
$array,
function ($a, $b) use ($trans) {
return strtr($a, ...$trans) <=> strtr($b, ...$trans);
}
);
var_export($array);
Output:
array (
'aA_2' =>
array (
0 => 'test2',
),
'awA_2' =>
array (
0 => 'test5',
),
'sqA_1' =>
array (
0 => 'test3',
),
'sqA_2' =>
array (
0 => 'test8',
),
'sq A_1' =>
array (
0 => 'test7',
),
'=k_0' =>
array (
0 => 'test1',
),
'=kj_0' =>
array (
0 => 'test4',
),
'= D_1' =>
array (
0 => 'test6',
),
)
From PHP7.4, the syntax can be reduced using arrow function syntax.
uksort(
$array,
fn($a, $b) => strtr($a, ...$trans) <=> strtr($b, ...$trans)
);
After feedback from #mickmackusa I have updated my example to work with uksrot and answer the question fully
$order = str_split("AjawbpfmnrhHxXsSqkgtTdD");
uksort($arr, function ($a, $b) use ($order) {
$posA = array_search($a, $order);
$posB = array_search($b, $order);
return $posA - $posB;
});
http://sandbox.onlinephpfunctions.com/code/9b6f39b30dcc932517bbe82608dd8a0c8d35b3da
-- original response with usort--
You can use usort() with a custom order array like so
$arr = array("w","b","m","n","x","x","z","T","T","A","A");
$order = array("A","j","a","w","b","p","f","m","n","r","h","H","x","X","s","S","q","k","g","t","T","d","D"," ","=");
usort($arr, function ($a, $b) use ($order) {
$posA = array_search($a, $order);
$posB = array_search($b, $order);
return $posA - $posB;
});
(you could probably just explode the $order string so it's nicer)
I actually just came across this answer which explains it better, and handles if a value is not inside the order array.
And a working example http://sandbox.onlinephpfunctions.com/code/3934aafe93377ec18549d326d6551608436242a7
<?php
$arr = [8,10,12,18,20,7,4,6,2,20,0]; //take array
$a= sortasc($arr); // call function
function sortasc($arr){
for($i=0;$i<=count($arr);$i++){
for($j=1;$j<=count($arr)-1;$j++){
if($arr[$j-1]>$arr[$j]){
$temp = $arr[$j];
$arr[$j]= $arr[$j-1];
$arr[$j-1] = $temp;
}
}
}
return $arr;
}
?>
See this code:
<?php
$arr = array('wr' => 1, 'wrS' => 6, 'wr,w' => 3, 'wr.w' => 4, 'wr-qA' => 2, 'wrs' => 5);
function compare_by_alphabet(array $alphabet, $str1, $str2)
{
$l1 = strlen($str1);
$l2 = strlen($str2);
$c = min($l1, $l2);
for ($i = 0; $i < $c; $i++)
{
$s1 = $str1[$i];
$s2 = $str2[$i];
if ($s1===$s2) continue;
$i1 = array_search($s1, $alphabet);
if ($i1===false) continue;
$i2 = array_search($s2, $alphabet);
if ($i2===false) continue;
if ($i2===$i1) continue;
if ($i1 < $i2) return -1;
else return 1;
}
if ($l1 < $l2) return -1;
elseif ($l1 > $l2) return 1;
return 0;
}
function compare_keys_by_alphabet($a, $b)
{
static $alphabet = array('-', ',', '.', 'A', 'j', 'a', 'w', 'b', 'p', 'f', 'm', 'n', 'r', 'h', 'H', 'x', 'X', 's', 'S', 'q', 'k', 'g', 't', 'T', 'd', 'D', '=', '/', '(', ')', '[', ']', '<', '>', '{', '}', '\'', '*', '#', 'I', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, '&', '#');
return compare_by_alphabet($alphabet, $a, $b);
}
uksort($arr, 'compare_keys_by_alphabet');
print_r($arr);
Result:
Array
(
[wr] => 1
[wr-qA] => 2
[wr,w] => 3
[wr.w] => 4
[wrs] => 5
[wrS] => 6
)