I get very strange PHP behavior with my function that allow to search within array of associative arrays to find array that have some key with searchable value.
My PHP code:
<?php
// Test array - results from database
$test_array[0] = array('period' => '2018', 'payment_type' => 'Direct Debit', 'value' => 0);
$test_array[1] = array('period' => '2018', 'payment_type' => 'Manual', 'value' => 20.85);
$test_array[2] = array('period' => '2018', 'payment_type' => 'Credit Debit Card', 'value' => 0);
// Function to find subarrays by payment type
function searchReportArrayForKeyValue($array, $searchvalue) {
$result_array = Array();
for($i = 0; $i < count($array); ++$i) {
$array_inside = $array[$i];
$keys = array_keys($array_inside);
for($j = 0; $j < count($array_inside); ++$j) {
$value = $array_inside[$keys[$j]];
if($value == $searchvalue) {
$result_array = $array_inside;
}
}
}
return $result_array;
}
var_dump(searchReportArrayForKeyValue($test_array, 'Direct Debit'));
var_dump(searchReportArrayForKeyValue($test_array, 'Manual'));
var_dump(searchReportArrayForKeyValue($test_array, 'Credit Debit Card'));
?>
If I run this code I should get 3 different arrays returned (0, 1, 2 keys from test array), however all three functions calls return 'Credit Debit Card' array key: http://take.ms/hZfec (screenshot).
BUT if I change 'value' => 0, to some float/integer all works as expected, for example if I change test array to this:
$test_array[0] = array('period' => '2018', 'payment_type' => 'Direct Debit', 'value' => 11.2);
$test_array[1] = array('period' => '2018', 'payment_type' => 'Manual', 'value' => 20.85);
$test_array[2] = array('period' => '2018', 'payment_type' => 'Credit Debit Card', 'value' => 10.5);
I get 3 correct different subarrays returned by my three function calls: http://take.ms/SSTu1 (screenshot)
Why this happens? How this ZERO in 'value' break arrays iteration? What is wrong in my function?
P.S.: previously I have 'for each' in code, and changed it to 'for' to make sure this issue does not related to pointer reset in 'for each' inside 'for each', but this does not helped in this situation.
Finally I fixed this with this code changes:
I changed this:
if($value == $searchvalue)
To this:
if(strval($value) == strval($searchvalue))
But I'am still does not understand how and why this gives so strange behavior.
You are using a loose comparison using ==
In every loop, the last comparision is 0 == Direct Debit which is true, and then you set $result_array = $array_inside;
You could see it when you run for example:
echo "$value == $searchvalue = " . ($value == $searchvalue ? "true" : "false") . PHP_EOL;
if($value == $searchvalue) {
You can't compare floating point values directly because not all rational decimal numbers have rational floating-point equivalents.
See: https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems
To properly compare floating point numbers for you need to do something like:
$e = 0.00001;
if( abs($float_a - $float_b) < $e );
Where $e is a sufficiently small margin of error for a particular comparison.
Your solution of casting to string only works because PHP's default float format precision is 14 digits and the imprecision is less than that.
That said, no one should ever use floating point to record amounts of money for these exact reasons, and more. Generally you store the value as an integer of the base unit of currency, eg: $1 == 100 cents, 1BTC == 1,000,000 satoshis.
However there are further concerns, such as safely splitting $4 across 3 recipients, safely rounding amounts without losing pennies, etc. For this reason there is Fowler's Money Pattern, and libraries that implement it.
Related
Sorting a multidimensional array by value is fairly easy and has been answered multiple times already here
usort($myArray, function($a, $b) {
return $a['field'] - $b['field'];
});
The problem I am having now, is that I need another condition. Let's imagine I have an array with 10 cars and 10 motorcycles. These cars and motorcycles are each an array/object with values containing a field speed. Like
$car1 = [
'speed' => 100,
'type' => 'car'
]
$car2 = [
'speed' => 120,
'type' => 'car'
]
$car3 = [
'speed' => 180,
'type' => 'car'
]
$motorcycle1 = [
'speed' => 80,
'type' => 'motorcycle'
]
Those are all stored in one array
$vechicles = [$car1, $car2, $car3, $motorcycle1]
What I now want to do is to sort by speed. Which is, as I said, easy
usort($myArray, function($a, $b) {
return $a['speed'] - $b['speed'];
});
The problem I am facing now, is, that independent of the speed value, at least every third vehicle MUST be a motorcycle. It can be, that the first entries are motorcycles, doesn't matter, but it can't be all cars. It always have to be at least one motorcycle. Doesn't matter here if it's 1, 2 or 3 motorcycles.
So in the end it should look like this
$finalArray = [$car3, $car2, $motorcycle1, $car1];
As you can see, while $car1 is faster than $motorcycle1, the motorcycle comes earlier.
The thing is, when I have two different arrays
$cars = [$car1, $car2, $car3]
and
$motorcycles = [$motorcycle1]
I can simply splice it in, like
array_splice($cars, 2, 0, $motorcycles[0]);
But then I got the problem that I can't sort it by speed. Is there a way to achieve this?
Well, easiest solution
$vehicles = [...]; // sorted by speed already
$count = count($vehicles);
$temp = 0;
foreach($vehicles as $key => $veh){
if(is_car($veh)){
$temp++;
}else{
$temp = 0;
}
if($temp == 3){
for($i = $key + 1; $i<=$count; $i++){
if(is_motorcycle($vehicles[$i])){
array_splice($vehicles, $key, 0, $vehicles[$i]);
break;
}
}
$temp = 0;
}
}
Obviously this is not working code, I just wrote it to illustrate my idea, if you think you can benefit from it, happy tweaking! :P
This is a quick one. Is there a way to compare a given value to a multidimensional array of preset rules, without looping through our rules?
I'll give you an example. Let's say we are evaluating students from 1 to 10. And we want to assign a performance assessment with a few words. So we'd have ranges of marks and what they represent, for example:
$evaluation = array(
array('from' => 1, 'to' => 3, 'comment' => 'Stop watching TV'),
array('from' => 4, 'to' => 6, 'comment' => 'Keep trying'),
array('from' => 7, 'to' => 8, 'comment' => 'Almost there'),
array('from' => 9, 'to' => 10, 'comment' => 'EMC2')
);
A student got 8, so we'd do:
$grade = 8;
foreach($evaluations as $evaluation) {
if($grade >= $evaluation['from'] && $grade <= $evaluation['to']) {
echo $evaluation['comment'];
}
}
Which I guess is fine. But is there a more neater way to do this? Perhaps a built-in PHP function that would be faster than looping through our set of rules?
Thanks.
A switch statement would be slightly faster but since you are dealing with a 4-element array, almost anything will do.
If you unroll the loop by using a switch statement, you reduce the number of expressions that need to be evaluated since in your loop you need to make a greater than and a less than comparison. Using the switch, you need only one comparison.
There are other PHP functions like array_filter and array_reduce that may lead to the same result if you define the correct callback function, but what you have right now may be as good as anything else.
Think you're over thinking the array a bit, you could key by max value in each range, it's a little less data, and less to evaluate, but in all honesty it seems like a micro optimisation
$evaluation = array(
3 => 'Stop watching TV',
6 => 'Keep trying',
8 => 'Almost there',
10 => 'EMC2'
);
$grade = 8;
foreach ($evaluation as $limit => $comment) {
if ($limit < $grade) continue;
break;
}
echo $comment;
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]));
}
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);
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