I have two arrays in PHP as follows:
People:
Array
(
[0] => 3
[1] => 20
)
Wanted Criminals:
Array
(
[0] => 2
[1] => 4
[2] => 8
[3] => 11
[4] => 12
[5] => 13
[6] => 14
[7] => 15
[8] => 16
[9] => 17
[10] => 18
[11] => 19
[12] => 20
)
How do I check if any of the People elements are in the Wanted Criminals array?
In this example, it should return true because 20 is in Wanted Criminals.
You can use array_intersect().
$peopleContainsCriminal = !empty(array_intersect($people, $criminals));
There's little wrong with using array_intersect() and count() (instead of empty).
For example:
$bFound = (count(array_intersect($criminals, $people))) ? true : false;
if 'empty' is not the best choice, what about this:
if (array_intersect($people, $criminals)) {...} //when found
or
if (!array_intersect($people, $criminals)) {...} //when not found
That code is invalid as you can only pass variables into language constructs. empty() is a language construct.
You have to do this in two lines:
$result = array_intersect($people, $criminals);
$result = !empty($result);
Performance test for in_array vs array_intersect:
$a1 = array(2,4,8,11,12,13,14,15,16,17,18,19,20);
$a2 = array(3,20);
$intersect_times = array();
$in_array_times = array();
for($j = 0; $j < 10; $j++)
{
/***** TEST ONE array_intersect *******/
$t = microtime(true);
for($i = 0; $i < 100000; $i++)
{
$x = array_intersect($a1,$a2);
$x = empty($x);
}
$intersect_times[] = microtime(true) - $t;
/***** TEST TWO in_array *******/
$t2 = microtime(true);
for($i = 0; $i < 100000; $i++)
{
$x = false;
foreach($a2 as $v){
if(in_array($v,$a1))
{
$x = true;
break;
}
}
}
$in_array_times[] = microtime(true) - $t2;
}
echo '<hr><br>'.implode('<br>',$intersect_times).'<br>array_intersect avg: '.(array_sum($intersect_times) / count($intersect_times));
echo '<hr><br>'.implode('<br>',$in_array_times).'<br>in_array avg: '.(array_sum($in_array_times) / count($in_array_times));
exit;
Here are the results:
0.26520013809204
0.15600109100342
0.15599989891052
0.15599989891052
0.1560001373291
0.1560001373291
0.15599989891052
0.15599989891052
0.15599989891052
0.1560001373291
array_intersect avg: 0.16692011356354
0.015599966049194
0.031199932098389
0.031200170516968
0.031199932098389
0.031200885772705
0.031199932098389
0.031200170516968
0.031201124191284
0.031199932098389
0.031199932098389
in_array avg: 0.029640197753906
in_array is at least 5 times faster. Note that we "break" as soon as a result is found.
You could also use in_array as follows:
<?php
$found = null;
$people = array(3,20,2);
$criminals = array( 2, 4, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
foreach($people as $num) {
if (in_array($num,$criminals)) {
$found[$num] = true;
}
}
var_dump($found);
// array(2) { [20]=> bool(true) [2]=> bool(true) }
While array_intersect is certainly more convenient to use, it turns out that its not really superior in terms of performance. I created this script too:
<?php
$found = null;
$people = array(3,20,2);
$criminals = array( 2, 4, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
$fastfind = array_intersect($people,$criminals);
var_dump($fastfind);
// array(2) { [1]=> int(20) [2]=> int(2) }
Then, I ran both snippets respectively at: http://3v4l.org/WGhO7/perf#tabs and http://3v4l.org/g1Hnu/perf#tabs and checked the performance of each. The interesting thing is that the total CPU time, i.e. user time + system time is the same for PHP5.6 and the memory also is the same. The total CPU time under PHP5.4 is less for in_array than array_intersect, albeit marginally so.
Here's a way I am doing it after researching it for a while. I wanted to make a Laravel API endpoint that checks if a field is "in use", so the important information is: 1) which DB table? 2) what DB column? and 3) is there a value in that column that matches the search terms?
Knowing this, we can construct our associative array:
$SEARCHABLE_TABLE_COLUMNS = [
'users' => [ 'email' ],
];
Then, we can set our values that we will check:
$table = 'users';
$column = 'email';
$value = 'alice#bob.com';
Then, we can use array_key_exists() and in_array() with eachother to execute a one, two step combo and then act upon the truthy condition:
// step 1: check if 'users' exists as a key in `$SEARCHABLE_TABLE_COLUMNS`
if (array_key_exists($table, $SEARCHABLE_TABLE_COLUMNS)) {
// step 2: check if 'email' is in the array: $SEARCHABLE_TABLE_COLUMNS[$table]
if (in_array($column, $SEARCHABLE_TABLE_COLUMNS[$table])) {
// if table and column are allowed, return Boolean if value already exists
// this will either return the first matching record or null
$exists = DB::table($table)->where($column, '=', $value)->first();
if ($exists) return response()->json([ 'in_use' => true ], 200);
return response()->json([ 'in_use' => false ], 200);
}
// if $column isn't in $SEARCHABLE_TABLE_COLUMNS[$table],
// then we need to tell the user we can't proceed with their request
return response()->json([ 'error' => 'Illegal column name: '.$column ], 400);
}
// if $table isn't a key in $SEARCHABLE_TABLE_COLUMNS,
// then we need to tell the user we can't proceed with their request
return response()->json([ 'error' => 'Illegal table name: '.$table ], 400);
I apologize for the Laravel-specific PHP code, but I will leave it because I think you can read it as pseudo-code. The important part is the two if statements that are executed synchronously.
array_key_exists() and in_array() are PHP functions.
source:
https://php.net/manual/en/function.array-key-exists.php
https://php.net/manual/en/function.in-array.php
The nice thing about the algorithm that I showed above is that you can make a REST endpoint such as GET /in-use/{table}/{column}/{value} (where table, column, and value are variables).
You could have:
$SEARCHABLE_TABLE_COLUMNS = [
'accounts' => [ 'account_name', 'phone', 'business_email' ],
'users' => [ 'email' ],
];
and then you could make GET requests such as:
GET /in-use/accounts/account_name/Bob's Drywall (you may need to uri encode the last part, but usually not)
GET /in-use/accounts/phone/888-555-1337
GET /in-use/users/email/alice#bob.com
Notice also that no one can do:
GET /in-use/users/password/dogmeat1337 because password is not listed in your list of allowed columns for user.
Good luck on your journey.
I've created a clean Helper Function for you to use.
if (!function_exists('array_has_one')) {
/**
* array_has_one
*
* Uses the search array to match at least one of the haystack to return TRUE
*
* #param {array} $search
* #param {array} $haystack
* #return {boolean}
*/
function array_has_one(array $search, array $haystack){
if(!count(array_intersect($search, $haystack)) === FALSE){
return TRUE;
}else{
return FALSE;
}
}
}
you would use this like
if(array_has_one([1,2,3,4,5], [5,6,7,8,9])){
echo "FOUND 5";
}
Related
TL;DR
Sort/group array by key without adding another level to the array (data parsed by jQuery plugin)?
Details
I am building an array to return to some <select> DOM element.
It takes in CC (engine size stuff) as a parameter and uses that as a key, the problem lies with sorting the array after.
Let's say, user selects this range of CC's:
50, 100, 125
50 has 32 options available
100 has 3 options available
125 has 12 options available
My current code loops through the CC's, executes the SQL to get the options and using a loop counter creates the key like this:
$options[$cc. $id] = $someValue;
This works as you'd expect, however my output is showing results not in exactly the order I need (CC ASC - so all 50s should show first, together).
The problem is that
50 with 32 goes upto 5031 as a key.
100 with 3 goes upto 1002 as a key.
125 with 12 goes upto 12511 as a key.
By now hopefully you can clearly see the issue. 5031 is greater than 1002. So options for 50cc with a loop counter passed 9 is greater than 100cc options.
(just for clarity, example output is):
50cc Option 1
50cc Option 2
50cc Option 3
50cc Option 4
50cc Option 5
100cc Option 1
100cc Option 2
100cc Option 3
50cc Option 6
50cc Option 7
Maybe the initial problem is how I'm creating the keys, but I've tried to use ksort with a few different flags to try and achieve my goal but none of the flags seem to target what I'm after:
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
How do I sort/group my keys without adding another level to my array (the data is parsed by a jQuery plugin that needs the data in a certain format)?
EDIT: Full Script
<?php
if (strpos(PHP_OS, 'Linux') > -1) {
require_once $_SERVER['DOCUMENT_ROOT']. '/app/connect.php';
} else {
require_once getcwd(). '\\..\\..\\..\\..\\app\\connect.php';
}
$make = $_POST['make'];
$cc = $_POST['cc'];
$sql = 'SELECT * FROM `table`
WHERE `UKM_CCM` = :cc
AND `UKM_Make` = :make
ORDER BY `UKM_Model`, `UKM_StreetName`, `Year` ASC;';
$options = array();
foreach ($cc as $k => $value)
{
$res = $handler->prepare($sql);
$res->execute(array(':cc' => $value, ':make' => $make));
$data = $res->fetchAll(PDO::FETCH_ASSOC);
$i = 0;
if (count($data) > 0) {
foreach ($data as $result)
{
$arrayKey = sprintf('%03d%02d', $cc, $i);
$epid = $result['ePID'];
$make = $result['UKM_Make'];
$model = $result['UKM_Model'];
$cc = $result['UKM_CCM'];
$year = $result['Year'];
$sub = $result['UKM_Submodel'];
$street = $result['UKM_StreetName'];
$options[$arrayKey]['name'] = $make. ' ' .$model. ' ' .$cc. ' ' .$year. ' ' .$sub. ' ' .$street;
$options[$arrayKey]['value'] = $epid;
$options[$arrayKey]['checked'] = false;
$options[$arrayKey]['attributes']['data-epid'] = $epid;
$options[$arrayKey]['attributes']['data-make'] = $make;
$options[$arrayKey]['attributes']['data-model'] = $model;
$options[$arrayKey]['attributes']['data-cc'] = $cc;
$options[$arrayKey]['attributes']['data-year'] = $year;
$options[$arrayKey]['attributes']['data-sub'] = $sub;
$options[$arrayKey]['attributes']['data-street'] = $street;
$i++;
}
}
}
ksort($options, SORT_STRING);
echo json_encode($options);
You could format the key to have 3 digits for the cc and 2 for the option...
$options[sprintf('%03d%02d', $cc, $id)] = $someValue;
which should give you keys 05031 and 10002.
Then use SORT_STRING to force it to sort them as strings (although they would sort as numbers as well)
If you can add the key additional into your array, you could create a usort() that will sort your array as you need it:
$arrSort = [50, 100, 125];
$arrData = [
501 => [
'foo',
'bar',
'arrayKey' => 501
],
504 => [
'foo',
'bar',
'arrayKey' => 504
],
1002 => [
'foo',
'bar',
'arrayKey' => 1002
],
10045 => [
'foo',
'bar',
'arrayKey' => 10045
],
1251 => [
'foo',
'bar',
'arrayKey' => 1251
],
5045 => [
'foo',
'bar',
'arrayKey' => 5045
]
];
usort($arrData, function($a, $b) use ($arrSort)
{
$posA = array_search(substr($a['arrayKey'], 0, 2), $arrSort);
if ($posA === false) {
$posA = array_search(substr($a['arrayKey'], 0, 3), $arrSort);
}
$posB = array_search(substr($b['arrayKey'], 0, 2), $arrSort);
if ($posB === false) {
$posB = array_search(substr($b['arrayKey'], 0, 3), $arrSort);
}
return $posA - $posB;
});
The return of this function in this example would be:
array:6 [▼ 0 => array:3 [▼
0 => "foo"
1 => "bar"
"arrayKey" => 501 ] 1 => array:3 [▼
0 => "foo"
1 => "bar"
"arrayKey" => 504 ] 2 => array:3 [▼
0 => "foo"
1 => "bar"
"arrayKey" => 5045 ] 3 => array:3 [▼
0 => "foo"
1 => "bar"
"arrayKey" => 1002 ] 4 => array:3 [▼
0 => "foo"
1 => "bar"
"arrayKey" => 10045 ] 5 => array:3 [▼
0 => "foo"
1 => "bar"
"arrayKey" => 1251 ] ]
Make your 'faux delimiter' an uncommon pattern like "929292" in this example. Then you can use uksort to go through just your keys. replace "929292" with "." so you end up with something like "100.3", "125.12" and "150.32".
Now you're no longer limited to trying to work numerically and with patterns, you can use a good old explode and compare manually.
This solution doesn't care what's nested in your arrays, it's only concerned with the keys for sorting.
$options = [
'1009292923' => '100cc',
'12592929212' => '150cc',
'5092929232' => '50cc'
];
$sorted = uksort($options, function($a, $b){
$parts = explode('.',str_replace('929292', '.', $a));
$acc = $parts[0];
$parts = explode('.',str_replace('929292', '.', $b));
$bcc = $parts[0];
if($acc == $bcc) { return 0; }
if($acc > $bcc) { return 1; }
if($acc < $bcc) { return -1; }
});
var_dump($options);
Edit: str_replace is kind of pointless here, you could just run explode directly on the key using "929292" as your delimeter.
I'm not sure this could be an exact answer to my own question as it handles the issue but in a different way. Essentially, I've changed my SQL to this:
$sql = 'SELECT * FROM `ebay_mml`
WHERE `UKM_CCM` IN ('. $where .')
AND `UKM_Make` = :make
ORDER BY CAST(SUBSTR(`UKM_CCM`, INSTR(`UKM_CCM`, " ") + 1) AS UNSIGNED),
`UKM_Model`,
`UKM_StreetName`,
`Year`
ASC;';
$where is a variable generated from a foreach loop:
foreach ($cc as $k => $v)
{
$where .= ':'. $k .($k != end(array_keys($cc)) ? ', ' : '');
$whereData[':'. $k] = $v;
}
This returns all my data at once, so all I need to do now is loop through the results and count the iterations as I go to build the key:
$i = 0;
foreach ($data as $result)
{
# my sexy code
$i++;
}
Now my results are as I want them.
Disclaimer: As this does resolve the issue at hand, it kinda veers away from the original question posed as it's more of a MySQL solution as opposed to sorting/grouping the array by it's key value. Let me know if this answer is ok (if so, will remove the disclaimer segment) or not.
Thanks for your help all :)
I'm creating a LDAP directory search using PHP.
I'm able to successfully search and return results.
I want to be able to sort results according to hierarchy.
Now I have:
ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
ou=IT,ou=Employees,ou=People,dc=instatsport,dc=com
ou=Video,ou=Employees,ou=People,dc=instatsport,dc=com
ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
ou=aHR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
ou=HR2,ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
Using php.net/manual/en/function.ldap-sort.php what filter should be applied to get this result?:
ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
ou=HR2,ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
ou=aHR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
ou=IT,ou=Employees,ou=People,dc=instatsport,dc=com
ou=Video,ou=Employees,ou=People,dc=instatsport,dc=com
You can't with ldap_sort.
What this function do is apply an strcmp on an attribute of the entries returned.
Here you try to order the DN of the entries returned, and not on an alphabetical way, but on the hierarchical level.
You have to implement this (or find someone who already did this).
The easiest implementation would be to create a tree with all the RDN and print this tree from root to leaf recursively imploding each RDN with a comma.
EDIT :
I took some time to try to implement it quickly, this is not the best code you can find, but it can ba a base for what you want to achieve :
function hierarchySort ($a, $b){
$rdn1 = ldap_explode_dn($a,0);
$rdn2 = ldap_explode_dn($b,0);
$count = $rdn1["count"] < $rdn2["count"] ? $rdn1["count"] : $rdn2["count"];
for ($i=0; $i<$count; $i++) {
$j1 = $rdn1["count"] - 1 - $i;
$j2 = $rdn2["count"] - 1 - $i;
$ret = strnatcasecmp($rdn1[$j1], $rdn2[$j2]);
if ($ret != 0) break;
}
if ($rdn1["count"] == $rdn2["count"]) {
return $ret;
}else {
if ($ret != 0) return $ret;
else return $rdn1["count"] < $rdn2["count"] ? -1 : 1;
}
}
// Some simulated LDAP result
$entries = [
0 => [ "dn" => "ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com" ],
1 => [ "dn" => "ou=IT,ou=Employees,ou=People,dc=instatsport,dc=com" ],
2 => [ "dn" => "ou=Video,ou=Employees,ou=People,dc=instatsport,dc=com" ],
3 => [ "dn" => "ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com" ],
4 => [ "dn" => "ou=aHR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com" ],
5 => [ "dn" => "ou=HR2,ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com" ],
"count" => 6
];
for ($i=0; $i<$entries['count']; $i++) {
$e = $entries[$i];
$dns[] = $e["dn"];
}
print_r($dns);
usort($dns, 'hierarchySort');
print_r($dns);
Ouput :
Array
(
[0] => ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
[1] => ou=IT,ou=Employees,ou=People,dc=instatsport,dc=com
[2] => ou=Video,ou=Employees,ou=People,dc=instatsport,dc=com
[3] => ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
[4] => ou=aHR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
[5] => ou=HR2,ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
)
Array
(
[0] => ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
[1] => ou=aHR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
[2] => ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
[3] => ou=HR2,ou=HR1,ou=HR,ou=Employees,ou=People,dc=instatsport,dc=com
[4] => ou=IT,ou=Employees,ou=People,dc=instatsport,dc=com
[5] => ou=Video,ou=Employees,ou=People,dc=instatsport,dc=com
)
ldap_sort is deprecated by now as it only sorts the result set returned by the server. For small result sets that might not be an issue, but on larger (paged) result sets that means that you get results from "a" to "z" on the first page which will then be sorted. And on the second page you will also get results from "a" to "z" that will then be sorted and so on. Therefore the sort does not really what most people expect.
Also ldap_sort can only sort on fields that where actually retrieved. So you are not able to only retrieve the "first name" and sort by "second name".
To answer your question: I would write a sort function like the following and use that with f.e. usort:
function sortByDn($a, $b) {
return strnatcasecmp($a['dn'], $b['dn']);
}
$result = ldap_get_Entries($handle, $resulthandle);
unset($result['count']);
usort($result, 'sortByDn');
Something along these lines might actually help ;)
Depending on what you want to sort by your sort function might need to be different!
I have associative array like
array
{
[company 1]=>array
(
[1981] => 1
[1945] => 3
)
[company 2]=>array
(
[1990] => 18
[2005] => 13
)
[company 3]=>array
(
[1950] => 6
[2012] => 9
)
}
I want to get lowest and highest key i.e. 1945 and 2012.
How can i achieve this? I have already searched over stackoverflow and Hightest value of an associative array is the nearest possibility but it gives out min and max value and I want min and max key.
**I don't want to use foreach loop **
If you really hate foreach, here's a solution:
$arr = array(
"Company 1" => array(
"1981" => 1,
"1945" => 3
),
"Company 2" => array(
"1990" => 18,
"2005" => 13
),
"Company 3" => array(
"1950" => 6,
"2012" => 9
)
);
$arr = array_map("array_keys", $arr);
$arr = array_reduce($arr, "array_merge", array());
Your $arr would end up like this:
Array
(
[0] => 1981
[1] => 1945
[2] => 1990
[3] => 2005
[4] => 1950
[5] => 2012
)
Now you can use min() and max() functions or sort() it get the highest and lowest value easily.
sort($arr);
echo end($arr); /*highest value; actual output: 2012*/
echo reset($arr); /*lowest value; actual output: 1945*/
Try this one:
using foreach.
$array = array("company 1" => array(1981 => 1, 1945 =>3),
"company 2" => array(1990 => 18, 2005 => 13),
"company 3" => array(1950 => 6, 2012 =>9),
);
$keys = array();
foreach($array as $arr)
{
foreach( array_keys($arr) as $val)
{
array_push($keys, $val);
}
}
sort($keys);
$min = $keys[0];
$max = $keys[count($keys)-1];
Without foreach:
global $keys;
$GLOBALS['keys'] = array();
function sortme($arr)
{
is_array($arr)? array_map("sortme",array_keys($arr)): array_push($GLOBALS['keys'], $arr);
}
array_map("sortme",$array);
sort($GLOBALS['keys']);
$min = $GLOBALS['keys'][0];
$max = $GLOBALS['keys'][count($GLOBALS['keys'])-1];
echo "min = ".$min . "<br/>max = ".$max;
ksort($array);
$min = reset($array);
end($array);
$max = key($array);
EDIT:
This works for simple array. You have 2 level structure, so it would be close to impossible to avoid looping through it.
However, if you have so many entries that even foreach is too slow, probably you should rethink your approach. For example, put this list into database and use SQL to do heavy lifting for you.
How exactly? Setup instance of MySQL or PostgreSQL (I personally prefer Postgres for many reasons), create database, create table with structure like this:
CREATE TABLE mytable (
mytable_id INTEGER PRIMARY KEY,
company VARCHAR(16),
year INTEGER,
value INTEGER,
-- put some more metadata...
)
Technically, you should normalize your database and create separate tables for every object (like table for companies, for customers, orders, etc...), but you can come to this later.
Create indexes on columns you are going to be searching on (like year, company, etc):
CREATE INDEX mytable_year_idx ON mytable (year);
...
Finally, in your PHP script, connect to database and query it for what you wanted, something like this:
SELECT min(year) AS min_year,
max(year) AS max_year
FROM mytable
Because your input array is "very large" and "consumes [a] lot of time", that is precisely a justification for using a set of simple language constructs to iterate the dataset in a single pass.
Using multiple array functions may look more concise or clever, but they will be making multiple passes over the data and invariably draw more resources than absolutely necessary.
Because your data is dealing with years, you can build business logic into the code logic with "magic numbers" -- assuming that you will never encounter a negative year or a year from 10,000 and beyond.
If you want a more business-agnostic approach, you can seed the default min and max values by shifting the first row off the input array and fetching that subarray's min and max keys.
Magic Number Code: (Demo)
$minYear = 9999;
$maxYear = 0;
foreach ($array as $row) {
foreach ($row as $year => $value) {
if ($year > $maxYear) {
$maxYear = $year;
}
if ($year < $minYear) {
$minYear = $year;
}
}
}
echo "MinYear = $minYear, MaxYear = $maxYear";
// MinYear = 1945, MaxYear = 2012
Shifted Defaults Code: (Demo)
if ($array) {
$firstKeys = array_keys(array_shift($array));
$minYear = min($firstKeys);
$maxYear = max($firstKeys);
} else {
$minYear = null;
$maxYear = null;
}
foreach ($array as $row) {
foreach ($row as $year => $value) {
if ($year > $maxYear) {
$maxYear = $year;
} elseif ($year < $minYear) {
$minYear = $year;
}
}
}
echo "MinYear = $minYear, MaxYear = $maxYear";
Notice that the Shifted snippet specifically handles the first dataset to get the defaults - which takes a few extra lines - but enjoys higher potential efficiency due to the elseif.
Neither of these techniques use iterated function calls, so they are virtually assured to perform faster than functional techniques.
This is probably a real simple question but I'm looking for the most memory efficient way of finding out data on a particular multi-dimensional array.
An example of the array:
[0] => Array(
[fin] => 2
[calc] => 23.34
[pos] => 6665
)
[1] => Array(
[fin] => 1
[calc] => 25.14
[pos] => 4543
)
[2] => Array(
[fin] => 7
[calc] => 21.45
[pos] => 4665
)
I need a method of identifying the values of the following things:
The max 'calc'
The min 'calc'
The max 'pos'
The min 'pos'
(you get the gist)
The only way I can think of is manually looping through each value and adjusting an integer so for example:
function find_lowest_calc($arr) {
$int = null;
foreach($arr['calc'] as $value) {
if($int == null || $int > $value) {
$int = $value;
}
}
return $int;
}
The obvious drawbacks of a method like this is I would have to create a new function for each value in the array (or at least implement a paramater to change the array key) and it will slow up the app by looping through the whole array 3 or more times just to get the values. The original array could have over a hundred values.
I would assume that there would be an internal function to gather all of (for example) the 'calc' values into a temporary single array so I could use the max function on it.
Any ideas?
Dan
$input = array(
array(
'fin' => 2
'calc' => 23.34
'pos' => 6665
),
array(
'fin' => 1
'calc' => 25.14
'pos' => 4543
),
array(
'fin' => 7
'calc' => 21.45
'pos' => 4665
)
);
$output = array(
'fin' => array(),
'calc' => array(),
'pos' => array(),
);
foreach ( $input as $data ) {
$output['fin'][] = $data['fin'];
$output['calc'][] = $data['calc'];
$output['pos'][] = $data['pos'];
}
max($output['fin']); // max fin
max($output['calc']); // max calc
min($output['fin']); // min fin
There is no way to speed that up, besides calculating all three values at once. The reason for this is that you always need to loop through the array to find the sub-arrays. Even if you find a bultin function, the time complexity will be the same. The only way to make a real difference is by using another datastructure (i.e, not any of the bultin ones, but one you write yourself).
How are you receiving the array? If it is your code which is creating the array, you could calculate the minimum and maximum values as you are reading in the data values:
$minCalc = null;
$arr = array();
for(...){
//read in 'calc' value
$subArr = array();
$subArr['calc'] = //...
if ($minCalc === null || $minCalc > $subArr['calc']){
$minCalc = $subArr['calc'];
}
//read in other values
//...
$arr[] = $subArr;
}
Also, in your find_lowest_calc function, you should use the triple equals operator (===) to determine whether the $int variable is null. This is because the statement $int == null will also return true if $int equals 0, because null equals 0 when converted to an integer.
You don't have to crate a new function for each value, just pass the key you want in the function
function find_lowest($arr, $indexkey) {
$int = null;
foreach($arr[$indexkey] as $value) {
if($int == null || $int > $value) {
$int = $value;
}
}
return $int;
}
As php is not type-safe, you should be fine passing both string or int index
This question already has answers here:
How can I sort arrays and data in PHP?
(14 answers)
Closed 8 years ago.
I have CSV data loaded into a multidimensional array. In this way each "row" is a record and each "column" contains the same type of data. I am using the function below to load my CSV file.
function f_parse_csv($file, $longest, $delimiter)
{
$mdarray = array();
$file = fopen($file, "r");
while ($line = fgetcsv($file, $longest, $delimiter))
{
array_push($mdarray, $line);
}
fclose($file);
return $mdarray;
}
I need to be able to specify a column to sort so that it rearranges the rows. One of the columns contains date information in the format of Y-m-d H:i:s and I would like to be able to sort with the most recent date being the first row.
Introducing: a very generalized solution for PHP 5.3+
I 'd like to add my own solution here, since it offers features that other answers do not.
Specifically, advantages of this solution include:
It's reusable: you specify the sort column as a variable instead of hardcoding it.
It's flexible: you can specify multiple sort columns (as many as you want) -- additional columns are used as tiebreakers between items that initially compare equal.
It's reversible: you can specify that the sort should be reversed -- individually for each column.
It's extensible: if the data set contains columns that cannot be compared in a "dumb" manner (e.g. date strings) you can also specify how to convert these items to a value that can be directly compared (e.g. a DateTime instance).
It's associative if you want: this code takes care of sorting items, but you select the actual sort function (usort or uasort).
Finally, it does not use array_multisort: while array_multisort is convenient, it depends on creating a projection of all your input data before sorting. This consumes time and memory and may be simply prohibitive if your data set is large.
The code
function make_comparer() {
// Normalize criteria up front so that the comparer finds everything tidy
$criteria = func_get_args();
foreach ($criteria as $index => $criterion) {
$criteria[$index] = is_array($criterion)
? array_pad($criterion, 3, null)
: array($criterion, SORT_ASC, null);
}
return function($first, $second) use (&$criteria) {
foreach ($criteria as $criterion) {
// How will we compare this round?
list($column, $sortOrder, $projection) = $criterion;
$sortOrder = $sortOrder === SORT_DESC ? -1 : 1;
// If a projection was defined project the values now
if ($projection) {
$lhs = call_user_func($projection, $first[$column]);
$rhs = call_user_func($projection, $second[$column]);
}
else {
$lhs = $first[$column];
$rhs = $second[$column];
}
// Do the actual comparison; do not return if equal
if ($lhs < $rhs) {
return -1 * $sortOrder;
}
else if ($lhs > $rhs) {
return 1 * $sortOrder;
}
}
return 0; // tiebreakers exhausted, so $first == $second
};
}
How to use
Throughout this section I will provide links that sort this sample data set:
$data = array(
array('zz', 'name' => 'Jack', 'number' => 22, 'birthday' => '12/03/1980'),
array('xx', 'name' => 'Adam', 'number' => 16, 'birthday' => '01/12/1979'),
array('aa', 'name' => 'Paul', 'number' => 16, 'birthday' => '03/11/1987'),
array('cc', 'name' => 'Helen', 'number' => 44, 'birthday' => '24/06/1967'),
);
The basics
The function make_comparer accepts a variable number of arguments that define the desired sort and returns a function that you are supposed to use as the argument to usort or uasort.
The simplest use case is to pass in the key that you 'd like to use to compare data items. For example, to sort $data by the name item you would do
usort($data, make_comparer('name'));
See it in action.
The key can also be a number if the items are numerically indexed arrays. For the example in the question, this would be
usort($data, make_comparer(0)); // 0 = first numerically indexed column
See it in action.
Multiple sort columns
You can specify multiple sort columns by passing additional parameters to make_comparer. For example, to sort by "number" and then by the zero-indexed column:
usort($data, make_comparer('number', 0));
See it in action.
Advanced features
More advanced features are available if you specify a sort column as an array instead of a simple string. This array should be numerically indexed, and must contain these items:
0 => the column name to sort on (mandatory)
1 => either SORT_ASC or SORT_DESC (optional)
2 => a projection function (optional)
Let's see how we can use these features.
Reverse sort
To sort by name descending:
usort($data, make_comparer(['name', SORT_DESC]));
See it in action.
To sort by number descending and then by name descending:
usort($data, make_comparer(['number', SORT_DESC], ['name', SORT_DESC]));
See it in action.
Custom projections
In some scenarios you may need to sort by a column whose values do not lend well to sorting. The "birthday" column in the sample data set fits this description: it does not make sense to compare birthdays as strings (because e.g. "01/01/1980" comes before "10/10/1970"). In this case we want to specify how to project the actual data to a form that can be compared directly with the desired semantics.
Projections can be specified as any type of callable: as strings, arrays, or anonymous functions. A projection is assumed to accept one argument and return its projected form.
It should be noted that while projections are similar to the custom comparison functions used with usort and family, they are simpler (you only need to convert one value to another) and take advantage of all the functionality already baked into make_comparer.
Let's sort the example data set without a projection and see what happens:
usort($data, make_comparer('birthday'));
See it in action.
That was not the desired outcome. But we can use date_create as a projection:
usort($data, make_comparer(['birthday', SORT_ASC, 'date_create']));
See it in action.
This is the correct order that we wanted.
There are many more things that projections can achieve. For example, a quick way to get a case-insensitive sort is to use strtolower as a projection.
That said, I should also mention that it's better to not use projections if your data set is large: in that case it would be much faster to project all your data manually up front and then sort without using a projection, although doing so will trade increased memory usage for faster sort speed.
Finally, here is an example that uses all the features: it first sorts by number descending, then by birthday ascending:
usort($data, make_comparer(
['number', SORT_DESC],
['birthday', SORT_ASC, 'date_create']
));
See it in action.
You can use array_multisort()
Try something like this:
foreach ($mdarray as $key => $row) {
// replace 0 with the field's index/key
$dates[$key] = $row[0];
}
array_multisort($dates, SORT_DESC, $mdarray);
For PHP >= 5.5.0 just extract the column to sort by. No need for the loop:
array_multisort(array_column($mdarray, 0), SORT_DESC, $mdarray);
With usort. Here's a generic solution, that you can use for different columns:
class TableSorter {
protected $column;
function __construct($column) {
$this->column = $column;
}
function sort($table) {
usort($table, array($this, 'compare'));
return $table;
}
function compare($a, $b) {
if ($a[$this->column] == $b[$this->column]) {
return 0;
}
return ($a[$this->column] < $b[$this->column]) ? -1 : 1;
}
}
To sort by first column:
$sorter = new TableSorter(0); // sort by first column
$mdarray = $sorter->sort($mdarray);
Multiple row sorting using a closure
Here's another approach using uasort() and an anonymous callback function (closure). I've used that function regularly. PHP 5.3 required – no more dependencies!
/**
* Sorting array of associative arrays - multiple row sorting using a closure.
* See also: http://the-art-of-web.com/php/sortarray/
*
* #param array $data input-array
* #param string|array $fields array-keys
* #license Public Domain
* #return array
*/
function sortArray( $data, $field ) {
$field = (array) $field;
uasort( $data, function($a, $b) use($field) {
$retval = 0;
foreach( $field as $fieldname ) {
if( $retval == 0 ) $retval = strnatcmp( $a[$fieldname], $b[$fieldname] );
}
return $retval;
} );
return $data;
}
/* example */
$data = array(
array( "firstname" => "Mary", "lastname" => "Johnson", "age" => 25 ),
array( "firstname" => "Amanda", "lastname" => "Miller", "age" => 18 ),
array( "firstname" => "James", "lastname" => "Brown", "age" => 31 ),
array( "firstname" => "Patricia", "lastname" => "Williams", "age" => 7 ),
array( "firstname" => "Michael", "lastname" => "Davis", "age" => 43 ),
array( "firstname" => "Sarah", "lastname" => "Miller", "age" => 24 ),
array( "firstname" => "Patrick", "lastname" => "Miller", "age" => 27 )
);
$data = sortArray( $data, 'age' );
$data = sortArray( $data, array( 'lastname', 'firstname' ) );
I know it's 2 years since this question was asked and answered, but here's another function that sorts a two-dimensional array. It accepts a variable number of arguments, allowing you to pass in more than one key (ie column name) to sort by. PHP 5.3 required.
function sort_multi_array ($array, $key)
{
$keys = array();
for ($i=1;$i<func_num_args();$i++) {
$keys[$i-1] = func_get_arg($i);
}
// create a custom search function to pass to usort
$func = function ($a, $b) use ($keys) {
for ($i=0;$i<count($keys);$i++) {
if ($a[$keys[$i]] != $b[$keys[$i]]) {
return ($a[$keys[$i]] < $b[$keys[$i]]) ? -1 : 1;
}
}
return 0;
};
usort($array, $func);
return $array;
}
Try it here: http://www.exorithm.com/algorithm/view/sort_multi_array
You can sort an array using usort function.
$array = array(
array('price'=>'1000.50','product'=>'product 1'),
array('price'=>'8800.50','product'=>'product 2'),
array('price'=>'200.0','product'=>'product 3')
);
function cmp($a, $b) {
return $a['price'] > $b['price'];
}
usort($array, "cmp");
print_r($array);
Output :
Array
(
[0] => Array
(
[price] => 134.50
[product] => product 1
)
[1] => Array
(
[price] => 2033.0
[product] => product 3
)
[2] => Array
(
[price] => 8340.50
[product] => product 2
)
)
Example
Here is a php4/php5 class that will sort one or more fields:
// a sorter class
// php4 and php5 compatible
class Sorter {
var $sort_fields;
var $backwards = false;
var $numeric = false;
function sort() {
$args = func_get_args();
$array = $args[0];
if (!$array) return array();
$this->sort_fields = array_slice($args, 1);
if (!$this->sort_fields) return $array();
if ($this->numeric) {
usort($array, array($this, 'numericCompare'));
} else {
usort($array, array($this, 'stringCompare'));
}
return $array;
}
function numericCompare($a, $b) {
foreach($this->sort_fields as $sort_field) {
if ($a[$sort_field] == $b[$sort_field]) {
continue;
}
return ($a[$sort_field] < $b[$sort_field]) ? ($this->backwards ? 1 : -1) : ($this->backwards ? -1 : 1);
}
return 0;
}
function stringCompare($a, $b) {
foreach($this->sort_fields as $sort_field) {
$cmp_result = strcasecmp($a[$sort_field], $b[$sort_field]);
if ($cmp_result == 0) continue;
return ($this->backwards ? -$cmp_result : $cmp_result);
}
return 0;
}
}
/////////////////////
// usage examples
// some starting data
$start_data = array(
array('first_name' => 'John', 'last_name' => 'Smith', 'age' => 10),
array('first_name' => 'Joe', 'last_name' => 'Smith', 'age' => 11),
array('first_name' => 'Jake', 'last_name' => 'Xample', 'age' => 9),
);
// sort by last_name, then first_name
$sorter = new Sorter();
print_r($sorter->sort($start_data, 'last_name', 'first_name'));
// sort by first_name, then last_name
$sorter = new Sorter();
print_r($sorter->sort($start_data, 'first_name', 'last_name'));
// sort by last_name, then first_name (backwards)
$sorter = new Sorter();
$sorter->backwards = true;
print_r($sorter->sort($start_data, 'last_name', 'first_name'));
// sort numerically by age
$sorter = new Sorter();
$sorter->numeric = true;
print_r($sorter->sort($start_data, 'age'));
Before I could get the TableSorter class to run I had came up with a function based on what Shinhan had provided.
function sort2d_bycolumn($array, $column, $method, $has_header)
{
if ($has_header) $header = array_shift($array);
foreach ($array as $key => $row) {
$narray[$key] = $row[$column];
}
array_multisort($narray, $method, $array);
if ($has_header) array_unshift($array, $header);
return $array;
}
$array is the MD Array you want to sort.
$column is the column you wish to sort by.
$method is how you want the sort performed, such as SORT_DESC
$has_header is set to true if the first row contains header values that you don't want sorted.
The "Usort" function is your answer.
http://php.net/usort
I tried several popular array_multisort() and usort() answers and none of them worked for me. The data just gets jumbled and the code is unreadable. Here's a quick a dirty solution. WARNING: Only use this if you're sure a rogue delimiter won't come back to haunt you later!
Let's say each row in your multi array looks like: name, stuff1, stuff2:
// Sort by name, pull the other stuff along for the ride
foreach ($names_stuff as $name_stuff) {
// To sort by stuff1, that would be first in the contatenation
$sorted_names[] = $name_stuff[0] .','. name_stuff[1] .','. $name_stuff[2];
}
sort($sorted_names, SORT_STRING);
Need your stuff back in alphabetical order?
foreach ($sorted_names as $sorted_name) {
$name_stuff = explode(',',$sorted_name);
// use your $name_stuff[0]
// use your $name_stuff[1]
// ...
}
Yeah, it's dirty. But super easy, won't make your head explode.