I have an array that reflects rebate percentages depending on the number of items ordered:
$rebates = array(
1 => 0,
3 => 10,
5 => 25,
10 => 35)
meaning that for one or two items, you get no rebate; for 3+ items you get 10%, for 5+ items 20%, for 10+ 35% and so on.
Is there an elegant, one-line way to get the correct rebate percentage for an arbitrary number of items, say 7?
Obviously, this can be solved using a simple loop: That's not what I'm looking for. I'm interested whether there is a core array or other function that I don't know of that can do this more elegantly.
I'm going to award the accepted answer a bounty of 200, but apparently, I have to wait 24 hours until I can do that. The question is solved.
Here's another one, again not short at all.
$percent = $rebates[max(array_intersect(array_keys($rebates),range(0,$items)))];
The idea is basically to get the highest key (max) which is somewhere between 0 and $items.
I think that the above one-line solutions aren't really elegant or readable. So why not use something that can really be understood by someone on first glance?
$items = NUM_OF_ITEMS;
$rabate = 0;
foreach ($rabates as $rItems => $rRabate) {
if ($rItems > $items) break;
$rabate = $rRabate;
}
This obviously needs a sorted array, but at least in your example this is given ;)
Okay, I know, you don't want the solution with the simple loop. But what about this:
while (!isset($rabates[$items])) {
--$items;
}
$rabate = $rabates[$items];
Still quite straightforward, but a little shorter. Can we do even shorter?
for (; !isset($rabates[$items]); --$items);
$rabate = $rabates[$items];
We are already getting near one line. So let's do a little bit of cheating:
for (; !isset($rabates[$items]) || 0 > $rabate = $rabates[$items]; --$items);
This is shorter then all of the approaches in other answers. It has only one disadvantage: It changes the value of $items which you may still need later on. So we could do:
for ($i = $items; !isset($rabates[$i]) || 0 > $rabate = $rabates[$i]; --$i);
That's again one character less and we keep $items.
Though I think that the last two versions are already too hacky. Better stick with this one, as it is both short and understandable:
for ($i = $items; !isset($rabates[$i]); --$i);
$rabate = $rabates[$i];
This might work without changing the rebate array.
But the array must be constructed in another way for this to work
$rebates = array(
3 => 0, //Every number below this will get this rebate
5 => 10,
10 => 25,
1000 => 35); //Arbitrary large numer to catch all
$count = $_REQUEST["count"];
$rv = $rebates[array_shift(array_filter(array_keys($rebates), function ($v) {global $count; return $v > $count;}))];
echo $rv;
Working testcase, just change count in url
http://empirium.dnet.nu/arraytest.php?count=5
http://empirium.dnet.nu/arraytest.php?count=10
Best I can manage so far:
$testValue = 7;
array_walk( $rebates, function($value, $key, &$test) { if ($key > $test[0]) unset($test[1][$key]); } array($testValue,&$rebates) );
Uses a nasty little quirk of passing by reference, and strips off any entry in the $rebates array where the key is numerically greater than $testValue... unfortunately, it still leaves lower-keyed entries, so an array_pop() would be needed to get the correct value. Note that it actively reduces the entries in the original $rebates array.
Perhaps somebody can build on this to discard the lower entries in the array.
Don't have 5.3.3 available to hand at the moment, so not tested using an anonymous function, but works (as much as it works) when using a standard callback function.
EDIT
Building on my previous one-liner, adding a second line (so probably shouldn't count):
$testValue = 7;
array_walk( $rebates, function($value, $key, &$test) { if ($key > $test[0]) unset($test[1][$key]); } array($testValue,&$rebates) );
array_walk( array_reverse($rebates,true), function($value, $key, &$test) { if ($key < $test[0]) unset($test[1][$key]); } array(array_pop(array_keys($rebates)),&$rebates) );
Now results in the $rebates array containing only a single element, being the highest break point key from the original $rebates array that is a lower key than $testValue.
Related
I need to calculate the sum of the values in array where key index is lower than... something.
I have done that, by this:
$temp_sum = 0;
for($temp_start = 0; $temp_start < 10; $temp_start++)
{
$temp_sum += $array[$temp_start];
}
But my question is... Another way to do this in more fashion way?
With usage of array functions? Maybe one specific array function for this task?
This for loop (or using even foreach loop) doesn't look nice and proper - but maybe it's only way to use standard looping.
for loops allow multiple statements inside its expressions, separated by commas. So, you could instantiate $temp_sum inside the for construct, to make it a little more concise:
for($temp_sum = 0, $temp_start = 0; $temp_start < 10; $temp_start++)
{
$temp_sum += $array[$temp_start];
}
I don't know if it necessarily helps readability though. Also, you might want to make sure there's at least 10 elements in $array to begin with, if you are not already verifying this.
Another alternative could be:
$sum = array_sum( array_slice( $array, 0, 10 ) );
but this requires the array keys to start at 0 and be adjacent and sequentially sorted. In other words, this array:
$array = [
13 => 12,
31 => 23,
1 => 24,
0 => 21
/* ... */
];
will create an undesirable result.
Not sure if it's more efficient either, but it's a nice one-liner and it has the possibly added benefit that it won't complain if there's less than 10 elements inside the array. You'll have to decide if that is desirable or not.
All in all, I think your initial use of the for loop is a pretty good example of a typical use-case. foreach loops, for example, are typically used in cases where you want to iterate all elements, not just a limited set.
So I'm really not good with math, formula's and the like. This is a little bit above my head.
I basically have an array, or a database with about 30 emoji's in it. I want to basically enter in a number into a form, lets say 3, then hit submit. the php script will then make as many unique combinations of 3 emoji's as possible, and then place them back into an array, or even just output right onto the screen separated by a new line.
I know how to code the form, i know out to output things to the screen and place items back into the array, etc etc... I have some good experience coding, but i'm not sure how to go about creating the unqiue combinations of the emoji's based upon user input.
any help is appreciated. if any clarification is needed let me know.
The number of combinations for 3 would be 30*29*28 = 24360 for 4 you would add *27 so the total would be 657720 so this probably isn't the best idea to build as you may run out of storage space or cause a stack overflow, but for fun you could use a recursive script to build it.
$emojis = range(1, 30);
$combos = makeCombos($emojis, 3);
echo json_encode($combos);
function makeCombos($emojis, $depth, $now = []) {
$results = [];
$depth--;
foreach ($emojis as $key => $value) {
$current = $now;
$current[] = $value;
$emojisNew = $emojis;
unset($emojisNew[$key]);
if ($depth > 0) {
$results = array_merge(
$results,
makeCombos($emojisNew, $depth, $current)
);
} else {
$results[] = $current;
}
}
return $results;
}
Using a range of (1, 3) so there are only 3*2*1 = 6 results yields the following result:
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
Note: Use $combos = makeCombos($emojis, 4); for combos of 4 emojis.
I have the following code :
$json = json_decode(URL, true);
foreach($json as $var)
{
if($var[id] == $valdefined)
{
$number = $var[count];
}
}
With json it looks like this :
[{"id":"1","count":"77937"},
{"id":"2","count":"20"},
{"id":"4","count":"25"},
{"id":"5","count":"11365"}]
This is what the array ($json) looks like after jsondecode
Array ( [0] => Array ( [id] => 1 [count] => 77937 ) [1] => Array ( [id] => 2 [count] => 20 ) [2] => Array ( [id] => 4 [count] => 25 ) [3] => Array ( [id] => 5 [count] => 11365) )
is there a way to say what is $json[count] where $json[id] = 3 for example
I'm not sure about a better way, but this is also fine, provided the JSON object is not huge. php is pretty fast when looping through JSON. If the object is huge, then you may want to split it. What I personally do is make my JSON into an array of normal objects, sort them, and then searching is faster on sorted items.
EDIT
Do json_decode($your_thing, true); set it true to make it an associative array, and then the id would be key and and the count would be value. After you do this, getting the value with the ID should really be easy and far more efficient.
If you change the way you build your json object to look like this :-
{"1":77937,"2":20,"4":25,"5":11365}
And then use the json_decode() parameter 2 set to TRUE i.e. turn the json into an array.
Then you have a usable assoc array with the ID as the key like so:
<?php
$json = '{"1":77937,"2":20,"4":25,"5":11365}';
$json_array = json_decode($json, TRUE);
print_r( $json_array);
?>
Resulting in this array
Array
(
[1] => 77937
[2] => 20
[4] => 25
[5] => 11365
)
Which you can do a simple
$number = json_array( $valdefined );
Or better still
if ( array_key_exists( $valdefined, $json_array ) ) {
$number = json_array( $valdefined );
} else {
$number = NULL; // or whatever value indicates its NON-EXISTANCE
}
Short answer to your initial question: why can't you write $json['count'] where $json['id'] = 3? Simply because PHP isn't a query language. The way you formulated the question reads like a simple SQL select query. SQL will traverse its indexes, and (if needs must) will perform a full table scan, too, its Structured Query Language merely enables you not to bother writing out the loops the DB will perform.
It's not that, because you don't write a loop, there is no loop (the absence of evidence is not the evidence of absence). I'm not going to go all Turing on you, but there's only so many things we can do on a machine level. On the lower levels, you just have to take it one step at a time. Often, this means incrementing, checking and incrementing again... AKA recursing and traversing.
PHP will think it understands what you mean by $json['id'], and it'll think you mean for it to return the value that is referenced by id, in the array $json, whereas you actually want $json[n]['id'] to be fetched. To determine n, you'll have to write a loop of sorts. Some have suggested sorting the array. That, too, like any other array_* function that maps/filters/merges means looping over the entire array. There is just no way around that. Since there is no out-of-the-box core function that does exactly what you need to do, you're going to have to write the loop yourself.
If performance is important to you, you can write a more efficient loop. Below, you can find a slightly less brute loop, a semi Interpolation search. You could use ternary search here, too, implementing that is something you can work on.
for ($i = 1, $j = count($bar), $h = round($j/2);$i<$j;$i+= $h)
{
if ($bar[++$i]->id === $search || $bar[--$i]->id === $search || $bar[--$i]->id === $search)
{//thans to short-circuit evaluation, we can check 3 offsets in one go
$found = $bar[$i];
break;
}//++$i, --$i, --$i ==> $i === $i -1, increment again:
if ($bar[++$i]->id > $search)
{// too far
$i -= $h;//return to previous offset, step will be halved
}
else
{//not far enough
$h = $j - $i;//set step the remaining length, will be halved
}
$h = round($h/2);//halve step, and round, in case $h%2 === 1
//optional:
if(($i + $h + 1) === $j)
{//avoid overflow
$h -= 1;
}
}
Where $bar is your json-decoded array.
How this works exactly is explained below, as are the downsides of this approach, but for now, more relevant to your question: how to implement:
function lookup(array $arr, $p, $val)
{
$j = count($arr);
if ($arr[$j-1]->{$p} < $val)
{//highest id is still less value is still less than $val:
return (object) array($p => $val, 'count' => 0, 'error' => 'out of bounds');
}
if ($arr[$j-1]->{$p} === $val)
{//the last element is the one we're looking for?
return $end;
}
if ($arr[0]->{$p} > $val)
{//the lowest value is still higher than the requested value?
return (object) array($p => $val, 'count' => 0, 'error' => 'underflow');
}
for ($i = 1, $h = round($j/2);$i<$j;$i+= $h)
{
if ($arr[++$i]->{$p} === $val || $arr[--$i]->{$p} === $val || $arr[--$i]->{$p} === $val)
{//checks offsets 2, 1, 0 respectively on first iteration
return $arr[$i];
}
if ($arr[$i++]->{$p} < $val && $arr[$i]->{$p} > $val)
{//requested value is in between? don't bother, it won't exist, then
return (object)array($p => $val, 'count' => 0, 'error' => 'does not exist');
}
if ($arr[++$i]->{$p} > $val)
{
$i -= $h;
}
else
{
$h = ($j - $i);
}
$h = round($h/2);
}
}
$count = lookup($json, 'id', 3);
echo $count['count'];
//or if you have the latest version of php
$count = (lookup($json, 'id', 3))['count'];//you'll have to return default value for this one
Personally, I wouldn't return a default-object if the property-value pair wasn't found, I'd either return null or throw a RuntimeException, but that's for you to decide.
The loop basically works like this:
On each iteration, the objects at offset $i, $i+1 and $i-1 are checked.
If the object is found, a reference to it is assigned to $found and the loop ends
The object isn't found. Do either one of these two steps:
ID at offset is greater than the one we're looking for, subtract step ($h) from offset $i, and halve the step. Loop again
ID is smaller than search (we're not there yet): change step to half of the remaining length of the array
A diagram will show why this is a more "clever" way of looping:
|==========x=============================|//suppose x is what we need, offset 11 of a total length 40:
//iteration 1:
012 //checked offsets, not found
|==========x=============================|
//offset + 40/2 == 21
//iteration 2:
012//offsets 20, 21 and 22, not found, too far
|==========x=============================|
//offset - 21 + round(21/2)~>11 === 12
//iteration 3:
123 //checks offsets 11, 12, 13) ==> FOUND
|==========x=============================|
assign offset-1
break;
Instead of 11 iterations, we've managed to find the object we needed after a mere 3 iterations! Though this loop is somewhat more expensive (there's more computation involved), the downsides rarely outweigh the benefits.
This loop, as it stands, though, has a few blind-spots, so in rare cases it will be slower, but on average it performs pretty well. I've tested this loop a couple of times, with an array containing 100,000 objects, looking for id random(1,99999) and I haven't seen it take more time than .08ms, on average, it manages .0018ms, which is not bad at all.
Of course, you can improve on the loop by using the difference between the id at the offset, and the searched id, or break if id at offset $i is greater than the search value and the id at offset $i-1 is less than the search-value to avoid infinite loops. On the whole, though, this is the most scalable and performant loopup algorithm provided here so far.
Check the basic codepad in action here
Codepad with loop wrapped in a function
Please tell me which is the best way to unset middle element of associative array in PHP?
Suppose I have an array of 10,000 elements and I want to remove middle element of that array, which is efficient way to remove middle element?
$temp = array('name1'=>'value1','name2'=>'value2',...,'name10000'=>'value10000');
$middleElem = ceil(count($temp) / 2);
$i = 0;
foreach ($temp as $key=>$val) {
if ($i == $middleElem) {
unset($temp[$key]);
break;
}
$i++;
}
Is above code efficient way?
Considering $array is your array, this code remove the middle element if it has odd number of elements. If its event it'll remove the first of 2 middle elements.
$i = round(count($array)/2) - 1;
$keys = array_keys($array);
unset ($array[$keys[$i]]);
Test Result: http://ideone.com/wFEM2
The thing you have to figure out is what you want to do when you have an array with an even number of elements. What element do you want to get then?
The above code picks the 'lower' element, the code could easily be edited to make it pick the 'higher' element. The only thing you have to check is (what all others answers failed to do) what happens if you have three elements. It doesn;t pick the middle element, but the last. So you would have to add a check for that then.
$temp = Array("name1"=>"value1","name2"=>"value2",...,"name10000"=>"value10000");
$middleElem = ceil(count($temp)/2);
$keys = array_keys($temp);
$middleKey = $keys[$middleElem];
unset($temp[$middleKey]);
There ^_^
I think it's a proper way to do it. Try this:
array_remove_at( $temp, ceil(count($temp) / 2) - 1);
function array_remove_at(&$array, $index){
if (array_key_exists($index, $array)) {
array_splice($array, $index, 1);
}
}
You can find the size of the array, divide that number by two and then proceed to remove the element. Not sure about the performance isssues about that though
Firstly, I wouldn't worry too much about what is the most efficient way at this point. You're much better off coding for how easy the code is to read, debug and change. Micro-optimisations like this rarely produce great results (as they're often not the biggest bottlenecks).
Having said that, if you want a solution that is easy to read, then how about using array_splice.
$temp = array('name1'=>'value1','name2'=>'value2',...,'name10000'=>'value10000');
$middleElem = ceil(count($temp) / 2);
array_splice( $temp, $middleElem, 1 );
I would take it that the following code is more efficient, because you don't have to execute it in a loop. I generally follow the same pattern as Kolink, but my version checks if there actually is a "middle element". Works on all types of arrays, I think.
<?php
for( $i = 0; $i <= 9; $i ++ ) {
$temp['name'.$i] = 'value'.$i;
}
if( ( $count = count( $temp ) ) % 2 === 0 ) {
/** Only on uneven arrays. */
array_splice( $temp, ( ceil( $count ) / 2 ), 1 );
}
var_dump( $temp );
EDIT: Thavarith seems to be right; array_splice is much faster than simply unsetting the value. Plus, you get the added benefit of not having to use array_keys, as you already now at what $offset the middle is.
Proper way seems to be:
unset(arr[id]);
arr = array_values(arr);
For first removing element at index id and then re-indexing the array arr properly.
unset($myArray[key])
since your array is associative, you can drop any element easily this way
I have an if statement that needs to check if a series of items exist AND if any other items exist.
So for example
foreach($id_cart as $id) {
if(($id == 4 || $id == 5) && **IF ANY OTHER ITEM EXISTS**){
echo "Yes";
}
Is there a simple way of doing that?
An item exists or it does not. $id_cart will have, let's say, ids: 4,5,6,8,12,14
There is nothing else stored. So if 4 or 5 are there, plus any other number...
If your question is asking
"I have a list of ids, from which I want to know if one is there, and apart of that one, if other items exist", one course of action could be the following:
if the item you look for exists, remove it from the array and check if it still has any members
if count(array) is > 0 do this; else do that;
else wasn't there or was 'alone'.
This course of action is very optimizable: for instance, your original array would lose members at each iteration. One easy change would be just do count(array) - 1 if you don't care which members the array contains.
Perhaps this does what you want:
foreach ($id_cart as $id) {
if (($id == 4 || $id == 5) && count(array_diff($id_cart, array(4, 5)))) {
// do something
}
}
or, better:
if ((in_array(4, $id_cart) || in_array(5, $id_cart)) && count(array_diff($id_cart, array(4, 5)))) {
// four or five exists, while other elements are also in the array
// do something
}
<?php
$a = array(4, 5);
if (array_intersect($id_cart, $a) && array_diff($id_cart, $a))
{
echo "Yes\n";
} else {
echo "No\n";
}
Tested:
4,5: No
4,6: Yes
5,6: Yes
6,8: No
4,5,6,8,12,14: Yes
See array_intersect() and array_diff().
Intersect with array(4,5) tests for presence of either 4 or 5, because the result would be empty if neither value occurred in $id_cart.
Diff with array(4,5) tests for presence of another value besides 4 and 5, because the result would be empty if no value but 4 or 5 occurred in $id_cart.
Using count() to test for a non-empty array is unnecessary. An empty array evaluates as false in a condition.
Re Adriano's comment about simplicity or efficiency: PHP is tricky this way. Some functions are more efficient than others, so it's hard to say 2 function calls is better than 3. I tried running both my solution and Adriano's, and measuring elapsed time using microtime():
Bill's solution: 6.25 seconds for 100000 iterations
Adriano's solution: 5.59 seconds for 100000 iterations
So they're not equal, but Adriano's is only 10.5% faster. Close enough that I'd choose a solution for readability instead of for performance. If optimal performance were one's highest priority, one wouldn't be using PHP in the first place. :-)
FWIW, Adriano's other solution using foreach took 7.13 seconds for 100000 iterations.
this will find items which are not 4 or 5
$others = array_diff($id_cart, array(4, 5));
this will find items whose keys are not 4 or 5
$others = array_diff_key($id_cart, array_flip(array(4, 5)));
An efficient way is to store the ID's in the array's keys and check for their existence with isset().
PHP's arrays are dictionaries.
Take advantage of this feature, instead of wasting time with in_array().