I'm having this issue where I want to sorty a multidimensional array based on 2 parameters
I build my array like this:
$teamList[$t['id']] = array(
'id' => $t['id'],
'name' => $t['name'],
'score' => $score,
'points' => $array
);
I then sort like this:
foreach ($teamList as $key => $row) {
$score[$key] = $row['score'];
$points[$key] = $row['points'];
}
array_multisort($score, SORT_DESC, $points, SORT_DESC, $teamList);
But the $teamList remains unsorted?
You can easily use a user defined compare function instead of doing all the copying of values and abusing array_multisort().
function sortByScoreAndPoints($a, $b) {
if ($a['score'] == $b['score']) {
if ($a['points'] == $b['points']) {
return 0;
}
return ($a['points'] > $b['points']) ? -1 : 1;
}
return ($a['score'] > $b['score']) ? -1 : 1;
}
uasort($teamlist, 'sortByScoreAndPoints');
The sort function has to accept two parameters which can have arbitrary names, but $a and $b is used in the docs. During sorting, PHP passes any two values of the array as $a and $b and expects an answer in which order they should appear. Your sort function has to return -1 if $a should appear first, 0 if they are equal, or 1 if $a should appear last, compared to $b.
My code first tests if the scores are equal. If not, the last return will compare which score is higher ( $a > $b ), and the highes score goes into the list first (if a is bigger than b, return -1 to say a goes first).
If the scores are equal, points will be tested. If they are not equal, the comparison takes place again. Otherwise 0 is returned.
Any entry in the team list with equal score and points might appear in arbitrary location in the result (but not random - the same input array will always be sorted the same), because there is no further ordering specified. You might easily extend your sorting by adding another comparison for the name or the id, if you like.
If you want your sorted array to be renumbered starting at 0, use usort() instead of uasort().
Related
I have an array like
$arr = array(
array(
'id'=>'1',
'time'=>'08:00'
),
array(
'id'=>'2',
'time'=>'11:00'
),
array(
'id'=>'3',
'time'=>'14:00'
),
array(
'id'=>'4',
'time'=>'17:00'
)
);
what i want is suppose if the time is 11:30 i want the third array(time->14:00) to come in the first position of the array and the rest to come underneath it. I'm using PHP 5.3 (That's all far i can go with the technology).
The output should be:
array(
array(
'id'=>'1',
'time'=>'14:00'
),
array(
'id'=>'2',
'time'=>'17:00'
)
array(
'id'=>'3',
'time'=>'08:00'
),
array(
'id'=>'4',
'time'=>'11:00'
)
);
You could use this callback to usort:
$t = date("h:m") . "_"; // add a character so it cannot be EQUAL to any time in the input
usort($arr, function ($a, $b) use ($t) {
return strcmp($a["time"],$b["time"]) * strcmp($t,$a["time"]) * strcmp($t,$b["time"]);
});
Explanation
First the current time is retrieved in HH:MM format. A character is appended to it so to make sure none of the time strings in the input array will exactly match it. This will ensure that calling strcmp with the current time as argument, and an input date as other argument, will never return 0, but either -1 or 1. Note that -1 means that the first argument is less than the second argument. 1 means the opposite.
The callback passed to usort will be called for several pairs of input time strings and should return a negative or positive value (or 0) to indicate how the pair should be ordered in the output: negative means the first value should come before the second, 0 means they are identical and can be ordered any way, and 1 means the second should be ordered before the first.
The return value is here a multiplication of three values, each the result of strcmp. The second and third can never be zero.
When the second and third value are equal (both 1 or both -1), it means that the two input times are at the same side of the current time: either both before the current time, or both after it. The product of these two factors is then 1.
When these values are not equal (one is 1, the other -1), then it means the two input times are at different sides of the current time, and so their order should be opposite. The product is then -1.
Finally, the first factor tells us how the two input values compare to eachother. This should be reversed when the above mentioned product is -1, and indeed the multiplication will take care of that.
Use usort for that:
usort($array, function ($a, $b) {
$aTime = strtotime(date('Y-m-d ' . $a['time']));
$bTime = strtotime(date('Y-m-d ' . $b['time']));
$time = strtotime(date('Y-m-d 13:00'));
if ($aTime > $time && $bTime < $time)
return -1; // $a is in future, while $b is in past
elseif ($aTime < $time && $bTime > $time)
return 1; // $a is in past, while $b is in future
else
return $aTime - $bTime; // $a and $b is either in future or in past, sort it normally.
});
Live example
In PHP 7
usort($arr, function($a, $b)
{
$t = time();
$a = $t < ($a = strtotime($a['time'])) ? $a : $a + 86400; // +1 day
$b = $t < ($b = strtotime($b['time'])) ? $b : $b + 86400;
return $a <=> $b;
});
This is the Example #1 from the php.net usort() page:
<?php
function cmp($a, $b) {
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
$a = array(3, 2, 5, 6, 1);
usort($a, "cmp");
foreach ($a as $key => $value) {
echo "$key: $value\n";
}
?>
The usort function takes the values within the array as pairs ($a-$b; so this is - 3-2, 2-5, 5-6, 6-1) and moves the $b value depending on whether the cmp() function returns -1, 0 or 1. If it is -1 the $b gets moved down (within a current pair), if it is 0 it stays in the same place and if it is 1 it gets moved up. This is how this is suppose to be working based on the top comment from the php.net manual usort() page.
Is there any way to see how this works step by step (the sorting process)? Am I able to see it or is it only possible to see the final result after the sorting is done? I want to understand fully how this process works.
Using some debug output in your comparison function you can only see the comparisons that PHP does, but can't see intermediate states of the array.
But the algorithm used in usort is well known - it's QuickSort ( Which sort algorithms does PHP's usort apply? ).
You can see its vizualisation at http://www.algomation.com/algorithm/quick-sort-visualization (or just google "quicksort algorithm visualization")
This question already has an answer here:
How the usort() sorting algorithm works?
(1 answer)
Closed 5 years ago.
<?php
function list_cmp($a, $b) {
global $order;
foreach ($order as $key => $value) {
if ($a == $value) {
return 0;
}
if ($b == $value) {
return 1;
}
}
}
$order[0] = 1;
$order[1] = 3;
$order[2] = 4;
$order[3] = 2;
$array[0] = 2;
$array[1] = 1;
$array[2] = 3;
$array[3] = 4;
$array[4] = 2;
$array[5] = 1;
$array[6] = 2;
usort($array, "list_cmp");
I know that this is Insertion Sort (not a Quicksort, since the array is 6-15 elements large). I see that the $a-$b pairs get compared to the order and if the list_cmp callback function returns 1, the current $b value gets moved towards the beginning of the array. This is how the usort() works, only the $b gets moved. I am assuming that it is always in this direction and that this assumption is correct.
The $a-$b pairs are like this - 2-1, 2-3, 1-3, 2-4, 3-4, 2-2, 2-1, 2-1, 4-1, 3-1, 1-1, 2-2, and these are the values that get returned in all the steps - 1,1,0,1,0,0,1,1,1,1,0,0 (if 1 is returned the $b gets moved and if 0 is returned I guess the usort() is looking for the correct insertion spot, but how this works? I am not seeing how this works, what or where is the mechanism of this?)
I know that it is 1) comparing 2) finding an insertion point and 3) inserting and that it looks like this:
-- [2,1],3,4,2,1,2 -> 1./2./3. compare [2,1], find and insert 1 before 2
-- 1,[2,3],4,2,1,2 -> 1./2. compare [2,3], find insert point for 3 (since order of 3 < order of 2)
-- [1,3],2,4,2,1,2 -> 3. compare [1,3], found insert point for 3 before 2
-- 1,3,[2,4],2,1,2 -> 1./2. compare [2,4], find insert point for 4 (since order of 4 < order of 2)
-- 1,[3,4],2,2,1,2 -> 3. compare [3,4], found insert point for 4 before 2
-- 1,3,4,[2,2],1,2 -> 1. compare [2,2], skip
-- 1,3,4,2,[2,1],2 -> 1./2. compare [2,1], find insert point for 1
-- 1,3,4,[2,1],2,2 -> 2. compare [2,1], find insert point for 1
-- 1,3,[4,1],2,2,2 -> 2. compare [4,1], find insert point for 1
-- 1,[3,1],4,2,2,2 -> 2. compare [3,1], find insert point for 1
-- [1,1],3,4,2,2,2 -> 3. compare [1,1], fond insert point for 1 before 3
-- 1,1,3,4,2,[2,2] -> 1. compare [2,2], skip
-- sorted: 1,1,3,4,2,2,2
But again, I am basically not seeing a mechanism of finding the insertion spots. The dynamic of this is - list_cmp returns 1 - move the $b (and towards the beginning of the array), but what is the mechanism of finding the right spot? One could say, this is within the usort() but we are not generating something like puttting 5 here 1,2,3,4,x,6,7,8,9. The result of this is 1,1,3,4,2,2,2 and this is based on whats within the $order array.
http://php.net/manual/en/function.usort.php states:
value_compare_func
The comparison function must return an integer less
than, equal to, or greater than zero if the first argument is
considered to be respectively less than, equal to, or greater than the
second
When you call the function, it takes any two values and decides whether they're in the right order.
Really all it does is move $b variable up or down the array in respect to $a.
Adding some output to your function can show you what's going on:
<?php
function list_cmp($a, $b) {
global $order;
echo "comparing $a and $b...\n";
foreach ($order as $key => $value) {
echo " checking against $value\n";
if ($a == $value) {
echo " \$a ($a) == $value: returning 0\n";
return 0;
}
if ($b == $value) {
echo " \$b ($b) == $value: returning 1\n";
return 1;
}
}
}
Let's look at the first part of the output:
comparing 4 and 1...
checking against 1
$b (1) == 1: returning 1
Here you can see that it's checking the two values 4 and 1 (we don't know which 1 it is). It then checks each item in $order. The first value is 1, which is saying all the 1s must come before any other value.
$a doesn't match 1 but $b does. So these items are in the wrong order - the first item is greater than the second, so we return 1. i.e. 4 must come after 1.
Let's look at another bit of output:
comparing 4 and 2...
checking against 1
checking against 3
checking against 4
$a (4) == 4: returning 0
Here it has checked the values 1 and 3 - neither of them match the two numbers we're considering, so they're irrelevant. Then we get to $a (4). That equals the next wanted value. This means it's in the right place, so we return 0. i.e. 4 must come before 2 - and it does.
That continues for each pair the sort compares.
First of all, your custom comparison function is incorrect. It must return a negative value when $a must stay in front of $b in the sorted array, zero (0) when $a and $b are equal and a positive value when $a must stay after $b in the sorted array.
If you read "$a is smaller than $b" instead of "$a stays before $b" (and "greater" instead of "after") you get the array sorted ascending. But nobody says the array must be sorted ascending and there is no custom array sorting function for descending sorting. The order of the elements in the final array is decided by the custom comparison function provided.
You are talking about "insertion points" in the question. There are dozens of sorting algorithms. Some of them start with an empty list and insert the elements in their final positions, others swap two items and do the sorting in-place.
I don't know what algorithm is implemented by the u*sort() PHP functions (and for the daily use I don't even care) but I assume it uses quicksort or other fast algorithm. Just for your curiosity, quicksort doesn't insert anything, it swaps items.
No matter what algorithm is used, the main operation used by sorting is the comparison of two items. How the algorithm uses the items after it finds out which one should stay in front of the other is a different story but it doesn't interfere with the comparison itself.
I'm trying to make a way to sort words first by length, then alphabetically.
// from
$array = ["dog", "cat", "mouse", "elephant", "apple"];
// to
$array = ["cat", "dog", "apple", "mouse", "elephant"];
I've seen this answer, but it's in Java, and this answer, but it only deals with the sorting by length. I've tried sorting by length, using the code provided in the answer, and then sorting alphabetically, but then it sorts only alphabetically.
How can I sort it first by length, and then alphabetically?
You can put both of the conditions into a usort comparison function.
usort($array, function($a, $b) {
return strlen($a) - strlen($b) ?: strcmp($a, $b);
});
The general strategy for sorting by multiple conditions is to write comparison expressions for each of the conditions that returns the appropriate return type of the comparison function (an integer, positive, negative, or zero depending on the result of the comparison), and evaluate them in order of your desired sort order, e.g. first length, then alphabetical.
If an expression evaluates to zero, then the two items are equal in terms of that comparison, and the next expression should be evaluated. If not, then the value of that expression can be returned as the value of the comparison function.
The other answer here appears to be implying that this comparison function does not return an integer greater than, less than, or equal to zero. It does.
Note: I didn`t post my answer early,because #Don't Panic faster then me. However,I want to add some explanation to his answer ( hope, it will be useful for more understanding).
usort($array, function($a, $b) {
return strlen($a) - strlen($b) ?: strcmp($a, $b);
});
Ok. Function usort waits from custom comparison function next (from docs):
The comparison function must return an integer less than, equal to, or
greater than zero if the first argument is considered to be
respectively less than, equal to, or greater than the second.
Ok, rewrite #Don't Panic code to this view (accoding the condition above):
usort($array, function($a, $b) {
// SORT_ORDER_CONDITION_#1
// equals -> going to next by order sort-condition
// in our case "sorting alphabetically"
if (strlen($a) == strlen($b)){
// SORT_ORDER_CONDITION_#2
if (strcmp($a,$b)==0) // equals - last sort-condition? Return 0 ( in our case - yes)
return 0; //
return (strcmp($a,$b)) ? -1 : 1;
}else{
return (strlen($a) < strlen ($b) ) ? - 1 : 1;
}
});
"Common sort strategy" (abstract) with multi sort-conditions in order like (CON_1,CON_2... CON_N) :
usort($array, function(ITEM_1, ITEM_2) {
// SORT_ORDER_CONDITION_#1
if (COMPARING_1_EQUALS){
// SORT_ORDER_CONDITION_#2
if (COMPARING_2_EQUALS){ // If last con - return 0, else - going "deeper" ( to next in order)
//...
// SORT_ORDER_CONDITION_#N
if (COMPARING_N_EQUALS) // last -> equals -> return 0;
return 0;
return ( COMPARING_N_NOT_EQUALS) ? -1 : 1;
//...
}
return ( COMPARING_2_NOT_EQUALS) ? -1 : 1;
}else{
return ( COMPARING_1_NOT_EQUALS ) ? - 1 : 1;
}
});
In practise (from my exp), it's sorting unordered multidimensional-array by several conditions. You can use usort like above.
This is not as short as other methods, but I would argue that it's clearer, and can be easily extended to cover other use cases:
$f = function ($s1, $s2) {
$n = strlen($s1) <=> strlen($s2);
if ($n != 0) {
return $n;
}
return $s1 <=> $s2;
};
usort($array, $f);
I have a multidimensional array with locations data (e.g. address, phone, name,..) and their relative distance from a certain point as floats (e.g. 0.49012608405149 or 0.72952439473047 or 1.4652101344361 or 13.476735354172).
Now I need to sort this array so that it starts with the data set of closest distance (0.49012608405149) and ends with the farthest (13.476735354172).
The function I use so far does a good job, but messes up some times, which is of course as it uses strcmp
function cmp($a, $b) {
return strcmp($a["distance"], $b["distance"]);
}
usort($resultPartner, "cmp");
I googled a lot but couldn't find anything for my case. If possible I would like to avoid a foreach statement, as I read it can have a poor performance with big arrays.
Do you have any idea/experience with that and can give me a working function for this? Thank you!
strcmp() is binary safe string comparison why you don't just compare floats?
When comparing floats php manual says
Returning non-integer values from the comparison function, such as
float, will result in an internal cast to integer of the callback's
return value. So values such as 0.99 and 0.1 will both be cast to an
integer value of 0, which will compare such values as equal.
So you must be careful.
Look at this: http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm
Since floating point calculations involve a bit of uncertainty we can
try to allow for this by seeing if two numbers are ‘close’ to each
other.
Try something like this:
function cmpfloat($a, $b) {
if (abs($a["distance"]-$b["distance"]) < 0.00000001) {
return 0; // almost equal
} else if (($a["distance"]-$b["distance"]) < 0) {
return -1;
} else {
return 1;
}
}
Following function is good if comparing integer values:
function cmp($a, $b) {
return $a["distance"] < $b["distance"] ? -1 : ($a["distance"] === $b["distance"] ? 0 : 1);
}
if a distance is smaller than b distance return -1
if a distance equals b distance return 0
if a distance is greater than b distance return 1
Reason:
The comparison function must return an integer less than, equal to, or greater than zero if the first argument is considered to be respectively less than, equal to, or greater than the second.
Maybe in such way:
$data = array(
array('dist' => 0.72952439473047),
array('dist' => 0.49012608405149),
array('dist' => 0.95452439473047),
array('dist' => 0.12952439473047),
);
foreach ($data as $k => $v) {
$dist[$k] = $v['dist'];
}
array_multisort($dist, SORT_ASC, $data);