usort and compare function with arguments PHP - php

I have question
My code works but i dont understand what is $x[1] and $y[1] in function
I tryed $x[0], $x[1], $x[2], $y[0], $y[1], $y[2] and dont get the logical output ? Where i am wrong to understand ? Please if someone can help me ?
<?php
$products = array( array('TIR', 'Tires', 100),
array('OIL', 'Oil', 10),
array ('SPK', 'Spark Plugs', 4));
//print_r ($products); echo '<br />';
function compare($x, $y) {
if ($x[1] == $y[1]) {
return 0;
} else if ($x[1]<$y[1]) {
return -1;
} else {
return 1;
}
}
usort ($products, 'compare');
echo compare('Tires', 'Tires' );
echo compare('Oil', 'Spark Plugs' );
echo compare('Spark Plugs', 'Oil' );
echo compare('Tires', 'Tires');
echo '<br />';
Output is for this code for echo 1, 2, 3, 4:
0
-1
1
0

When you call the compare($x, $y) function, you are passing the strings as the parameters. These string are treated as arrays with 0-based indexing.
So, when echo compare('Tires', 'Tires' ); is executed, these two strings are passed and according to compare function, the character at index 1(indexing starts at 0) i.e. 2nd character is compared.
So, for this ```echo compare('Tires', 'Tires' );```, the compared characters are 'i' and 'i' which are equal and hence 0 is returned.
So, for this echo compare('Oil', 'Spark Plugs' );, the compared characters are 'i' and 'p'. 'i' is less than p and hence -1 is returned. To decide which character is lower than the other, lookup ASCII codes.
And so on for other function calls. Let me know, if you still have any doubt.
This I have explained for just the independent echo compare('Oil', 'Spark Plugs' ); line not for usort function.
UPDATE For the usort function
Let me first explain the way a comparator functions works. Whenever two parameters are passed to the compare function, it returns true or false and this is used to determine whether you need to swap those values or not.
In the earlier case, echo compare('Tirez', 'Tires' );
$x = Tires, and
$y = Tirez
You compare $x[1] and $y[1], particularly the character at index 1. But what if in the case of these strings, you just do $x < &y, the strings are compared automatically character-by-character according to ASCII codes for English alphabets and the result is returned on the first position, the characters do not match.
i.e. if you want to compare if one string is lexicographically smaller than the other string then you can use the below comparator function.
function compare($x, $y) {
if ($x == $y) {
return 0;
} else if ($x < $y) {
return -1;
} else {
return 1;
}
}
The output will be 1, since while comparing character-by-character 'z' > 's'.
So, when a complete array is passed to compare function, the first two elements are passed. Here the array $products is a 2D array (an array of arrays), so the first two arrays are passed
i.e. $x = array('TIR', 'Tires', 100), and
      $y = array('OIL', 'Oil', 10)
So, it depends on your requirement. For example, if you want to sort by index 0 of any array of $products i.e. 'TIR', 'OIL', 'SPK' then change the comparator function to $x[0] and $y[0].
I hope you are able to understand now :).

Related

What is the reason of returning -1 instead of lets say 0 at the end of this function's code?

