Unwanted behavior of PHP usort - php

I have a problem with PHP usort(). Let's suppose I have an array like this (that's a simplification, I've not to work with names, and I have an array of objects, not arrays):
$data = array(
array('name' => 'Albert', 'last' => 'Einstein'),
array('name' => 'Lieserl', 'last' => 'Einstein'),
array('name' => 'Alan', 'last' => 'Turing' ),
array('name' => 'Mileva', 'last' => 'Einstein'),
array('name' => 'Hans Albert', 'last' => 'Einstein')
);
As you can see, the array is sorted arbitrarily.
Now, if want to sort it by last, I do:
function sort_some_people($a, $b) { return strcmp($a['last'], $b['last']); }
usort($data, 'sort_some_people');
And I have:
Array (
[0] => Array ( [name] => Mileva [last] => Einstein )
[3] => Array ( [name] => Albert [last] => Einstein )
[1] => Array ( [name] => Lieserl [last] => Einstein )
[2] => Array ( [name] => Hans Albert [last] => Einstein )
[4] => Array ( [name] => Alan [last] => Turing )
)
That is ok, now they are sorted by last. But, as you can see, I've just completely lost the previous sorting. What am I saying? I want to preserve array sorting as it was before, but as a secondary sorting. I hope I was clear. Practically I want to sort data using something as usort() (so, a completely custom sorting), but if sorting field is identical between two items, I want to keep their relative position as it was before. Considering the given example, I want Lieserl Einstein to appear before Mileva Einstein because it was like this at beginning.

The sorting algorithms used in PHP have this property where the order is undefined if the items match.
If you need to keep the order, then you have to roll your own code.
Fortunately some one already has:
http://www.php.net/manual/en/function.usort.php#38827
$data = array(
array('name' => 'Albert', 'last' => 'Einstein'),
array('name' => 'Lieserl', 'last' => 'Einstein'),
array('name' => 'Alan', 'last' => 'Turing' ),
array('name' => 'Mileva', 'last' => 'Einstein'),
array('name' => 'Hans Albert', 'last' => 'Einstein')
);
function sort_some_people($a, $b) {
return strcmp($a['last'], $b['last']);
}
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;
}
mergesort($data, 'sort_some_people');
print_r($data);
Output:
Array
(
[0] => Array
(
[name] => Albert
[last] => Einstein
)
[1] => Array
(
[name] => Lieserl
[last] => Einstein
)
[2] => Array
(
[name] => Mileva
[last] => Einstein
)
[3] => Array
(
[name] => Hans Albert
[last] => Einstein
)
[4] => Array
(
[name] => Alan
[last] => Turing
)
)
Voila!

$compare = strcmp($a['last'], $b['last']);
if ($compare == 0)
$compare = strcmp($a['name'], $b['name']);
return $compare

You're asking for a stable sorting algorithm, which php doesn't offer. Beware that even if it may appear stable sometimes, there's no guarantee of it, and so likely to misbehave given certain inputs.
If you know the sorting criteria of the other columns, you can resort it all at once to get the desired behavior. array_multisort does this. The following is a bit more powerful way in that the comparison logic is completely user defined.
// should behave similar to sql "order by last, first"
$comparatorSequence = array(
function($a, $b) {
return strcmp($a['last'], $b['last']);
}
, function($a, $b) {
return strcmp($a['first'], $b['first']);
}
// more functions as needed
);
usort($theArray, function($a, $b) use ($comparatorSequence) {
foreach ($comparatorSequence as $cmpFn) {
$diff = call_user_func($cmpFn, $a, $b);
if ($diff !== 0) {
return $diff;
}
}
return 0;
});
If you really need a stable sort because the existing order of elements isnt well defined, try writing your own sort. bubble sort for example is very easy to write. This is a good solution so long as the number of elements in your lists are relatively small, otherwise look to implement one of the other stable sorting algorithms.

Try this:
function sort_some_people($a, $b)
{
$compareValue = 10 * strcmp($a['last'], $b['last']);
$compareValue += 1 * strcmp($a['name'], $b['name']);
return $compareValue;
}
Sample: http://codepad.org/zkHviVBM
This functions uses each digit of the decimal system for a sort criteria. The one that comes first has the highest digit. Might not be the smartest, but works for me.

