How to sort a multidimensional array by multiple columns? - php

I'm trying to do the same as MySQL query
SELECT * FROM table ORDER BY field1, field2, ...
but with php and a multidimensional array:
$Test = array(
array("a"=>"004", "n"=>"03"),
array("a"=>"003", "n"=>"02"),
array("a"=>"001", "n"=>"02"),
array("a"=>"005", "n"=>"01"),
array("a"=>"001", "n"=>"01"),
array("a"=>"004", "n"=>"02"),
array("a"=>"003", "n"=>"01"),
array("a"=>"004", "n"=>"01")
);
function msort(&$array, $keys){
array_reverse($keys);
foreach($keys as $key){
uasort($array, sortByKey);
}
//
function sortByKey($A, $B){
global $key;
$a = $A[$key];
$b = $B[$key];
if($a==$b) return 0;
return ($a < $b)? -1 : 1 ;
}
}
//
msort($Test, array("a","n"));
//
foreach($Test as $t){
echo('<p>'.$t["a"].'-'.$t["n"].'</p>');
}
My theory is: if I sort multiple times on columns with "lesser importance" then columns of "greater importance", I'll achieve an order like the above MySQL query.
Unfortunately, php is returning:
Warning: uasort() expects parameter 2 to be a valid callback, function 'sortByKey' not found or invalid function name in /Library/WebServer/Documents/www/teste.array_sort.php on line 23" (uasort line)
It's a simple order function. What am I missing?

Fundamentally we're going to use the same approach as explained here, we're just going to do it with a variable number of keys:
/**
* Returns a comparison function to sort by $cmp
* over multiple keys. First argument is the comparison
* function, all following arguments are the keys to
* sort by.
*/
function createMultiKeyCmpFunc($cmp, $key /* , keys... */) {
$keys = func_get_args();
array_shift($keys);
return function (array $a, array $b) use ($cmp, $keys) {
return array_reduce($keys, function ($result, $key) use ($cmp, $a, $b) {
return $result ?: call_user_func($cmp, $a[$key], $b[$key]);
});
};
}
usort($array, createMultiKeyCmpFunc('strcmp', 'foo', 'bar', 'baz'));
// or
usort($array, createMultiKeyCmpFunc(function ($a, $b) { return $a - $b; }, 'foo', 'bar', 'baz'));
That's about equivalent to an SQL ORDER BY foo, bar, baz.
If of course each key requires a different kind of comparison logic and you cannot use a general strcmp or - for all keys, you're back to the same code as explained here.

Here's some code I wrote to do something similar:
uasort($array,function($a,$b) {
return strcmp($a['launch'],$b['launch'])
?: strcmp($a['tld'],$b['tld'])
?: strcmp($a['sld'],$b['sld']);
});
It kind of abuses the fact that negative numbers are truthy (only zero is falsy) to first compare launch, then tld, then sld. You should be able to adapt this to your needs easily enough.

PHP7.4's arrow syntax eliminates much of the code bloat which was previously necessary to bring your column orders into the usort() scope.
Code: (Demo)
$orderBy = ['a', 'n'];
usort($Test, fn($a, $b) =>
array_map(fn($v) => $a[$v], $orderBy)
<=>
array_map(fn($v) => $b[$v], $orderBy)
);
var_export($Test);
I reckon this is a very elegant and concise way to script the task. You generate the nominated column values from $a and $b as separate arrays and write the spaceship operator between them.
Without the arrow syntax, the snippet gets a little more chunky.
Code: (Demo)
$orderBy = ['a', 'n'];
usort($Test, function($a, $b) use ($orderBy) {
return
array_map(function($v) use ($a){
return $a[$v];
}, $orderBy)
<=>
array_map(function($v) use ($b){
return $b[$v];
}, $orderBy);
});
var_export($Test);
The spaceship operator will walk through corresponding pairs of data ([0] vs [0], then [1] vs [1], and so on) until it reaches a non-zero evaluation or until it exhausts the comparison arrays.
With fewer total iterated function calls, you could call array_multisort() after preparing flat columns to sort by.
Code: (Demo)
$orderBy = ['a', 'n'];
$params = array_map(fn($colName) => array_column($Test, $colName), $orderBy);
$params[] = &$Test;
array_multisort(...$params);
var_export($Test);

