PHP sort array by number in filename - php

I am working on a photo gallery that automatically sorts the photos based on the numbers of the file name.
I have the following code:
//calculate and sort
$totaal = 0;
if($handle_thumbs = opendir('thumbs')){
$files_thumbs = array();
while(false !== ($file = readdir($handle_thumbs))){
if($file != "." && $file != ".."){
$files_thumbs[] = $file;
$totaal++;
}
}
closedir($handle_thumbs);
}
sort($files_thumbs);
//reset array list
$first = reset($files_thumbs);
$last = end($files_thumbs);
//match and split filenames from array values - image numbers
preg_match("/(\d+(?:-\d+)*)/", "$first", $matches);
$firstimage = $matches[1];
preg_match("/(\d+(?:-\d+)*)/", "$last", $matches);
$lastimage = $matches[1];
But when i have file names like photo-Aname_0333.jpg, photo-Bname_0222.jpg, it does start with the photo-Aname_0333 instead of the 0222.
How can i sort this by the filename numbers?

None of the earlier answers are using the most appropriate/modern technique to perform the 3-way comparison -- the spaceship operator (<=>).
Not only does it provide a tidier syntax, it also allows you to implement multiple sorting rules in a single step.
The following snippet break each filename string in half (on the underscore), then compare the 2nd half of both filenames first, and if there is a tie on the 2nd halves then it will compare the 1st half of the two filenames.
Code: (Demo)
$photos = [
'photo-Bname_0333.jpg',
'photo-Bname_0222.jpg',
'photo-Aname_0333.jpg',
'photo-Cname_0111.jpg',
'photo-Cname_0222.jpg',
'photo-Aname_0112.jpg',
];
usort($photos, function ($a, $b) {
return array_reverse(explode('_', $a, 2)) <=> array_reverse(explode('_', $b, 2));
});
var_export($photos);
Output:
array (
0 => 'photo-Cname_0111.jpg',
1 => 'photo-Aname_0112.jpg',
2 => 'photo-Bname_0222.jpg',
3 => 'photo-Cname_0222.jpg',
4 => 'photo-Aname_0333.jpg',
5 => 'photo-Bname_0333.jpg',
)
For anyone who still thinks the preg_ calls are better, I will explain that my snippet is making potentially two comparisons and the preg_ solutions are only making one.
If you wish to only use one sorting criteria, then this non-regex technique will outperform regex:
usort($photos, function ($a, $b) {
return strstr($a, '_') <=> strstr($b, '_');
});
I super-love regex, but I know only to use it when non-regex techniques fail to provide a valuable advantage.
Older and wiser me says, simply remove the leading portion of the string before sorting (use SORT_NATURAL if needed), then sort the whole array. If you are scared of regex, then make mapped calls of strtok() on the underscore.
Code: (Demo)
array_multisort(preg_replace('/.*_/', '', $photos), $photos);

usort is a php function to sort array using values.
usort needs a callback function that receives 2 values.
In the callback, depending of your needs, you will be return the result of the comparision 1, 0 or -1. For example to sort the array asc, I return -1 when the firts value of the callback is less than second value.
In this particular case I obtain the numbers of the filename, and compare it as string, is not necesary to cast as integer.
<?php
$photos=[
'photo-Bname_0222.jpg',
'photo-Aname_0333.jpg',
'photo-Cname_0111.jpg',
];
usort($photos, function ($a, $b) {
preg_match("/(\d+(?:-\d+)*)/", $a, $matches);
$firstimage = $matches[1];
preg_match("/(\d+(?:-\d+)*)/", $b, $matches);
$lastimage = $matches[1];
if ($firstimage == $lastimage) {
return 0;
}
return ($firstimage < $lastimage) ? -1 : 1;
});
print_r($photos);

It sorts alphabetically because you use sort() on the filename. The 2nd part of your code does nothing.
You might want to take a look at usort http://php.net/manual/en/function.usort.php
You can do something like
function cmp($a, $b) {
if ($a == $b) {
return 0;
}
preg_match('/(\d+)\.\w+$/', $a, $matches);
$nrA = $matches[1];
preg_match('/(\d+)\.\w+$/', $b, $matches);
$nrB = $matches[1];
return ($nrA < $nrB) ? -1 : 1;
}
usort($files_thumb, 'cmp');
Also, I'm not sure about your regex, consider a file named "abc1234cde2345xx". The one I used takes the last digits before a file extension at the end. But it all depends on your filenames.

sort(array,sortingtype) , you have to set the second parameter of the sort() function to 1 so it will sort items numerically
//calculate and sort
$totaal = 0;
if($handle_thumbs = opendir('thumbs')){
$files_thumbs = array();
while(false !== ($file = readdir($handle_thumbs))){
if($file != "." && $file != ".."){
$files_thumbs[] = $file;
$totaal++;
}
}
closedir($handle_thumbs);
}
sort($files_thumbs,1);
//reset array list
$first = reset($files_thumbs);
$last = end($files_thumbs);
//match and split filenames from array values - image numbers
preg_match("/(\d+(?:-\d+)*)/", "$first", $matches);
$firstimage = $matches[1];
preg_match("/(\d+(?:-\d+)*)/", "$last", $matches);
$lastimage = $matches[1];