Related

Find the difference from two arrays in php

--$arr1----
Array
(
[0] => Array
(
[id] => 1
[Name] => AAA
)
[1] => Array
(
[id] => 6
[Name] => BBB
)
)
--$arr2---
Array
(
[0] => Array
(
[id] => 1
[Name] => AAA
)
[1] => Array
(
[id] => 6
[Name] => BBB
)
[2] => Array
(
[id] => 46
[Name] => CCC
)
)
I would like the final result as following. Is there anyone can help me?
--Final Result--
Array
(
[0] => Array
(
[id] => 46
[Name] => CCC
)
)
UPDATE---
In this case, the result of array_diff($arr1,$arr2) is empty.
The easiest way is Mark Baker's solution or just write your own simple function:
Code:
function arrdiff($a1, $a2) {
$res = array();
foreach($a2 as $a) if (array_search($a, $a1) === false) $res[] = $a;
return $res;
}
print_r(arrdiff($arr1, $arr2));
Output:
Array
(
[0] => Array
(
[id] => 46
[name] => CCC
)
)
$arr1 = array(array('id' => 1, 'Name' => 'AAA'),
array('id' => 6, 'Name' => 'BBB')
);
$arr2 = array(array('id' => 1, 'Name' => 'AAA'),
array('id' => 6, 'Name' => 'BBB'),
array('id' => 46, 'Name' => 'CCC')
);
$results = array_diff(array_map('serialize',$arr2),array_map('serialize',$arr1));
$results = array_map('unserialize',$results);
var_dump($results);
EDIT
And just for the sheer fun of it, you could use array_filter() instead of array_diff() - which means no need to serialize anything at all
$results = array_filter($arr2, function ($value) use($arr1) { return !in_array($value,$arr1); } );
var_dump($results);
You should use array_diff():
$finalResult = array_diff($arr2, $arr1);
If you need more complex comparison you may also build foreach loop and use like this:
function compareItems( $a, $b){
return $a['id'] == $b['id']; // Example compare criteria
}
$result = array();
foreach( $arr1 as $item){
foreach( $arr2 as $key => $it){
if( !compareItems( $item, $it)){
$result[] = $it; // Either build new array
unset( $arr2[$key]); // Or remove items from original array
break;
}
}
}
And than you'll probably want to implement the same with reversed order of $arr1 and $arr2.
You can solve this with array_udiff()
function arr_comp($a, $b)
{
if ($a['id'] == $b['id'])
return 0;
else if ($a['id'] > $b['id'])
return 1;
else
return -1;
}
$result = array_udiff($arr2, $arr1, 'arr_comp');
or if you don't know in which array the differences may be you can try:
$res1 = array_udiff($arr1, $arr2, 'arr_comp');
$res2 = array_udiff($arr2, $arr1, 'arr_comp');
$result = array_merge($res1, $res2);
$arrDif=array();
$i=0;
foreach($arr1 as $value)
{
if(!in_array($value, $arr2))
{
$arrDif[$i]=$value;
$i++;
}
}
Take a look at the PHP built-in function array_diff. That'll help you out :-) Just pass your two arrays, and store the array returned by array_diff(), which will contain the differences between the two arrays.
As you're using multi-dimensional arrays, look at this comment on the PHP website: http://www.php.net/manual/en/function.array-diff.php#98680.

Sorting on 2 columns in PHP without having to redo MySQL query