This will do the job, thanks for contributions!
function mdsort(&$array, $keys){
global $KeyOrder;
$KeyOrder = $keys;
uasort($array, cmp);
}
function cmp(array $a, array $b) {
global $KeyOrder;
foreach($KeyOrder as $key){
$res = strcmp($a[$key], $b[$key]);
if($res!=0) break;
}
return $res;
}
//
mdsort($Test, array("a","n"));
This code is a little ugly, though, I believe it can be better - maybe a class to solve the issue of passing the array with the keys to the "cmp" function. But it's a start point, you can use it with any number of keys to sort.

Related

PHP Array Filter on KEYS that pass a certain test

I have an associative PHP array and I would like to generate a list of keys that pass a certain test. For example
$myArray = ('28'=>0.01,'51'=>-0.1,'48'=>0.4,'53'=>-0.3);
And I'd like to filter the keys in the same way I can simply filter the values. So if I filter the values on "return the elements that are bigger than 0.2" would be
print_r(array_filter($myArray,"biggerThanFilter");
with
function biggerThanFilter($v){
return $v>0.2;
}
But how would I apply a filter to the keys which say the "keyValueIsBiggerThan50"
i.e something like this
print_r(array_KEY_filter($myArray,"keyValueIsBiggerThan50");
function keyValueIsBiggerThan50($key){
return $key*1>50;
}
I would loop through the array_keys and unset, personally:
function array_filter_keys($array, callable $fn)
foreach (array_keys($array) as $key) {
if (!$fn($key)) unset($array[$key])
}
return $array;
}
$filtered_array = array_filter_keys($array, function($key) { return $key > 50 });
This assumes PHP >= 5.4
function keyValueIsBiggerThan50 ($myArray) {
$newArray = array();
foreach($myArray as $key => $value){
if($key * 1 > 50){
$newArray[$key] = $value
}
}
return $newArray;
}
to be used like
print_r(keyValueIsBiggerThan50 ($myArray));
Are you looking for this specific case, or a generic?
For PHP 5.6+ my answer to a similar question also applies to this one: use ARRAY_FILTER_USE_KEY
<?php
$myArray = ['28' => 0.01, '51' => -0.1, '48' => 0.4, '53' => -0.3];
$filtered = array_filter(
$myArray,
function ($key) {
return $key > 50;
},
ARRAY_FILTER_USE_KEY
);
?>
For PHP < 5.6, array_diff_ukey performs a custom comparison of two arrays by key and appears to do a full N x M comparison so you can filter a single array by using a dummy as the second array.
Using PHP 5.5.9, I used the following to remove the numeric key elements from an array:
$res = array_diff_ukey($res, array(0), function ($a,$b){ return is_string($a); });

Ordering an array by specific value

I have original array like this:
$arr=array(10,8,13,8,5,10,10,12);
and want to sort so finally become:
$arr=array(10,10,10,8,8,13,12,5);
To get final result i using this way:
$arr=array(10,8,13,8,5,10,10,12);
// Remove array that contain array(1)
$arr_diff=array_diff(array_count_values($arr), array(1));
// Remove array that contain keys of $arr_diff
$arr=array_diff($arr, array_keys($arr_diff));
// Sort
rsort($arr);
ksort($arr_diff);
// unshift
foreach ($arr_diff as $k=>$v)
{
while ($v > 0)
{
array_unshift($arr,$k);
$v--;
}
}
// print_r($arr);
My question is there an another more simple way?
Thanks.
$occurrences = array_count_values($arr);
usort($arr, function ($a, $b) use ($occurrences) {
// if the occurrence is equal (== 0), return value difference instead
return ($occurrences[$b] - $occurrences[$a]) ?: ($b - $a);
});
See Reference: all basic ways to sort arrays and data in PHP.

Preserve key order (stable sort) when sorting with PHP's uasort

This question is actually inspired from another one here on SO and I wanted to expand it a bit.
Having an associative array in PHP is it possible to sort its values, but where the values are equal to preserve the original key order, using one (or more) of PHP's built in sort function?
Here is a script I used to test possible solutions (haven't found any):
<?php
header('Content-type: text/plain');
for($i=0;$i<10;$i++){
$arr['key-'.$i] = rand(1,5)*10;
}
uasort($arr, function($a, $b){
// sort condition may go here //
// Tried: return ($a == $b)?1:($a - $b); //
// Tried: return $a >= $b; //
});
print_r($arr);
?>
Pitfall: Because the keys are ordered in the original array, please don't be tempted to suggest any sorting by key to restore to the original order. I made the example with them ordered to be easier to visually check their order in the output.
Since PHP does not support stable sort after PHP 4.1.0, you need to write your own function.
This seems to do what you're asking: http://www.php.net/manual/en/function.usort.php#38827
As the manual says, "If two members compare as equal, their order in the sorted array is undefined." This means that the sort used is not "stable" and may change the order of elements that compare equal.
Sometimes you really do need a stable sort. For example, if you sort a list by one field, then sort it again by another field, but don't want to lose the ordering from the previous field. In that case it is better to use usort with a comparison function that takes both fields into account, but if you can't do that then use the function below. It is a merge sort, which is guaranteed O(n*log(n)) complexity, which means it stays reasonably fast even when you use larger lists (unlike bubblesort and insertion sort, which are O(n^2)).
<?php
function mergesort(&$array, $cmp_function = 'strcmp') {
// Arrays of size < 2 require no action.
if (count($array) < 2) return;
// Split the array in half
$halfway = count($array) / 2;
$array1 = array_slice($array, 0, $halfway);
$array2 = array_slice($array, $halfway);
// Recurse to sort the two halves
mergesort($array1, $cmp_function);
mergesort($array2, $cmp_function);
// If all of $array1 is <= all of $array2, just append them.
if (call_user_func($cmp_function, end($array1), $array2[0]) < 1) {
$array = array_merge($array1, $array2);
return;
}
// Merge the two sorted arrays into a single sorted array
$array = array();
$ptr1 = $ptr2 = 0;
while ($ptr1 < count($array1) && $ptr2 < count($array2)) {
if (call_user_func($cmp_function, $array1[$ptr1], $array2[$ptr2]) < 1) {
$array[] = $array1[$ptr1++];
}
else {
$array[] = $array2[$ptr2++];
}
}
// Merge the remainder
while ($ptr1 < count($array1)) $array[] = $array1[$ptr1++];
while ($ptr2 < count($array2)) $array[] = $array2[$ptr2++];
return;
}
?>
Also, you may find this forum thread interesting.
array_multisort comes in handy, just use an ordered range as second array ($order is just temporary, it serves to order the equivalent items of the first array in its original order):
$a = [
"key-0" => 5,
"key-99" => 3,
"key-2" => 3,
"key-3" => 7
];
$order = range(1,count($a));
array_multisort($a, SORT_ASC, $order, SORT_ASC);
var_dump($a);
Output
array(4) {
["key-99"]=>
int(3)
["key-2"]=>
int(3)
["key-0"]=>
int(5)
["key-3"]=>
int(7)
}
I used test data with not-ordered keys to demonstrate that it works correctly. Nonetheless, here is the output your test script:
Array
(
[key-1] => 10
[key-4] => 10
[key-5] => 20
[key-8] => 20
[key-6] => 30
[key-9] => 30
[key-2] => 40
[key-0] => 50
[key-3] => 50
[key-7] => 50
)
Downside
It only works with predefined comparisons, you cannot use your own comparison function. The possible values (second parameter of array_multisort()) are:
Sorting type flags:
SORT_ASC - sort items ascendingly.
SORT_DESC - sort items descendingly.
SORT_REGULAR - compare items normally (don't change types)
SORT_NUMERIC - compare items numerically
SORT_STRING - compare items as strings
SORT_LOCALE_STRING - compare items as strings, based on the current locale. It uses the locale, which can be changed using
setlocale()
SORT_NATURAL - compare items as strings using "natural ordering" like natsort()
SORT_FLAG_CASE - can be combined (bitwise OR) with SORT_STRING or SORT_NATURAL to sort strings case-insensitively
For completeness sake, you should also check out the Schwartzian transform:
// decorate step
$key = 0;
foreach ($arr as &$item) {
$item = array($item, $key++); // add array index as secondary sort key
}
// sort step
asort($arr); // sort it
// undecorate step
foreach ($arr as &$item) {
$item = $item[0]; // remove decoration from previous step
}
The default sort algorithm of PHP works fine with arrays, because of this:
array(1, 0) < array(2, 0); // true
array(1, 1) < array(1, 2); // true
If you want to use your own sorting criteria you can use uasort() as well:
// each parameter is an array with two elements
// [0] - the original item
// [1] - the array key
function mysort($a, $b)
{
if ($a[0] != $b[0]) {
return $a[0] < $b[0] ? -1 : 1;
} else {
// $a[0] == $b[0], sort on key
return $a[1] < $b[1] ? -1 : 1; // ASC
}
}
This is a solution using which you can achieve stable sort in usort function
public function sortBy(array &$array, $value_compare_func)
{
$index = 0;
foreach ($array as &$item) {
$item = array($index++, $item);
}
$result = usort($array, function($a, $b) use ($value_compare_func) {
$result = call_user_func($value_compare_func, $a[1], $b[1]);
return $result == 0 ? $a[0] - $b[0] : $result;
});
foreach ($array as &$item) {
$item = $item[1];
}
return $result;
}
Just to complete the responses with some very specific case. If the array keys of $array are the default one, then a simple array_values(asort($array)) is sufficient (here for example in ascending order)
As a workaround for stable sort:
<?php
header('Content-type: text/plain');
for ($i = 0;$i < 10;$i++)
{
$arr['key-' . $i] = rand(1, 5) * 10;
}
uksort($arr, function ($a, $b) use ($arr)
{
if ($arr[$a] === $arr[$b]) return array_search($a, array_keys($arr)) - array_search($b, array_keys($arr));
return $arr[$a] - $arr[$b];
});
print_r($arr);

PHP Sorting a multi-dimensional array by field name

I have tried adapting this code to use to sort a multidimensional array on a named key/field. The field is an integer what I need to sort smallest to biggest.
function myCmp($a, $b)
{
return strcmp($a["days"], $b["days"]);
}
uasort($myArray, "myCmp");
This sorts the arrays as I need but in the wrong order. At the moment it sorts biggest to smallest, not using natural order. I need to sort smallest to biggest in natural order (eg 2 comes before 5, 12 and 24).
strnatcmp() is your friend
e.g. (using a php 5.3 closure/anonymous function):
<?php
$myArray = array( 'foo'=>array('days'=>2), 'bar'=>array('days'=>22), 'ham'=>array('days'=>5), 'egg'=>array('days'=>12) );
uasort($myArray, function($a, $b) { return strnatcmp($a["days"], $b["days"]); });
foreach($myArray as $k=>$v) {
echo $k, '=>', $v['days'], "\n";
}
prints
foo=>2
ham=>5
egg=>12
bar=>22
You can just reverse the parameters of strcmp :
function myCmp($a, $b)
{
return strcmp($b["days"], $a["days"]);
}
uasort($myArray, "myCmp");
Since you want to sort in natural order you should not be using strcmp, you can do:
function myCmp($a, $b)
{
if ($a['days'] == $b['days']) return 0;
return ($b['days'] > $a['days']) ? -1 : 1;
}
Here is a working example.

Return a single Element from an Associative Array from a Function

Lets assume we have two PHP-Arrays:
$some_array = array('a','b','c','d','e','f');
$another_array = array(101,102,103,104,105,106);
In PHP, there are already some Array-Functions that allow the construction of an Associative Array (AKA hash), e,g,;
$hash = array_combine(
$some_array,
$another_array
);
And here it comes. What should I do if I want to create a hash in a more functional style, if I want to compute key and value on the fly and build the hash through a map-operation, like (not working):
# wishful thinking
$hash = array_map(
function($a, $b){ return ($a => $b); },
$some_array,
$another_array
);
The problem here seems to be the expectation, the line
...
function($a, $b){ return ($a => $b); }
...
would indeed return an key value/pair of a hash - which it doesn't.
Q: How can I return a key/value pair from a function - which can be used to build up an associative array?
Addendum
To make clear what I really was looking for, I'll provide a perl example of hash generation:
...
# we have one array of characters (on which our hash generation is based)
my #array = qw{ a b c d e f };
# now *generate* a hash with array contents as keys and
# ascii numbers as values in a *single operation *
# (in Perl, the $_ variable represents the actual array element)
my %hash = map +($_ => ord $_), #array;
...
Result (%hash):
a => 97
b => 98
c => 99
d => 100
e => 101
f => 102
From the responses, I'd now think this is impossible in PHP. Thanks to all respondends.
EDIT: It's not entirely clear whether you're having a problem merely with returning multiple variables from a function, or whether you're having problems storing a function in an array. Your post gives the impression that storing the function in the array works, so I'll tackle the return-multiple-variables problem.
There is no way to return a single instance of a key/value pair in PHP. You have to have them in an array... but remember that in PHP, an array and hashmap are exactly the same thing. It's weird (and controversial), but that means it's perfectly legitimate to return an array/hashmap with the multiple values you wish to return.
There are only two sane ways that I know (from 10+ years of PHP experience) to get more than one variable out of a function. One is the good'ol fashion way of making the input variable changeable.
function vacuumPackSandwitch(&$a, &$b) {
$a = 505;
$b = 707;
}
This will change both $a and $b as opposed to changing copies of them like usual. For example:
$a = 1;
$b = 2;
vacuumPackSandwitch($a, $b);
print $a.' '.$b;
This will return "505 707", not "1 2" like normally. You might have to do:
vacuumPackSandwitch(&$a, &$b);
But if that's the case, the PHP compiler will duly let you know.
The other way is to return an array, which I suppose is the clearer and preferred way.
function ($a, $b) {
return array($a, $b);
}
You can grab both variables at the same time by doing:
list($c, $d) = vacuumPackSandwitch($a, $b);
Hope it helps!
In PHP arrays can be associative too! An int-indexed array is just an associative array with 0 => element0 and so on. Thus, you can accomplish what you are looking for like this:
$somearray = array('name' => 'test', 'pass' => '***', 'group' => 'admin');
function myfunc($a, $b) { return array($a => $b); }
if($somearray['name'] == 'test') { echo 'it works!'; }
// Let's add more data with the function now
$somearray += myfunc('location', 'internet');
//Test the result
if($somearray['location'] == 'internet') { echo 'it works again!'; }
It is really very simple. Hope this helps.
I know that this is an old question, but google still likes it for a search I recently made, so I'll post my findings. There are two ways to do this that come close to what you're attempting, both relying on the same general idea.
Idea 1:
Instead of returning a key => value pair, we return an array with only one element, 'key => value', for each sequential element of the original arrays. Then, we reduce these arrays, merging at every step.
$array = array_map(
function($a, $b){
return array($a => $b);
},
$arr1,
$arr2
);
$array = array_reduce(
$array,
function($carry, $element){
$carry = array_merge($carry, $element);
return $carry;
},
array()
);
OR
Idea 2:
Similar to idea one, but we do the key => value assignment in array_reduce. We pass NULL to array_map, which creates an array of arrays (http://php.net/manual/en/function.array-map.php)
$array = array_map(NULL, $a, $b);
$array = array_reduce(
$array,
function($carry, $element){
$carry[$element[0]] = $element[1];
return $carry;
},
array()
);
Personally, I find Idea 2 to be a lot more elegant than Idea 1, though it requires knowing that passing NULL as the function to array_map creates an array of arrays and is therefore somewhat un-intuitive. I just think of it as a precursor to array_reduce, where all the business happens.
Idea 3:
$carry = array();
$uselessArray = array_map(
function($a, $b) use ($carry){
$carry[$a] = $b;
},
$a,
$b
);
Idea 3 is an alternative to Idea 2, but I think it's hackier than Idea 2. We have to use 'use' to jump out of the function's scope, which is pretty ugly and probably contrary to the functional style OP was seeking.
Lets just streamline Idea 2 a little and see how that looks:
Idea 2(b):
$array = array_reduce(
array_map(NULL, $a, $b),
function($carry, $element){
$carry[$element[0]] = $element[1];
return $carry;
},
array()
);
Yeah, that's nice.

Categories