Related

Count number of values in array with variables

I'm trying to count the number of times a certain value turns up in an array. Except this value will always increment by 1, and there is an unknown number of these values in the array.
Example:
$first = array( 'my-value-1','my-value-2','my-value-3' );
$second = array( 'my-value-1','my-value-2','my-value-3', 'my-value-4', 'my-value-5' );
My goal is to be able to retrieve a count of 3 for $first and a count of 5 for $second in the example above.
There may be other values in the array, but the only values I'm interested in counting are the ones that start with my-value-.
I won't know the number of the values in the array, but they will always start with my-value- with a number added to the end.
Is there a way to count the number of times my-value- shows up in the array with some sort of wildcard?
Use a regex to filter the array and count the values that match. You could also use ^ to force it to be at the beginning ^my-value-\d+:
$count = count(preg_grep('/my-value-\d+/', $first));
You could also do it this way. Again you could use === 0 instead to make it match at the beginning:
$count = count(array_filter($first, function($v) {
return strpos($v, 'my-value-') !== false;
}));
A quick function to count values by providing a partial string and the target array.
Note: search is case-insensitive.
function countValues($prefix, $array) {
return count(array_filter($array, function($item) use ($prefix) {
return stripos($item, $prefix) !== false;
}));
}
Usage:
$count = countValues('my-value', $first);
According to your question, "they will always start with my-value- with a number added to the end." So you don't need a regex, just a count of the number of items in your array, using PHP's built-in count() function. Try:
<?php
$first = array( 'my-value-1','my-value-2','my-value-3' );
$second = array( 'my-value-1','my-value-2','my-value-3', 'my-value-4', 'my-value-5' );
$size_of_first = count($first);
$size_of_first = count($first);
echo $size_of_first; //Will echo 3
echo $size_of_first; //Will echo 5
?>

How can I sort an array by two criteria?

I have an array I want to echo alphabetized while ignoring the number that starts each string, as such:
0 Apple
1 Apple
3 Apple
0 Banana
1 Banana
0 Carrot
//...
When I sort, the number is sorted first. So, I've tried asort, sort_string with no success.
$file = file("grades.txt");
asort($file, SORT_STRING);
Can I look only at the alphabet characters and ignore numbers? Or can I ignore the first character and sort starting with the second character? What should I do to get the above result?
It would be great if the numbers could be in order AFTER the arrays are echoed alphabetically, but it is not demanded if too difficult to do.
Maybe try php's uasort function.
http://php.net/manual/en/function.uasort.php
function cmp($a, $b) {
if ($a[2] == $b[2]) {
return 0;
}
return ($a[2] < $b[2]) ? -1 : 1;
}
uasort($array, 'cmp');
You can swap the position of the alphabetic part and numeric part, and use strcmp() to compare the string in usort().
http://php.net/manual/en/function.usort.php
usort($arr, function($a, $b) {
$a = $a[2].' '.$a[0];
$b = $b[2].' '.$b[0];
return strcmp($a, $b);
});
You can use preg_replace() to remove numbers from beginning of strings, preg_replace() accepts third param ( subject ) as an array ( the search and replace is performed on every item ).
$file = preg_replace( '/^[\d\s]+/', '', file("grades.txt") );
arsort( $file );
EDIT:
Use preg_replace( '/^([\d\s]+)(.+)/', '$2 $1', file("grades.txt") ) to shift the numbers to the end of string.
For this you need a custom order function, which you can do with uasort(), e.g.
Simply explode() your string by a space and save the number and the string in a variable. Then if string is the same order the elements by the number. Else sort by the string.
uasort($arr, function($a, $b){
list($numberA, $stringA) = explode(" ", $a);
list($numberB, $stringB) = explode(" ", $b);
if(strnatcmp($stringA, $stringB) == 0)
return $numberA < $numberB ? -1 : 1;
return strnatcmp($stringA, $stringB);
});

How to alphabetically sort a php array after a certain character in a string

