sort array by length and then alphabetically - php

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

Related

Usort on object returns wrong order

I need to modify an array of objects, based on a "custom_sorting" value. I use usort like this:
usort($this->rows, function($a, $b) {
return $a->custom_sorting <=> $b->custom_sorting;
});
However, the returned sort order is a bit off, as can be seen in the example order below. Notice 4 comes after 39, so it treats 4 as a higher value than 39. Same goes for 5,6,7,8,9 - all treated as higher values than 45. What can I do to sort it in the correct numeric order?
5-37
5-38
5-39
5-4
5-40
5-41
5-42
5-43
5-44
5-45
5-5
5-7
5-8
5-9
Thanks
As #Nigel Ren's comment suggests, your custom_sorting is probably based on string, so it's working as it should. Instead try:
Newer answer using the built in strnatcmp:
usort($this->rows, function($a, $b) {
return strnatcmp($a->custom_sorting, $b->custom_sorting);
});
Older, more manual answer:
usort($this->rows, function($a, $b) {
$aDash = strpos($a->custom_sorting, '-');
$bDash = strpos($b->custom_sorting, '-');
$compareFirstPart = ((int) substr($a->custom_sorting, 0, $aDash-1)) <=> ((int) substr($b->custom_sorting, 0, $bDash-1));
if ($compareFirstPart !== 0) {
return $compareFirstPart;
}
return ((int) substr($a->custom_sorting, $aDash)) <=> ((int) substr($b->custom_sorting, $bDash));
});
Explanation:
Take the parts before the dash ('-'), cast them to ints, and comare them as ints.
If they are equal the result is 0, so return the result for the same comparison with the part after the dash;

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.

PHP array multisort (version 5/7 differences)

I got a array like this:
$json = '{"Categorie1":[{"created":"2017-07-17 08:53:00","catid":"54"},{"created":"2017-05-23 10:15:00","catid":"54"},{"created":"2017-05-09 05:49:23","catid":"54"}],"Categorie2":[{"created":"2017-03-21 08:58:37","catid":"59"},{"created":"2016-12-23 12:48:00","catid":"59"},{"created":"2016-12-08 09:57:10","catid":"59"}],"Categorie3":[],"Categorie4":[{"created":"2017-08-02 07:15:07","catid":"70"},{"created":"2017-08-01 08:03:00","catid":"70"},{"created":"2017-07-31 09:25:00","catid":"70"}],"Categorie5":[{"created":"2017-07-26 14:09:00","catid":"74"},{"created":"2017-06-29 14:03:00","catid":"74"},{"created":"2017-06-28 06:35:35","catid":"74"}]}';
And I wrote a sorting function. Basically it checks which block (categorie) got the newest entry and brings that block to top, sort the blocks):
$array = json_decode($json, true);
function custom($a, $b) {
foreach($a as $k => $v) {
if (isset($b[$k])) {
return (strtotime($a[$k]["created"]) <= strtotime($b[$k]["created"]));
}
}
}
uasort($array, "custom");
When I print this with PHP 5 its perfect: Categorie4 is the first block". But with PHP 7 it doesn't.
print("<pre>");
print_r($array); // PHP 5 is as expected, php 7 is not
I know there where changes, but I can't figure out how to change my code.
Can you guys helping me change the code? The result should show categorie4 as first cat...
The callback function used by the user-defined array sorting functions must return an integer value that is <0 if $a < $b, 0 when $a == $b or >0 when $a > $b. Yours return a boolean that is converted to 1 or 0 and that doesn't reflect the correct order of $a and $b.
It's not clear from the question how should be sorted the empty entries (Categorie3) and I think their place is at the end.
Try this code for PHP 5:
uasort($array, function (array $a, array $b) {
if (empty($a)) { return +1; } // empty arrays go to the end of the list
if (empty($b)) { return -1; }
return strcmp($a[0]['created'], $b[0]['created']);
});
The date&time values of created use a format that can be sorted directly as strings, there is no need to convert them to timestamps.
In PHP 7 you can use the new <=> comparison operator that, in theory, should run faster than the strcmp() function while it produces the same result.
uasort($array, function (array $a, array $b) {
if (empty($a)) { return +1; } // empty arrays go to the end of the list
if (empty($b)) { return -1; }
return $b[0]['created'] <=> $a[0]['created'];
});

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