I have a $result variable in PHP from a MySQL query & I would like to sort it (with PHP) on 2 different columns.
I am doing this anytime a user clicks a different sort method, so I don't want to have to redo the MySQL query with ORDER BY ...., .... each time.
It would probably be more efficient to have MySQL do the ordering for you, but if you want to do it in PHP, check out usort which will let you sort an array using a callback function to decide the ordering.
If your $result is an array of associative database rows, here's a very simple example of using usort to sort on a string column 'foo' ...
//sort on string column
function sortStringFoo($a, $b)
{
return strcmp($a['foo'], $b['foo']);
}
usort($result, 'sortStringFoo');
Here's how you'd do a numeric sort...
//sort on numeric column
function sortNumericBar($a, $b)
{
if ($a['bar'] == $b['bar']) {
return 0;
}
return ($a['bar'] < $b['bar']) ? -1 : 1;
}
usort($result, 'sortNumericBar');
That illustrates the basics of how to use usort, but how might we sort on several columns at once? Here's how we could combine our comparison callbacks to illustrate this by sorting on 'foo' and then 'bar'...
function sortFooBar($a,$b)
{
$order=sortStringFoo($a,$b);
if ($order==0)
$order=sortNumericBar($a,$b);
return $order;
}
usort($result, 'sortFooBar');
You could unroll sortFooBar into a single function, but this illustrates the technique: compare first column, if same, compare second column, and so on.
You can achieve this by splitting all the columns of your table into separate arrays, then doing an array_multisort (notice example #3). Here is a test case:
<?php
$arr = array(
array(
'name' => 'Maria',
'surname' => 'White',
'age' => 24
),
array(
'name' => 'John',
'surname' => 'Brown',
'age' => 32
),
array(
'name' => 'John',
'surname' => 'Brown',
'age' => 33
)
);
$names = array();
$surnames = array();
$ages = array();
foreach($arr as $key => $row){
$names[$key] = $row['name'];
$surnames[$key] = $row['surnames'];
$ages[$key] = $row['age'];
}
// perform any sorting task you like
array_multisort($names, SORT_ASC, $surnames, SORT_ASC, $ages, SORT_DESC, $arr);
print_r($arr);
?>
The output will be:
Array
(
[0] => Array
(
[name] => John
[surname] => Brown
[age] => 33
)
[1] => Array
(
[name] => John
[surname] => Brown
[age] => 32
)
[2] => Array
(
[name] => Maria
[surname] => White
[age] => 24
)
)
If you split your columns into arrays, you have the power to sort by any column on any direction.

ordering array in specific order [duplicate]

This question already has answers here:
Sort multidimensional array by multiple columns
(8 answers)
Closed 5 months ago.
I have following array
Array
(
[0] => Array
(
[label] => Germany
[conversions] => 1
)
[1] => Array
(
[label] => United States
[conversions] => 8
)
[2] => Array
(
[label] => France
[conversions] => 1
)
[3] => Array
(
[label] => China
[conversions] => 1
)
[4] => Array
(
[label] => Philippines
[conversions] => 1
)
[5] => Array
(
[label] => Turkey
[conversions] => 1
)
)
I want to order following array, first by conversions (desc), then by label (asc)
output will have following sequence:
United States
China
France
Germany
Philippines
Turkey
If using >= PHP 5.3...
usort($arr, function($a, $b) {
$diff = $b['conversions'] - $a['conversions'];
if ( ! $diff) {
return strcmp($a['label'], $b['label']);
}
return $diff;
});
It is relatively simple to make it work with < PHP 5.3.
You need to use PHP's usort() function. This allows you to write a function which determines the sort order, so you can sort things into any order you like.
The function will be called repeatedly by the usort() algorithm, and will give you two parameters, being the two elements of the array that it wants to sort at any given moment. Your function should return -1 if the first of those two elements is bigger, +1 if it the second is bigger, and zero if they are to be considered the same for the purposes of the sort.
See the PHP manual page for more info and examples: http://php.net/manual/en/function.usort.php
I preferred array_multisort PHP Manual in my answer below as you can specify the sort order with parameters.
Next to flexibility it should be faster than using usort which has the problem that it's not really parametrized for the sort order, so not re-inventing the wheel as well.
For more comfort, wrap it up into a function to specify the keys as strings (Demo):
$sorted = $multisortByKey($array, 'conversions', SORT_DESC, 'label', SORT_ASC);
as in:
$array = array(
0 => array(
'label' => 'Germany',
'conversions' => 1,
),
1 => array(
'label' => 'United States',
'conversions' => 8,
),
2 => array(
'label' => 'France',
'conversions' => 1,
),
3 => array(
'label' => 'China',
'conversions' => 1,
),
4 => array(
'label' => 'Philippines',
'conversions' => 1,
),
5 => array(
'label' => 'Turkey',
'conversions' => 1,
),
);
$multisortByKey = function(array $a) {
$args = func_get_args();
$a = array_shift($args);
$extract = function($k) use($a) {return array_map(function($v) use($k) { return $v[$k]; }, $a); };
# NOTE: The following check/for-loop is not entirely correct
# as multiple sort parameters per entry are allowed. I leave this
# for practice.
$c = count($args);
if(!$c%2) throw new InvalidArgumentException('Parameter count mismatch');
for($i=0;$i<$c;$i+=2)
$args[$i] = $extract($args[$i]);
$args[] = &$a;
call_user_func_array('array_multisort', $args);
return $a;
};
$sorted = $multisortByKey($array, 'conversions', SORT_DESC, 'label', SORT_ASC);
var_dump($sorted);
try this
$myArray="your array";
<?php
foreach($myArray as $c=>$key) {
$sort_conv[$c] = $key['conversions'];
$sort_lable[$c] = $key['label'];
}
array_multisort($sort_conv, SORT_ASC, $sort_lable, SORT_STRING, $myArray);
print_r($myArray);
?>
You can use usort to provide your own sorting function
usort($a, function($x, $y) {
return $y["conversions"] < $x["conversions"] ? 1 :
($x["conversions"] < $y["conversions"] ? -1 :
strcmp($x["label"], $y["label"]))
);
});
Try the following: (didn't test it, source: PHP Example #3)
foreach ($data as $key => $row) {
$label[$key] = $row['label'];
$conversion[$key] = $row['conversion'];
}
array_multisort($conversion, SORT_DESC, $label, SORT_ASC, $data);

How do I sort a PHP array by an element nested inside? [duplicate]

This question already has answers here:
PHP Sort a multidimensional array by element containing Y-m-d H:i:s date
(11 answers)
Closed 11 months ago.
I have an array like the following:
Array
(
[0] => Array
(
'name' => "Friday"
'weight' => 6
)
[1] => Array
(
'name' => "Monday"
'weight' => 2
)
)
I would like to grab the last values in that array (the 'weight'), and use that to sort the main array elements. So, in this array, I'd want to sort it so the 'Monday' element appears before the 'Friday' element.
You can use usort as:
function cmp($a, $b) {
return $a['weight'] - $b['weight'];
}
usort($arr,"cmp");
Can be done using an anonymous function.
Also if your 'weight' is a string use one of the other returns (see the commented out lines):
<?php
$arr = array(
0 => array (
'name' => 'Friday',
'weight' => 6,
),
1 => array (
'name' => 'Monday',
'weight' => 2,
),
);
// sort by 'weight'
usort($arr, function($a, $b) { // anonymous function
// compare numbers only
return $a['weight'] - $b['weight'];
// compare numbers or strings
//return strcmp($a['weight'], $b['weight']);
// compare numbers or strings non-case-sensitive
//return strcmp(strtoupper($a['weight']), strtoupper($b['weight']));
});
var_export($arr);
/*
array (
0 => array (
'name' => 'Monday',
'weight' => 2,
),
1 => array (
'name' => 'Friday',
'weight' => 6,
),
)
*/
You can also use an anonymous function.
usort($items, function($a, $b) {
return $a['name'] > $b['name'];
});
Agree with usort, I also sometimes use array_multisort (http://ca2.php.net/manual/en/function.usort.php) example 3, sorting database results.
You could do something like:
<?php
$days = array(
array('name' => 'Friday', 'weight' => 6),
array('name' => 'Monday', 'weight' => 2),
);
$weight = array();
foreach($days as $k => $d) {
$weight[$k] = $d['weight'];
}
print_r($days);
array_multisort($weight, SORT_ASC, $days);
print_r($days);
?>
Output:
Array
(
[0] => Array
(
[name] => Friday
[weight] => 6
)
[1] => Array
(
[name] => Monday
[weight] => 2
)
)
Array
(
[0] => Array
(
[name] => Monday
[weight] => 2
)
[1] => Array
(
[name] => Friday
[weight] => 6
)
)
If the filed you sort by is string like title name,
array_multisort + Flags for Natural Sorting and CaseInSensitivity are the way to go:
$sort_by_title = array();
foreach($items as $item) {
$sort_by_title [] = $item['title'];
}
array_multisort($sort_by_title , SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $items );
NOTE, if the element you are sorting on is a float like .0534 and .0353 (like for a percentage), then you have to multiply both by 1000. not sure why frankly... it appears that usort seems to compare the integer values.
took me awhile to figure that one out...
and 2 tips that may not be immediately obvious:
if your arrays are objects, you can use return $a->weight - $b->weight of course
if you return $b->weight - $a->weight, it will sort desending.
Here's a cool function that might help:
function subval_sort($a,$subkey,$sort) {
foreach($a as $k=>$v) {
$b[$k] = strtolower($v[$subkey]);
}
if($b)
{
$sort($b);
foreach($b as $key=>$val) {
$c[] = $a[$key];
}
return $c;
}
}
Send in the array as $a the key as $subkey and 'asort' or 'sort' for the $sort variable

Intersecting multidimensional array of varying size

I have a multidimensional array that looks like this:
Array
(
[0] => Array
(
[0] => Array
(
[id] => 3
)
[1] => Array
(
[id] => 1
)
[2] => Array
(
[id] => 2
)
[3] => Array
(
[id] => 5
)
[4] => Array
(
[id] => 4
)
)
[1] => Array
(
[0] => Array
(
[id] => 1
)
[1] => Array
(
[id] => 3
)
[2] => Array
(
[id] => 4
)
[3] => Array
(
[id] => 5
)
)
[2] => Array
(
[0] => Array
(
[id] => 3
)
)
)
I need to find a way to return the intersecting value(s). In this case that would be
[id] => 3
The length of the array could be different, so I can't just use array_intersect().
This would be simple if your arrays contained only integers, but as they contain an another array, it gets a bit more complicated. But this should do it:
function custom_intersect($arrays) {
$comp = array_shift($arrays);
$values = array();
// The other arrays are compared to the first array:
// Get all the values from the first array for comparison
foreach($comp as $k => $v) {
// Set amount of matches for value to 1.
$values[$v['id']] = 1;
}
// Loop through the other arrays
foreach($arrays as $array) {
// Loop through every value in array
foreach($array as $k => $v) {
// If the current ID exists in the compare array
if(isset($values[$v['id']])) {
// Increase the amount of matches
$values[$v['id']]++;
}
}
}
$result = array();
// The amount of matches for certain value must be
// equal to the number of arrays passed, that's how
// we know the value is present in all arrays.
$n = count($arrays) + 1;
foreach($values as $k => $v) {
if($v == $n) {
// The value was found in all arrays,
// thus it's in the intersection
$result[] = $v;
}
}
return $result;
}
Usage:
$arrays = array(
array(array('id' => 3), array('id' => 1), array('id' => 2), array('id' => 5), array('id' => 4)),
array(array('id' => 1), array('id' => 3), array('id' => 4), array('id' => 5)),
array(array('id' => 3))
);
print_r(custom_intersect($arrays));
Result:
Array
(
[0] => 3
)
This function isn't perfect: if you have duplicate ID's in one array, it will not work. That would require a bit more code to first make the array values unique, but this will probably work in your case.
You can use array_uintersect() to get the intersection of arrays using a custom comparison function. You have to call it with call_user_func_array() though, as it expects each array as a separate argument:
//build list of parameters for array_uintersect()
$params = array_merge($input, array('compare_func'));
$result = call_user_func_array('array_uintersect', $params);
function compare_func($a, $b) {
return $a['id'] - $b['id'];
}
You can't simply call array_intersect() with call_user_func_array(), because it seems to compare arrays by comparing their string representation (which will always be 'Array').
$a = array(4,3);
$b = array(4,3,2,1);
$c = array(1,2,3,4,5);
$d = array(5,4,3);
$array = array($a,$b,$c,$d);
for ($i=0; $i<count($array); $i++){
if ($i==0){
$array2 = $array[$i];
} else {
$array2 = array_intersect($array2, $array[$i]);
}
}
print_r($array2);`
Result:
3,4
As mentioned in one of the comments of the php.net website (array_intersect function).
$a = array(1,2,3,4,5,2,6,1); /* repeated elements --> $a is not a set */
$b = array(0,2,4,6,8,5,7,9,2,1); /* repeated elements --> $b is not a set */
$ua = array_merge(array_unique($a)); /* now, $a is a set */
$ub = array_merge(array_unique($b)); /* now, $b is a set */
$intersect = array_merge(array_intersect($ua,$ub));
This will return this array:
Array
(
[0] => 1
[1] => 2
[2] => 4
[3] => 5
[4] => 6
)

Categories