Sort/group array by key - php

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

Related

I cant figure out this array_map issue I am having

**EDIT:
I am trying to display the number of keys in my arrays that start with a "P", "M" and "D". I think I should be using array_maps and have some luck with it but I am now stuck and tried looking through the manual, on here and w3schools with no luck.
I'm using version 5.6.36 of PHP with XAMPP on a local server. I've tried playing around with array_maps which I think is the right command to use, but I just cant get my head around how to use it properly. I've read the manual on it, looked on here, looked on youtube and W3Schools with no luck. Can anyone help please?
I have this array:
$tasks = array
(
0 => 'P1',
1 => 'M1',
2 => 'D1',
3 => 'P2',
4 => 'D2'
);
I want it to display this:
Array
(
[P] => 2
[M] => 1
[D] => 2
)
See how it returns the number of P's M's and D's nice and neatly?
From what I understand, the solution code should be something like this:
$array2 = array_map(function(???????){
return ??????????;
}, $tasks);
$array2a = (array_count_values($array2));
echo "<pre>"; print_r($array2a); echo "</pre>";
Please help?!
you can use array_map as following :
$tasks = array
(
0 => 'P1',
1 => 'M1',
2 => 'D1',
3 => 'P2',
4 => 'D2'
);
$charsToCheck = array('P','M','D');
$result = array_map(function($v) use ($charsToCheck){
if(in_array(substr( $v, 0, 1),$charsToCheck))
return substr( $v, 0, 1);
}, $tasks);
print_r(array_count_values($result));
Result:-
Array
(
[P] => 2
[M] => 1
[D] => 2
)
The function array_map() creates one output element from every input element. Since you don't want that, it is the wrong tool. Probably the easiest way to achieve your goal is to use a simple loop. However, if things get more complicated, this may not scale well. For those cases, array_reduce() could come in handy:
$input = [
0 => 'P1',
1 => 'M1',
2 => 'D1',
3 => 'P2',
4 => 'D2',
];
$frequency = array_reduce(
$input,
function ($carry, $item) {
$initial = substr($item, 0, 1);
if (array_key_exists($initial, $carry)) {
$carry[$initial] += 1;
}
return $carry;
},
[
'P' => 0,
'M' => 0,
'D' => 0,
]
);
echo json_encode($frequency, JSON_PRETTY_PRINT) . PHP_EOL;
The point of this is that it defines what to do with a single element ($item) and how to modify the resulting state ($carry) in a single function, keeping this part away from the iteration part. Since this avoids mutable state, this can also be seen as a functional (as in "functional programming") approach.
You cannot use array_map for that... You could use reduce I guess but here's a fast and easy way... Basically you create your new array and do the counting according to the first letter of your tasks array.
$list = new Array();
foreach($tasks as $task){
if($list[$task{0}]){
$list[$task{0}]++;
}else{
$list[$task{0}] = 1;
}
}
The problem you'd get with array_map is that it would always produce a 1:1 ratio of your array, which is not what you want...
(sorry for the bad PHP if it is, been ages...)
EDIT:
Using your edited question, here's your possible usage:
$array2 = array_map(function($val){
return $val{0};
}, $tasks);
The key to both answers is the $var{0} part, this extracts the character at index 0...

From a 2D array, how to create groups (i.e. arrays) of inner arrays which have the same value for a particular field?

I have the following array:
fruits = [
0 => ["color"=>"red", "name"=>"apple"],
1 => ["color"=>"red", "name"=>"tomato"],
2 => ["color"=>"green", "name"=>"kiwi"],
3 => ["color"=>"red", "name"=>"carrot"],
4 => ["color"=>"yellow", "name"=>"banana"],
5 => ["color"=>"yellow", "name"=>"mango"],
];
And I need to get it into the following form:
fruits = [
0 => [
0 => ["color"=>"red", "name"=>"apple"],
1 => ["color"=>"red", "name"=>"tomato"],
2 => ["color"=>"red", "name"=>"carrot"]
],
1 => [
0 => ["color"=>"yellow", "name"=>"banana"],
1 => ["color"=>"yellow", "name"=>"mango"],
],
2 => [
0 => ["color"=>"green", "name"=>"kiwi"],
]
];
That is I need to group on the basis of color of the fruit.
I tried but somehow I can not get it correct.
An associative array would be simple :
$sortedFruits = array();
for ($i = 0; $i < count($fruits); $i++) {
if(!$sortedFruits[$fruits[$i]['color']]) { $sortedFruits[$fruits[$i]['color']] = array(); }
array_push($sortedFruits[$fruits[$i]['color']], $fruits[$i]);
}
print_r($sortedFruits['red']);
One option is to loop the colors by extracting names and colors to seperate flat arrays.
This way you only loop the count of unique colors (in this case three times).
Then you do the matching with array_intersect.
$colors = array_column($fruits, "color");
$names = array_column($fruits, "name");
foreach(array_unique($colors) as $color) $new[$color] = array_intersect_key($names, array_intersect($colors, [$color]));
var_dump($new);
https://3v4l.org/5pODc
You're overcomplicating the resulting array, I would suggest to replace first level key with color name and just go with names in each of them.
Building a code logic for your example will be way to big and it doesn't need to be.

Split multidimensional array into arrays

So I have a result from a form post that looks like this:
$data = [
'id_1' => [
'0' => 1,
'1' => 2
],
'id_2' => [
'0' => 3,
'1' => 4
],
'id_3' => [
'0' => 5,
'1' => 6
]
];
What I want to achieve is to split this array into two different arrays like this:
$item_1 = [
'id_1' => 1,
'id_2' => 3,
'id_3' => 5
]
$item_2 = [
'id_1' => 2,
'id_2' => 4,
'id_3' => 6
]
I've tried using all of the proper array methods such as array_chunk, array_merge with loops but I can't seem to get my mind wrapped around how to achieve this. I've seen a lot of similar posts where the first keys doesn't have names like my array does (id_1, id_2, id_3). But in my case the names of the keys are crucial since they need to be set as the names of the keys in the individual arrays.
Much shorter than this will be hard to find:
$item1 = array_map('reset', $data);
$item2 = array_map('end', $data);
Explanation
array_map expects a callback function as its first argument. In the first line this is reset, so reset will be called on every element of $data, effectively taking the first element values of the sub arrays. array_map combines these results in a new array, keeping the original keys.
The second line does the same, but with the function end, which effectively grabs the last element's values of the sub-arrays.
The fact that both reset and end move the internal array pointer, is of no concern. The only thing that matters here is that they also return the value of the element where they put that pointer to.
Solution without loop and just for fun:
$result = [[], []];
$keys = array_keys($data);
array_map(function($item) use(&$result, &$keys) {
$key = array_shift($keys);
$result[0][$key] = $item[0];
$result[1][$key] = $item[1];
}, $data);
Just a normal foreach loop will do.
$item_1 = [];
$item_2 = [];
foreach ($data as $k => $v){
$item_1[$k] = $v[0];
$item_2[$k] = $v[1];
}
Hope this helps.

Running calculations on multi-dimensional arrays?

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

Checking if ANY of an array's elements are in another array

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";
}

Categories