strcmp equivelant for integers (intcmp) in PHP

So we got this function in PHP
strcmp(string $1,string $2) // returns -1,0, or 1;
We Do not however, have an intcmp(); So i created one:
function intcmp($a,$b) {
if((int)$a == (int)$b)return 0;
if((int)$a > (int)$b)return 1;
if((int)$a < (int)$b)return -1;
}
This just feels dirty. What do you all think?
this is part of a class to sort Javascripts by an ordering value passed in.
class JS
{
// array('order'=>0,'path'=>'/js/somefile.js','attr'=>array());
public $javascripts = array();
...
public function __toString()
{
uasort($this->javascripts,array($this,'sortScripts'));
return $this->render();
}
private function sortScripts($a,$b)
{
if((int)$a['order'] == (int)$b['order']) return 0;
if((int)$a['order'] > (int)$b['order']) return 1;
if((int)$a['order'] < (int)$b['order']) return -1;
}
....
}
Sort your data with:
function sortScripts($a, $b)
{
return $a['order'] - $b['order'];
}
Use $b-$a if you want the reversed order.
If the numbers in question exceed PHP's integer range, return ($a < $b) ? -1 : (($a > $b) ? 1 : 0) is more robust.
Purely as some additional information, there has been an accepted RFC for this (https://wiki.php.net/rfc/combined-comparison-operator).
So, the comparison function would be along the lines of ...
<?php
$data = [...];
usort($data, function($left, $right){ return $left <=> $right; });
?>
A few really nice feature here is that the comparison is done in exactly the same way as all other comparisons. So type juggling will happen as expected.
As yet, there is no magic __forCompare() like method to allow an object to expose a comparison value. The current proposal (a different RFC) is to have each object be injected into every other object during the comparison so that it does the comparison - something which just seems odd to me - potential opportunity for recursion and stack overflow ... ! I would have thought either injecting the type of object for comparison (allowing an object the ability to represent appropriate values depending upon the type of comparison) or a blind request for a value that the object can serve up for comparison, would have been a safer solution.
Not yet integrated into PHP-NG (PHP 7 at the moment), but hopefully will be soon.
why reinventing the wheel?
http://php.net/manual/en/function.strnatcmp.php
echo strnatcmp(1, 2) . PHP_EOL; // -1
echo strnatcmp(10, 2) . PHP_EOL; // 1
echo strnatcmp(10.5, 2) . PHP_EOL; // 1 - work with float numbers
echo strnatcmp(1, -2) . PHP_EOL; // 1 - work with negative numbers
Test it here:
https://3v4l.org/pSANR
You could use
function intcmp($a,$b)
{
return ($a-$b) ? ($a-$b)/abs($a-$b) : 0;
}
Although I don't see the point in using this function at all
Does it have to be +1 and -1? If not, just return (int) $a - (int) $b. I don't like the divide that someone else recommended, and there's no need to check for all three cases. If it's not greater and not equal, it must be less than.
return (int) $a > (int) $b ? 1 : (int) $a == (int) $b ? 0 : -1;
At a glance, yes it feels dirty. Except there must be a good reason you wrote that instead of just using the actual ==, >, and < operators. What was the motivation for creating this function?
If it were me, I'd probably just do something like:
$x = $a==$b ? 0 : ($a>$b ? 1 : ($a<$b ? -1 : null));
I realize this is just as ugly, and the : null; - not sure if PHP requires it or if I could have just done :; but I don't like it and that code should never execute anyway... I think I'd be a lot less confused about this if I knew the original requirements!
For strings
usort($points, function ($a, $b) use ($orderFlag, $key1, $key2) {
return strcmp($a[$key1][$key2], $b[$key1][$key2]) * $orderFlag;
});
orderFlag => 1 (ascending): -1 (descending)
For numbers
usort($points, function ($a, $b) use ($orderFlag, $key1, $key2) {
return ($a[$key1][$key2] - $b[$key1][$key2]) * $orderFlag;
});
orderFlag => 1 (ascending): -1 (descending)

Categories