I am talking about the second "return -1;" on the 12th line of the code. This gets reached only if two sets of numbers are exactly the same, like when comparing '192.167.11' to '192.167.11'. I will also add that using range(0,2) would be a better option for this piece of code (range(0,3) produces errors if two elements happen to be the same; I did not change that as this is the original code example from PHP Array Exercise #21 from w3resource.com).
<?php
function sort_subnets($x, $y){
$x_arr = explode('.', $x);
$y_arr = explode('.', $y);
foreach (range(0, 3) as $i) {
if ($x_arr[$i] < $y_arr[$i]) {
return -1;
} elseif ($x_arr[$i] > $y_arr[$i]) {
return 1;
}
}
return -1;
}
$subnet_list =
array('192.169.12',
'192.167.11',
'192.169.14',
'192.168.13',
'192.167.12',
'122.169.15',
'192.167.16'
);
usort($subnet_list, 'sort_subnets');
print_r($subnet_list);
?>
Returning "-1" would move the second element (the same as the first in the current $x and $y pair) towards the higher index of the array (down the array). Why not return "0" and keep everything as is if the two elements are exactly the same? Is there any reason for returning the "-1" maybe based on how the usort() works (or any other factor of this)?
Thanks.
EDIT:
I think that this is Insertion Sort (array size 6-15 elements; normally it would be Quicksort).
If the two elements are the same, there's no difference between swapping the order and keeping the order the same. So it doesn't make a difference what it returns in that case.
You're right that 0 is more appropriate. This would be more important if usort were "stable". But the documentation says
Note:
If two members compare as equal, their relative order in the sorted array is undefined.
To illustrate the excellent point of #Don'tPanic:
<?php
function sort_subnets($x, $y){
$x_arr = explode('.', $x);
$y_arr = explode('.', $y);
return $x_arr <=> $y_arr;
}
$subnet_list =
array('192.169.12',
'192.167.11',
'192.169.14',
'192.168.13',
'192.167.12',
'122.169.15',
'192.167.16'
);
usort($subnet_list, 'sort_subnets');
print_r($subnet_list);
See live code
Note the use of the "spaceship" operator, namely <=> which offers a conciseness that spares one from having to write code like the following in a function:
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
Lastly, note that the user-defined callback for usort() makes use of ternary logic because sometimes as in the case of sorting bivalent logic is insufficient. Yet, usort() itself utilizes two-part logic, returning TRUE on success and FALSE on failure.

Sort multidimensional array with floats

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);

php compare alphabet position?

I have two variables such as:
var1 = "z";
var2 = "A";
how can I check if var1 is after in the alphabet than var2 (in this case it should return true)?
I think everyone who has answered agrees that strcmp() is the right answer, but every answer provided so far will give you incorrect results. Example:
echo strcmp( "Z", "a" );
Result: -1
echo strcmp( "z", "A" );
Result: 1
strcmp() is comparing the binary (ord) position of each character, not the position in the alphabet, as you desire. If you want the correct results (and I assume that you do), you need to convert your strings to the same case before making the comparison. For example:
if( strcmp( strtolower( $str1 ), strtolower( $str2 ) ) < 0 )
{
echo "String 1 comes before string 2";
}
Edit: you can also use strcasecmp(), but I tend to avoid that because it exhibits behavior that I've not taken the time to understand on multi-byte strings. If you always use an all-Latin character set, it's probably fine.
What did you try?... pretty sure this works
<?php
if(strcmp($var1,$var2) > 0) {
return true;
}
If you're comparing a single character, you can use ord(string). Note that uppercase values compare as less than lowercase values, so convert the char to lowercase before doing the comparison.
function earlierInAlphabet($char1, $char2)
{
$char1 = strtolower($char1);
$char2 = strtolower($char2);
if(ord($char1) < ord($char2))
return true;
else
return false;
}
function laterInAlphabet($char1, $char2)
{
$char1 = strtolower($char1);
$char2 = strtolower($char2);
if(ord($char1) > ord($char2))
return true;
else
return false;
}
If you're comparing a string (or even a character) then you can also use strcasecmp(str1, str2):
if(strcasecmp($str1, $str2) > 0)
// str1 is later in the alphabet
return strcmp($var1,$var2) > 0?
You should use http://php.net/manual/en/function.strcmp.php
Returns < 0 if str1 is less than str2; > 0 if str1 is greater than str2, and 0 if they are equal.
So
return strcmp(var1, var2) > 0 // will return true if var1 is after var2
This solution may be over-the-top for just two variables; but this is for ever if you need to solve if a bunch of variables (2+) would be in the right order...
<?php
$var1='Z';
$var2='a';
$array1 = array();
$array[] = $var1;
$array1[] = $var2;
$array2 = sort($array1);
if($array2 === $array1){
return true;
}else{
return false;
}
?>
Other than that if you only want to do it with two variables this should work just fine.
<?php
return (strcmp($var1,$var2) > 0);
?>

php array_multisort not working

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().

How to sort an array of Roman numerals?