I have two php arrays. And have a different sorting question for each of these arrays:
1) First contains list of domains:
values[0] = "absd.com";
values[1] = "bfhgj.org";
values[2] = "sdfgh.net";
values[3] = "sdff.com";
values[4] = "jkuyh.ca";
I need to sort this array alphabetically by DOMAIN value, in other words by the value after the '.', so the sorted domain will be as follows:
values[0] = "jkuyh.ca";
values[1] = "absd.com";
values[2] = "sdff.com";
values[3] = "sdfgh.net";
values[4] = "bfhgj.org";
2) I also have second array that contains "double" domain values:
values[0] = "lkjhg.org.au";
values[1] = "bfhgj.co.uk";
values[2] = "sdfgh.org.uk";
I need to sort this array alphabetically by DOUBLE DOMAIN value, in other words by the value after the first instance of '.' in domain, so the sorted domain will be as follows:
values[1] = "bfhgj.co.uk";
values[0] = "lkjhg.org.au";
values[2] = "sdfgh.org.uk";
How do I tackle this issue? sort() approach sorts only based on first letter...
usort is the answer.
Try this:
usort($values,function($a,$b) {
return strcasecmp(
explode(".",$a,2)[1],
explode(".",$b,2)[1]
);
});
(Note that you will need to store the result of explode in a temporary variable and access that separately, if you're still using PHP 5.3 or older)
Another variant:
usort ($values,
function ($a,$b) {
return strcmp (strstr ($a, '.'), strstr ($b, '.'));
});
this will work for both your arrays, since the comparison uses the part of the string starting at the first '.'
If you want to print them out by groups, I think a good solution would be to first create a suitable data structure and then use it both for sorting and printing:
$values = array ("zercggj.co.uk", "lkjhg.org.au", "qqxze.org.au",
"bfhgj.co.uk", "sdfgh.org.uk");
echo "<br>input:<br>";
foreach ($values as $host) echo "$host<br>";
// create a suitable structure
foreach ($values as $host)
{
$split = explode('.', $host, 2);
$printable[$split[1]][] = $split[0];
}
// sort by domains
asort ($printable);
// output
echo "<br>sorted:<br>";
foreach ($printable as $domain => $hosts)
{
echo "domain: $domain<br>";
// sort hosts within the current domain
asort ($hosts);
// display them
foreach ($hosts as $host)
echo "--- $host<br>";
}
This is a good illustration of how you can benefit from thinking about what you will do with your data as a whole instead on focussing on unconnected sub-tasks (like sorting in that case).
It is import to not only sort on the second half of the string, but then also break ties using the first half of the string. Otherwise, sorting will appear to be unfinished.
To ensure a fully considered sort, sort from the first dot to the end, then sort on the full string to use the substring before the first do.
Code: (Demo)
usort(
$array,
fn($a, $b) =>
[strstr($a, '.'), $a]
<=>
[strstr($b, '.'), $b]
);
var_export($array);

How to match rows in array to an array of masks?

I have array like this:
array('1224*', '543*', '321*' ...) which contains about 17,00 "masks" or prefixes.
I have a second array:
array('123456789', '123456788', '987654321' ....) which contain about 250,000 numbers.
Now, how can I efficiently match every number from the second array using the array of masks/prefixes?
[EDIT]
The first array contains only prefixes and every entry has only one * at the end.
Well, here's a solution:
Prelimary steps:
Sort array 1, cutting off the *'s.
Searching:
For each number in array 2 do
Find the first and last entry in array 1 of which the first character matches that of number (binary search).
Do the same for the second character, this time searching not the whole array but between first and last (binary search).
Repeat 2 for the nth character until a string is found.
This should be O(k*n*log(n)) where n is the average number length (in digits) and k the number of numbers.
Basically this is a 1 dimensional Radix tree, for optimal performance you should implement it, but it can be quite hard.
My two cents....
$s = array('1234*', '543*', '321*');
$f = array('123456789', '123456788', '987654321');
foreach ($f as $haystack) {
echo $haystack."<br>";
foreach ($s as $needle) {
$needle = str_replace("*","",$needle);
echo $haystack "- ".$needle.": ".startsWith($haystack, $needle)."<br>";
}
}
function startsWith($haystack, $needle) {
$length = strlen($needle);
return (substr($haystack, 0, $length) === $needle);
}
To improve performance it might be a good idea to sort both arrays first and to add an exit clause in the inner foreach loop.
By the way, the startWith-function is from this great solution in SO: startsWith() and endsWith() functions in PHP
Another option would to be use preg_grep in a loop:
$masks = array('1224*', '543*', '321*' ...);
$data = array('123456789', '123456788', '987654321' ....);
$matches = array();
foreach($masks as $mask) {
$mask = substr($mask, 0, strlen($masks) - 2); // strip off trailing *
$matches[$mask] = preg_grep("/^$mask/", $data);
}
No idea how efficient this would be, just offering it up as an alternative.
Although regex is not famous for being fast, I'd like to know how well preg_grep() can perform if the pattern is boiled down to its leanest form and only called once (not in a loop).
By removing longer masks which are covered by shorter masks, the pattern will be greatly reduced. How much will the reduction be? of course, I cannot say for sure, but with 17,000 masks, there are sure to be a fair amount of redundancy.
Code: (Demo)
$masks = ['1224*', '543*', '321*', '12245*', '5*', '122488*'];
sort($masks);
$needle = rtrim(array_shift($masks), '*');
$keep[] = $needle;
foreach ($masks as $mask) {
if (strpos($mask, $needle) !== 0) {
$needle = rtrim($mask, '*');
$keep[] = $needle;
}
}
// now $keep only contains: ['1224', '321', '5']
$numbers = ['122456789', '123456788', '321876543234567', '55555555555555555', '987654321'];
var_export(
preg_grep('~^(?:' . implode('|', $keep) . ')~', $numbers)
);
Output:
array (
0 => '122456789',
2 => '321876543234567',
3 => '55555555555555555',
)
Check out the PHP function array_intersect_key.

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

Categories