Find max and min key in associative array - php

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.

Related

How to start a foreach from a specific key and continue after that to the rest of the array?

I have an array that contains key-value:
Array
(
[monday_deal] => 753394
[tuesday_deal] => 753401
[wednesday_deal] => 753435
[thursday_deal] => 753437
[friday_deal] => 771844
[saturday_deal] => 15047
[sunday_deal] => 753437
)
I need to start a foreach that starts from the today's day deal (key = wednesday_deal), for eg.
I need to know the deal ID of Wednesday first and then continue to get the deal IDs of Thurdsay, Friday, Saturday, Sunday, Monday, Tuesday serially.
NOTE: I wish to use foreach and not for since I have to make use of $key and $value combinations further in the code.
Use array_search on the keys to find the position of your key and then use array_slice to return the relevant part of the array. If you only need the deal ids, use array_values on the result, if you need more, use it in foreach.
<?php
$data = [
'monday_deal' => 753394,
'tuesday_deal' => 753401,
'wednesday_deal' => 753435,
'thursday_deal' => 753437,
'friday_deal' => 771844,
'saturday_deal' => 15047,
'sunday_deal' => 753437,
];
$pos = array_search('wednesday_deal', array_keys($data));
if ($pos===false) {
//error
}
$resultArray = array_slice($data, $pos);
//either
$resultDeals = array_values($resultArray);
$firstDeal = $resultDeals[0];
//or
foreach ($resultArray as $key=>$value) {
echo "$key => $value";
}
Of course if Wednesday deal is always at 3rd position, you can skip the array_search

Fast Filter of Array With Respect to Another

Suppose I have an array of indexes, A.
Suppose I have an array B, where every key is an array containing some indexes or number.
I would know which of the entries in B contain some index appearing in A.
For example (in php style):
A = [3,45,67,8]
B = [ 1 => [1,6,81],
2 => [5,67,3,4,5,66,6],
3 => [55,56,57,58],
4 => [45,80,81,82]
]
The entries of B which contain some value of A are 2 and 4.
So the function I would make should be:
function solution = filter(A,B) // solution = [2,4]
Now, with a brute loop, looping over entries of B, the complexity will be
O(nm), where n is #B and m is the size of longest row in B.
There are smarter solutions?
Edit #2:
By moving all values to be compared to key positions, php can more than double efficienct (by my demo's system time metric).
Furthermore, if you have duplicate values in your subsets, calling array_flip() will reduce the size by disallowing duplicate keys.
Code: (Demo)
$A = array_flip($A); // prepare for key comparisons
$result = [];
foreach ($B as $key => $haystack) {
if (array_intersect_key(array_flip($haystack), $A)) {
$result[] = $key;
}
}
var_export($result);
Edit:
Whenever you want to optimize array searching with php, it is often best to try to prepare your data in a way which allows php to leverage its strength with hash tables. https://codedmemes.com/lib/best-performance-array-intersection/
Consider this preparation...
Code: (Demo)
foreach ($B as $key => $haystack) {
foreach ($haystack as $hay) {
$C[$hay][] = $key;
}
}
var_export(array_keys(array_flip((array_merge(...array_intersect_key($C, array_flip($A)))))));
Output:
array (
0 => 1,
1 => 2,
2 => 4,
)
The nested foreach() loops generate a collection of subarrays which have unique values from B's subarrays as keys and $B's original keys as new subarray values.
array_intersect_key() checks the keys which php does much faster than checking values. (see first hyperlink article)
Then array_merge(...) flattens the subarrays into a single 1-dim array.
Finally array_flip() and array_keys() removes duplicates and re-indexes the results.
I don't know exactly how array_intersect() works its magic in terms of efficiency, but this is probably how I'd go about it:
Code: (Demo)
$A = [3,45,67,8];
$B = [ 1 => [1,6,8],
2 => [5,67,3,4,5,66,6],
3 => [55,56,57,58],
4 => [45,80,81,82]
];
$result = [];
foreach ($B as $key => $haystack) {
if (array_intersect($haystack, $A)) { // generate an array with co-existing values
$result[] = $key; // add key if array_intersect makes a non-empty array
}
}
var_export($result);
Output:
array (
0 => 1,
1 => 2,
2 => 4,
)
I suppose if there is a "downside" to using array_intersect() it would be that it will bother to make multiple matches, when only a single match per row is necessary. For this reason, array_search() or a breaking loop might have advantages.
Code: (Demo)
$result = [];
foreach ($B as $key => $haystack) {
foreach ($haystack as $hay) {
if (in_array($hay, $A)) {
$result[] = $key;
break;
}
}
}
var_export($result); // same result

What's the best PHP array function to use when intersecting nested arrays?

PHP's array is very flexible and useful. I counted over 30 array functions on the PHP.net array reference page. Some of them can solve my problem, but I'm looking for the best, most elegant way.
I have 2 arrays, called labor and cost, each containing a table:
labor = array(
0 => array('date' => date, 'labor'=> labor),
1 => array('date' => date, 'labor'=> labor),
...
);
cost = array(
0 => array('date' => date, 'cost'=> cost),
1 => array('date' => date, 'cost'=> cost),
...
);
My problem is that sometimes the number of dates don't match (i.e., there are days when you've incurred costs, even though you spent nothing on labor, or days when you had labor but no cost) - that means there are more lines in one array then the next - no way to know which has without using count().
What I'm interested in are only the days that had both labor and cost and I want to end up with an array:
laborcost = array(
0 => array('date' => date, 'labor'=> labor, 'cost' => cost),
1 => array('date' => date, 'labor'=> labor, 'cost' => cost),
...
)
I thought about using array_intersect() or one of the 'u' functions, but ended totally mixed up. Before giving up and writing my own array scanning function, I wanted to see if there are any ideas that will solve my issue with 1, possibly 2, lines of code.
There's is no intersect function accepting a user-defined comparison function that allows you to modify the arrays. The simplest way is just to do it yourself.
Here are a few examples:
O(2n + m)
// Remap dates as keys for faster lookups
$result = $nlabor = $ncost = array();
foreach ($labor as $l) $nlabor[$l['date']] = $l;
foreach ($cost as $c) $ncost[$c['date']] = $c;
// Compare
foreach ($nlabor as $date => $l) {
if (array_key_exists($date, $ncost)) {
$result[] = array_merge($l, $ncost[$date]);
}
}
~O(n * m)
// Just compare them all
$result = array();
foreach ($labor as $l) {
foreach ($cost as $c) {
if ($l['date'] == $c['date']) {
$result[] = array_merge($l, $c);
break;
}
}
}
Which way is the best depends on how many elements you have in each array. When used on smaller arrays ~O(n * m) is fine, while on bigger arrays O(2n + m) will be more efficient.
This should do that trick. Not quite as simple as a single function.
$merge = array();
for ($i = 0; $i < count($labor); $i++) {
array_push($merge, array_merge($labor[$i],$cost[$i]));
}