I have an array containing Roman numerals (as strings of course). Like this:
$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
I'd like to sort them according to the numeric values of these numerals, so the results should be something like:
$sorted_a = array('III', 'V', 'XIII', 'XIX', 'LII', 'MCCXCIV');
So my question is: what is the best way to sort an array of Roman numerals? I know how to use the array sorting functions of PHP, I'm interested in the logic that goes on inside the comparison function.
EDIT: For simplicity, I'm only looking for a way that deals with strings constructed of the basic numerals in a standard way (no CCCC for example):
I, V, X, L, C, D, M
TEST RESULTS
I took the time to extensively test all the code examples that were posted. Two tests were taken, one with a random array of 20 Roman numerals, and a second with an array containing 4000 of those. Same machine, lot of iterations, an average time taken, and all this run several times. Of course this is nothing offical, just my own tests.
TEST WITH 20 NUMERALS:
hakre, bazmegakapa - around 0.0005 s
anemgyenge, Andrea, Dirk McQuickly - around 0.0010 s
Joe Nelson - around 0.0050 s
Rob Hruska - around 0.0100 s
TEST WITH 4000 NUMERALS:
hakre, bazmegakapa - around 0.13 s
anemgyenge - around 1.4 s
Dirk McQuickly, Andrea - around 1.8 s
Rob Hruska - around 2.8 s
Joe Nelson - around 15 s (surprise, checked several more times)
I have a hard time awarding the bounty. hakre and I made the fastest versions, following the same route, but he made a variation of mine, which was previously based on borrible's idea. So I will accept hakre's solution, because that is the quickest and nicer than mine (IMO). But I will award the bounty to anemgyenge, because I love his version and a lot of effort seems to be put into it.
Picking your class to convert roman numbers to integers, a user-defined sort callback can handle this to sort the array:
$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
$bool = usort($a, function($a, $b) {
return RomanNumber::Roman2Int($a) - RomanNumber::Roman2Int($b);
});
var_dump($a);
So here you find the logic inside the comparison function: if both values are of the same weight, return 0. If the first is lower than the second, return < 0 (e.g. -1), otherwise the second is larger than the first so return > 0 (e.g. 1).
Naturally any other type of function that returns the decimal value for a roman number would work as well.
Edit:
As you commented, you do not want to run the conversion for each pair. That's fine, with a help of an additional array which contains all converted values, you can run the sort on the decimal values and use that sorting on the roman numbers as well (Demo):
$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
$b = array_map('RomanNumber::Roman2Int', $a);
array_multisort($b, $a);
var_dump($a);
array_multisort PHP Manual does most of the magic here.
function sortRomanNum($a, $b) {
if($a == $b) return 0;
$str = "0IVXLCDM";
$len = 0;
if(strlen($a) >= strlen($b)) {
$len = strlen($a);
$b .= str_repeat("0", $len - strlen($b));
}
else {
$len = strlen($b);
$a .= str_repeat("0", $len - strlen($a));
}
for($i = 0; $i < $len - 1; $i++) {
$a1 = $a[$i]; $b1 = $b[$i]; $a2 = $a[$i+1]; $b2 = $b[$i+1];
if( strpos($str, $a1.$b1.$a2) !== false ) return 1;
if( strpos($str, $b1.$a1.$b2) !== false ) return -1;
if($a1 != $b1) return strpos($str, $a1) > strpos($str, $b1) ? 1 : -1;
}
if($a[$i] != $b[$i]) return strpos($str, $a[$i]) > strpos($str, $b[$i]) ? 1 : -1;
}
Given two numbers (roman strings), $a and $b. If there are no substractions in the numbers (IV, IX, XC etc), then the solution would be trivial:
for all $i in $a and $b
if $a[$i] > $b[$i] then return 1; //($a is greater then $b)
if $a[$i] < $b[$i] then return 1; //($a is lower then $b)
return 0 //equality
Since there can be these special parts, the calculation is more complex. But the solution is to find the patterns:
a: IX | XC | CM
b: V | L | D
These are the only patterns which can mess up the trivial solution. If you find any of these, then $a will be greater then $b.
Note, that roman numbers don't include zeros, like the arabic ones. Therefore now we will use them (and basically put zeros where they are missing).
So here comes the function:
if $a == $b then return 0; //equality
create a string for ordering the roman numerals (strpos will give the right index)
define the length of the loop (take the longer string), and add zeros to the end of the shorter number
run the loop, and check:
1. if the patterns above are found, return the comparision accordingly (1 or -1)
2. otherwise do the trivial check (compare each numeral)
check the last numerals too.
Some people have suggested converting Roman numerals to integers, sorting, and mapping back. There is an easier way. All that we really need to do is compare any two arbitrary Roman numerals and let usort do the rest. Here is the code, and I will explain its design below.
$base = array( 'I' => 0, 'V' => 1, 'X' => 2, 'L' => 3,
'C' => 4, 'D' => 5, 'M' => 6 );
function single($a) { global $base; return $base[$a]; }
function compare($a, $b) {
global $base;
if(strlen($a) == 0) { return true; }
if(strlen($b) == 0) { return false; }
$maxa = max(array_map('single', str_split($a)));
$maxb = max(array_map('single', str_split($b)));
if($maxa != $maxb) {
return $maxa < $maxb;
}
if($base[$a[0]] != $base[$b[0]]) {
return $base[$a[0]] < $base[$b[0]];
}
return compare(substr($a, 1), substr($b, 1));
}
$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
usort($a, compare);
print_r($a);
First we create a lookup array to assign a "magnitude" to single digit Roman numerals. Notice this isn't their decimal value, just numbers assigned in such a way that bigger numerals get bigger values. Then we create a helper function single used by some PHP functions to to retrieve the magnitudes.
OK, now to the meat of the algorithm. It is the compare function which sometimes has to call itself recursively when it needs to break a tie. For this reason, we start with some tests to see if it has reached terminal states in the recursion. Disregard that for now and look at the first interesting test. It checks to see if either numeral being compared has a digit in it that dwarfs any digits of the other. For instance, if one of them has X in it, and the other only has I and V, then the one with X wins. This relies on the convention that certain Roman numerals are not valid, like VV or VIIIII or IIIIIIIII. At least I have never seen them written that way, so I count them as invalid.
To make this check, we map the digits to magnitudes and compare maximums. Well, this test may not decide the issue. In that case it is safe to compare the first digits of each number, since we won't have to deal with confusing issues like V < IX where the first digits don't suggest the truth. These confusing situations were taken care of by comparing largest digits.
Finally, if the first digits are equal, strip them off and repeat. At some point one of the numerals will be reduced to an empty string, and those initial tests we were temporarily disregarding will take care of that.
This method has passed all the tests I threw at it, but let me know if you find a bug or optimizations.
There would seem to be three approaches, namely:
Convert the numbers, sort using a standard integer sort, and convert back. (Or keep the converted versions with the roman numerals and sort the structures, to avoid the double conversion.)
Write a sort function that takes the strings, at that point calls a conversion function and does the appropriate comparison.
Write a sort function that can compare Roman numerals directly, without necessary involving a full conversion. Since Roman numerals have their higher components first, (Ms then D/Cs. then L/Xs, then I/Vs) such a function might be able to short circuit early.
The first will obviously involve additional overhead for storage. The second will involve additional conversion overhead (since the same number may be converted many times). The third might involve some unnecessary conversion overhead (again, the same number may be converted several times) but save some work on the short circuiting. If storage overheads are not an issue, the first is likely to be the best.
I got quite interested in #borrible's 1st approach, so I decided I will give it a try:
function sortRomanArray($array) {
$combined=array_combine($array, array_map('roman2int', $array));
asort($combined);
return array_keys($combined);
}
This basically converts all the Roman numerals in the array into integers using array_map() and a function called roman2int() (which can be any implementation). Then it creates an array where the keys are the Roman numerals and values are the integers. Then this array is sorted with asort() that preserves key associations, and the keys are returned as an array. This array will contain the sorted Roman numerals.
I like this method because it runs the conversion function only as much times as the size of the array (6 with my example array) and there is no need to convert back.
The conversion would run certainly much more if we put it in the comparison function (2 times for every comparison).
I think you'll have to either:
Wrap the strings into a RomanNumeral class, that has a sorting method OR
Write a method to calculate the value of each element in the array, and sort on that
See if someone has already written a RomanNumeral class/library that does this - something like this
Either way, you'll need custom sorting code that calculates the value somewhere. Since prefixing characters in Roman Numerals can sometimes mean "subtract this value" as opposed to "add this value". This is fine, because as you've pointed out, what you're really doing is sorting by numeric value, so you'll have to tell the computer how to interpret the value.
Convert the numeral to a decimal using this
Compare the decimals
function roman2dec($roman) {
// see link above
}
function compare($a, $b) {
return roman2dec($a) < $roman2dec($b) ? -1 : 1;
}
The simplest solution is probably to first convert each numeral into a regular integer (in a new array), and then sort both arrays based on the integer array. Not sure if PHP contains a function for that, though. Alternatively, you can define a comparison function that converts two Roman numerals to integers and compares them. Writing a function that directly compares two Roman numerals without converting them to integers first will likely be cumbersome.
Let's say you make this "alphabet": I, IV, V, IX, X, XL, L, XC, C, CD, D, CM, M.
Then you could sort the Roman numbers according to this 'alphabet'.
Maybe this will give someone new inspiration.
EDIT: got a working example. Not really fast, sorts 1000 Roman numbers in 1.3 secs
EDIT 2: added a check to avoid the 'notices', also optimized the code a little, runs a little faster, and about twice as fast than with a conversion to integer and than sort that (used PEAR Number_Roman package)
function sortromans($a, $b){
$alphabet = array('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I');
$pos = 0;
if ($a == $b) {
return 0;
}
//compare the strings, position by position, as long as they are equal
while(isset($a[$pos]) && isset($b[$pos]) && $a[$pos] === $b[$pos]){
$pos++;
}
//if string is shorter than $pos, return value
if(!isset($a[$pos])){
return -1;
} else if(!isset($b[$pos])){
return 1;
} else {
//check the ´character´ at position $pos, and pass the array index to a variable
foreach($alphabet as $i=>$ch){
if(isset($a_index) && isset($b_index)){
break;
}
$length = strlen($ch);
if(!isset($a_index) && substr($a, $pos, $length) === $ch){
$a_index = $i;
}
if(!isset($b_index) && substr($b, $pos, $length) === $ch){
$b_index = $i;
}
}
}
return ($a_index > $b_index) ? -1 : 1;
}
$romans = array('III', 'IX', 'I', 'CM', 'LXII','IV');
usort($romans, "sortromans");
echo "<pre>";
print_r($romans);
echo "</pre>";
I think the best (see my comment) first solution is to use the standard usort PHP function with the help of a special roman compare function.
The following roman_compare function is very intuitive and do not use any kind of conversion. To keep it simple, it uses tail recursion.
function roman_start( $a )
{
static $romans = array(
'I' => 1, 'V' => 5,
'X' => 10, 'L' => 50,
'C' => 100, 'D' => 500,
'M' => 1000,
);
return $a[0] . ($romans[$a[0]] < $romans[$a[1]] ? $a[1] : '');
}
function roman_compare( $a, $b )
{
static $romans = array(
'I' => 1, 'IV' => 4, 'V' => 5, 'IX' => 9,
'X' => 10, 'XL' => 40, 'L' => 50, 'XC' => 90,
'C' => 100, 'CD' => 400, 'D' => 500, 'CM' => 900,
'M' => 1000,
);
$blockA = roman_start($a);
$blockB = roman_start($b);
if ($blockA != $blockB)
{
return $romans[$blockA] - $romans[$blockB];
}
$compared = strlen($blockA);
if (strlen($a) == $compared) //string ended
{
return 0;
}
return roman_compare(substr($a, $compared), substr($b, $compared));
}
Using the above functions, we can write
function array_equal( $a, $b )
{
return count(array_diff_assoc($a, $b)) == 0 && count(array_diff_assoc($b, $a)) == 0;
}
$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
$sorted_a = array('III', 'V', 'XIII', 'XIX', 'LII', 'MCCXCIV');
var_dump(array_equal($sorted_a, $a));
usort($a, 'roman_compare');
var_dump(array_equal($sorted_a, $a));
Running all the above code we get
bool(false)
bool(true)

Categories