How to randomize a PHP array of records, giving more weight to more recent items?

I have an array of records from a database (although the database is irrelevant to this question -- it eventually becomes an array of "rows", each row is an array with string keys corresponding to the field name). For example:
$items = array(
1 => array('id' => 1, 'name' => 'John', 'created' => '2011-08-14 8:47:39'),
2 => array('id' => 2, 'name' => 'Mike', 'created' => '2011-08-30 16:00:12'),
3 => array('id' => 5, 'name' => 'Jane', 'created' => '2011-09-12 2:30:00'),
4 => array('id' => 7, 'name' => 'Mary', 'created' => '2011-09-14 1:18:40'),
5 => array('id' => 16, 'name' => 'Steve', 'created' => '2011-09-14 3:10:30'),
//etc...
);
What I want to do is shuffle this array, but somehow give more "weight" to items with a more recent "created" timestamp. The randomness does not have to be perfect, and the exact weight does not really matter to me. In other words, if there's some fast and simple technique that kinda-sorta seems random to humans but isn't mathematically random, I'm okay with that. Also, if this is not easy to do with an "infinite continuum" of timestamps, it would be fine with me to assign each record to a day or a week, and just do the weighting based on which day or week they're in.
A relatively fast/efficient technique is preferable since this randomization will occur on every page load of a certain page in my website (but if it's not possible to do efficiently, I'm okay with running it periodically and caching the result).
You can use eg. this comparison function:
function cmp($a, $b){
$share_of_a = $a['id'];
$share_of_b = $b['id'];
return rand(0, ($share_of_a+$share_of_b)) > $share_of_a ? 1 : -1;
}
and then use it like this:
usort($items, 'cmp');
It compares two elements of an array based on their IDs (it is easier and they are assigned based on the date of creation - newer elements have bigger IDs). The comparison is done randomly, with different chances of success for each element, giving more chances to the newer elements. The bigger the ID (the newer the element), the more chances it has to appear at the beginning.
For example element with id=16 has 16x more chances than element id=1 to appear earlier on the resulting list.
What about splitting it up into chunks by date, randomizing each chunk, and then putting them back together as one list?
//$array is your array
$mother=array();
foreach($array as $k->$v) $mother[rand(0,count($array))][$k]=$v;
ksort($mother);
$child=array();
foreach($mother as $ak->$av)
foreach($av as $k->$v) $child[$k]=$v;
$array=$child;
or you can use shuffle()
After being partially inspired by the response from #Tadeck , I came up with a solution. It's kind of long-winded, if anyone could simplify it that would be great. But it seems to work just fine:
//Determine lowest and highest timestamps
$first_item = array_slice($items, 0, 1);
$first_item = $first_item[0];
$min_ts = strtotime($first_item['created']);
$max_ts = strtotime($first_item['created']);
foreach ($items as $item) {
$ts = strtotime($item['created']);
if ($ts < $min_ts) {
$min_ts = $ts;
}
if ($ts > $max_ts) {
$max_ts = $ts;
}
}
//bring down the min/max to more reasonable numbers
$min_rand = 0;
$max_rand = $max_ts - $min_ts;
//Create an array of weighted random numbers for each item's timestamp
$weighted_randoms = array();
foreach ($items as $key => $item) {
$random_value = mt_rand($min_rand, $max_rand); //use mt_rand for a higher max value (plain old rand() maxes out at 32,767)
$ts = strtotime($item['created']);
$ts = $ts - $min_ts; //bring this down just like we did with $min_rand and $max_rand
$random_value = $random_value + $ts;
$weighted_randoms[$key] = $random_value;
}
//Sort by our weighted random value (the array value), with highest first.
arsort($weighted_randoms, SORT_NUMERIC);
$randomized_items = array();
foreach ($weighted_randomsas $item_key => $val) {
$randomized_items[$item_key] = $items[$item_key];
}
print_r($randomized_items);

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