Double Foreach unexpected result - php

I have two arrays, both associative;
ArrayUneaten ( lemon=> 7, banana=>6, apple=>10)//the units of UNeaten fruit
ArrayOrdered (lemon =>10, strawberry =>10, Kiwi=>0, Apple=>20, Banana=>6) // the units of ordered fruit
I want to create a third array (numeric is fine if in the same order as ArrayOrdered)
of all fruit options and the % of Fruit Eaten (compared to Fruit Ordered).
(NB for other reasons if Ordered Value = 0, %eaten = 0%)
(NB2, note that % desired for strawberries is 100, no strawberries are uneaten, 10 ordered, therefore 10 have been eaten)
So desired Array
ArrayEatenPercentage (70, 100, 0, 50, 0)
My coding attempt
$CompletedPercentagesArray = array ();
foreach( $ArrayOrdered as $fruitOrdered => $unitsOrdered) {
if ($unitsOrdered == 0){
//if it's zero it's never been selected
$completedPercentage = 0;
}
foreach( $ArrayUneaten as $fruitUneaten => $unitsUneaten) {
if ( $fruitUneaten == $fruitOrdered){
// ($totalCardsChosen = $timesSelected*25; - please ignore)
$percentageUneaten = 100*($fruitUneaten/$unitsOrdered);
$percentageEaten = 100 - $percentageUneaten;
$completedPercentage = round ($percentageEaten, 1);
}
else {//if this is true then it's been selected and been finished
$completedPercentage = 100;
}
}
array_push( $completedPercentagesArray, $completedPercentage ); //this adds the variable to the array
}
print_r($completedPercentagesArray);
Current output is unexpected.
Only last value of ArrayUneaten is processed correctly.
The other values return 100%.
ArrayEatenPercentage (100, 100, 100, 50, 100)

Assuming all fruits are in the "ordered" array, you could try something like this:
$results = array();
foreach( $arrayOrdered as $fruit => $numOrdered ) {
if( Isset( $arrayUneaten[$fruit] ) ) {
$numEaten = $numOrdered - $arrayUneaten[$fruit];
} else {
$numEaten = $numOrdered;
}
if( $numOrdered > 0 ) {
$percentEaten = $numEaten / $numOrdered * 100;
} else {
$percentEaten = 0;
}
$results[$fruit] = array( 'eaten' => $numEaten, 'percent' => $percentEaten );
}
Your result array would be keyed on fruit and have both the absolute amount eaten and the percentage.

You should have added break; at the end of if ( $fruitUneaten == $fruitOrdered){ condition block. Otherwise unless the current fruit is the last element(which is apple) of $ArrayUneaten, $completedPercentage becomes always 100, because on the last iteration of the inner loop exection goes to that else block.

Related

Pick random value by weight php

I'm about to create "lottary system."
Take a look at my table:
userid-lottaryid-amount
1 -------- 1 ---- 1
2 -------- 1 ---- 10
3 -------- 1 ---- 15
4 -------- 1 ---- 20
I want to choose a winner. and another person for second place.
I just can't select a winner randomly because 4th user has 20 tickets and 1st user has only one.
So I need to generate random results by weight in order to be more fair.
I found php function below but I couldn't figure out how to use it.
function weighted_random_simple($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(0, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
$values = array('1', '10', '20', '100');
$weights = array(1, 10, 20, 100);
echo weighted_random_simple($values, $weights);
I must fetch userid colomn as array to $values and amount colomn to $weights. But I couln't.
Here is my code so far:
$query = $handler->prepare("SELECT
`cvu`.`lottaryid` as `lottaryid`,
`cvu`.`userid` as `userid`,
`cvu`.`amount` as `amount`,
`members`.`id` as `members_memberid`,
`members`.`username` as `username`
FROM `lottariesandmembers` as `cvu`
LEFT JOIN `members` as `members` ON `cvu`.`userid` = `members`.`id` WHERE `cvu`.`lottaryid` = 2");
$query->bindParam(':lottaryid', $lottaryid, PDO::PARAM_INT);
$query->execute();
while($r = $query->fetch()) {
for ( $count=1 ; $count <= $r["amount"] ; $count++ ) {
$abcprint = "$r[userid].$count - $r[username] - <br>";
echo "$abcprint";
}
}
This code I have, only lists users as many times as their amount. For example:
1.1 user1
2.1 user2
2.2 user2
2.3 user2
..
2.10 user2
3.1 user3
..
3.15 user3
4.1 user4
..
4.20 user4
and so on.. But I'm stuck how to pick a winner on that list.
I would like to merge those codes and create this little script, if you would like to help me.
I'm also open for brainstorm if you see the solution on the other way around.
Instead of printing out the values as you are doing, you can just build a large array, and then choose a value randomly from that array.
while($r = $query->fetch()) {
for ( $i=0; $i <= $r["amount"]; $i++ ) {
// Add the user into the array as many times as they have tickets
$tickets[] = $r['userid'];
}
}
// select the first place winner
$first = $tickets[mt_rand(0, count($tickets) - 1)];
// remove the first place winner from the array
$tickets = array_values(array_filter($tickets, function($x) use ($first) {
return $x != $first;
}));
// select the second place winner
$second = $tickets[mt_rand(0, count($tickets) - 1)];
I'm sure there is a more efficient way to do this using math, but I need to think about it a bit more...
This isn't very elegant but should work for smallish lotteries.
It just constructs a massive array and picks an element at random.
Think of having a massive hat full of slips. Each holder gets their stake in 'slips' and each are labelled with their id. i.e. Ten slips with the holder's name 'a', 20 slips with 'b' and so on...
<?php
$holder_totals = array(
'a' => '10',
'b' => '20',
'c' => '20',
'd' => '50'
);
$big_hat = array();
foreach($holder_totals as $holder_id => $total) {
$holder_hat = array_fill(0, intval($total), $holder_id);
$big_hat = array_merge($big_hat, $holder_hat);
}
// Drum roll
foreach (range(1,4) as $n) {
$random_key = array_rand($big_hat);
printf("Winner %d is %s.\n", $n, $big_hat[$random_key]);
unset($big_hat[$random_key]); // Remove winning slip
}
Sample output:
Winner 1 is d.
Winner 2 is c.
Winner 3 is d.
Winner 4 is b.
Big hat looks like this:
Array
(
[0] => a
[1] => a
[2] => a
[3] => a
[4] => a
[5] => a
[6] => a
[7] => a
[8] => a
[9] => a
[10] => b
[11] => b
[12] => b
[13] => b
[14] => b
... and so on...
)
/**
* getRandomWeightedElement()
* Utility function for getting random values with weighting.
* Pass in an associative array, such as array('A'=>5, 'B'=>45, 'C'=>50)
* An array like this means that "A" has a 5% chance of being selected, "B" 45%, and "C" 50%.
* The return value is the array key, A, B, or C in this case. Note that the values assigned
* do not have to be percentages. The values are simply relative to each other. If one value
* weight was 2, and the other weight of 1, the value with the weight of 2 has about a 66%
* chance of being selected. Also note that weights should be integers.
*
* #param array $weightedValues
*/
function getRandomWeightedElement(array $weightedValues) {
$rand = mt_rand(1, (int) array_sum($weightedValues));
foreach ($weightedValues as $key => $value) {
$rand -= $value;
if ($rand <= 0) {
return $key;
}
}
}
Here is an efficient and flexible function. But You have to modify it if you want to use non-integer weighting.
You can use weightedChoice function from my library nspl.
use function \nspl\rnd\weightedChoice;
// building your query here
$pairs = [];
while($r = $query->fetch()) {
$pairs[] = [$r['userid'], $r['amount']];
}
$winnerId = weightedChoice($pairs);
You can install the library with composer:
composer require ihor/nspl
Or you can simply reuse weightedChoice code from GitHub:
/**
* Returns a random element from a non-empty sequence of items with associated weights
*
* #param array $weightPairs List of pairs [[item, weight], ...]
* #return mixed
*/
function weightedChoice(array $weightPairs)
{
if (!$weightPairs) {
throw new \InvalidArgumentException('Weight pairs are empty');
}
$total = array_reduce($weightPairs, function($sum, $v) { return $sum + $v[1]; });
$r = mt_rand(1, $total);
reset($weightPairs);
$acc = current($weightPairs)[1];
while ($acc < $r && next($weightPairs)) {
$acc += current($weightPairs)[1];
}
return current($weightPairs)[0];
}

Fill an array with random numbers while obeying designated sum, count, and number boundaries

I have to fill an array with random numbers to satisfy a few conditions:
The number of elements in the result array must match the designated number.
The sum of the numbers in the result array must equal the designated number.
Random numbers must be selected between designated lower and upper bounds.
For example:
Sum of the array: 130
Total array elements: 3
Random integers' lower bound: 23
Random integers' upper bound: 70
Possible result:
array(23, 70, 37)
What to do now? How to split/divide my number?
I started with this (pseudo code):
i=0;
while(sum(number) > 0 and i < arraykeys){
x = randomize(from, to)
number = number - x
myarray[i] = x
i++
}
This should work for you:
Code explanation
Workability
The first thing we need to check is, if it is possible to build the goal out of numbers from the scope:
if(checkWorkability($result, $goal, $amountOfElementsLeft, $scope))
Means it just uses the highest values possible and looks if it is bigger than the goal.
While loop
In the while loop we need to check if we still have elements left which we can use:
while($amountOfElementsLeft > 0)
Scope adjustment
Every iteration we need to check if we need to adjust the scope, so that at the end we will be able to build the goal.
This means if the current sum of numbers + the highest possible number is bigger than the goal, we need to make the max value of the scope smaller.
Also on the opposite side we need to make the min value of the scope bigger, when we can't reach our goal anymore.
Code
<?php
$goal = 130;
$amountOfElementsLeft = 3;
$scope = [23, 70];
$result= [];
function adjustScope(array $result, $goal, $amountOfElementsLeft, $scope) {
$newScope = $scope;
if($amountOfElementsLeft == 1) {
$leftOver = $goal - array_sum($result);
return [$leftOver, $leftOver];
}
if((($goal - (array_sum($result) + $scope[1])) / ($amountOfElementsLeft - 1)) < $scope[0])
$newScope[1] = (int) ($goal - array_sum($result)) / ($scope[0] * ($amountOfElementsLeft - 1));
elseif(($adjustTop = $goal - array_sum($result)) < $scope[1])
$newScope[1] = $adjustTop;
if(($adjustBottom = $goal - (array_sum($result) + $scope[0] + (($amountOfElementsLeft - 1) * $scope[1]))) < $goal && $adjustBottom > 0)
$newScope[0] = $scope[0] + $adjustBottom;
return $newScope;
}
function checkWorkability(array $result, $goal, $amountOfElementsLeft, $scope) {
if(array_sum($result) + $amountOfElementsLeft * $scope[1] >= $goal)
return TRUE;
return FALSE;
}
if(checkWorkability($result, $goal, $amountOfElementsLeft, $scope)) {
while($amountOfElementsLeft > 0) {
$scope = adjustScope($result, $goal, $amountOfElementsLeft, $scope);
$result[] = rand($scope[0], $scope[1]);
$amountOfElementsLeft--;
}
}
print_r($result);
echo array_sum($result);
?>
possible outputs:
Array
(
[0] => 58
[1] => 30
[2] => 42
) -> 130
Array
(
[0] => 35
[1] => 54
[2] => 41
) -> 130
Array
(
[0] => 52
[1] => 51
[2] => 27
) -> 130
I've written a custom function for portability and to meaningfully implement some guard conditions which throw exceptions when incoming parameters make the desired result impossible.
Loop one less than $count times -- this is because the final element in the returned array is determined by the difference between the desired total and the sum of the randomly acquired values.
Adjust the lower and upper bounds of the $scope array (if required) to ensure a successfully populated return array.
Get a random integer, push it into the return array, then subtract it from the $total.
When the looped processes are finished, push the remaining $total value as the final element in the return array.
Code: (Demo)
function getRandWithStipulations(int $total, int $count, array $scope): array
{
if ($scope[0] > $scope[1]) {
throw new Exception('Argument 3 (\$scope) is expected to contain a minimum integer then a maximum integer.');
}
if ($scope[0] * $count > $total) {
throw new Exception('Arguments 2 (\$count) and 3 (\$scope) can only exceed argument 1 (\$total).');
}
if ($scope[1] * $count < $total) {
throw new Exception('Arguments 2 (\$count) and 3 (\$scope) cannot reach argument 1 (\$total).');
}
$result = [];
for ($x = 1; $x < $count; ++$x) { // count - 1 iterations
$scope[0] = max($scope[0], $total - ($scope[1] * ($count - $x)));
$scope[1] = min($scope[1], $total - ($scope[0] * ($count - $x)));
$rand = rand(...$scope);
$result[] = $rand;
$total -= $rand;
}
$result[] = $total;
return $result;
}
try {
var_export(
getRandWithStipulations(
130,
3,
[23, 70]
)
);
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage();
}
A few random results:
[60, 34, 36]
[23, 59, 48]
[67, 36, 27]
[47, 23, 60]

Displaying min() value in array

I want to display 10 highest figure and 10 lowest figure in a multi-dimensional array.
I have figured out way to display the maximum figure using max(), but when I use min(), the least less value get looped again and again for 10 times e.g 2.
How can I reuse my code to display minimum value in my array?
$totalCustomer = count($customerArray);
$postion = 0;
foreach ($customerArray as $custom) {
$postion = $postion + 1;
if($totalCustomer - $postion < 9){
$top[] = $custom['spend'];
$maxprice = max($top);
echo "Max spend price is ". $maxprice. "<br>";
}
}
#hek2mgl answer is a good one. But you can take advantage of PHP array's indexes to avoid sorting and gaining performance.
$prices = [];
foreach ( $customerArray as $custom )
{
// This approach uses your price as an ordering index, and supposes two decimal points
$index = intval( $custom['spend'] * 100 );
$prices[$index] = $custom['spend'];
}
// Showing lowest 10 prices
$top = array_slice( $prices, 0, 10 );
// Showing top 10 prices
$low = array_slice( array_reverse( $prices ), 0, 10 );
I would use usort:
/* Sort the array the value of 'spend' */
usort($customerArray, function($a, $b) {
if($a['spend'] == $b['spend']) {
return 0;
} else if ($a['spend'] > $b['spend']) {
return -1;
} else {
return 1;
}
});
/* The top 10 elements are now on top of the array */
$top = array_slice($customerArray, 0, 10);
/* If we reverse the array using array_reverse() the
smallest items are on top */
$low = array_slice(array_reverse($customerArray), 0, 10);

Inserting value when there is no value in second arraylist

I've got a problem which takes up a lot of time. While it's supposed to be really easy (because it's just so simple!).
My problem:
I have these values inside two arraylists:
$row[0]->COUNTER1 20 10 15
$row[0]->GRADE_POINTS 0 3 5
I am supposed to change these arraylists into this example:
$row[0]->COUNTER1 20 0 0 10 0 15
$row[0]->GRADE_POINTS 0 1 2 3 4 5
So the missing values are supposed to have 0 as the counter.
While this isn't that hard to do it I'm probably over thinking it.
The code which I use to create the first set of numbers is:
$result = new SimpleXMLElement($xmlresult);
$xml = $result->children("soapenv", true)->Body->children();
$xmlBody = $xml[0];
$countPerResultaat = array();
foreach($xmlBody->query[0] as $row)
{
$countPerResultaat[] = (int) $row[0]->COUNTER1;
$xaxis[] = (string) $row[0]->GRADE_POINTS;
}
The code I though that would work is this:
for($i; $i<=10; $i++){
//foreach($xmlBody->query[0] as $row)
//{
$row = $xmlBody->query[0];
if($i==$row[0]->GRADE_POINTS){
$countPerResultaat[] = (int) $row[0]->COUNTER1;
$xaxis[] = (string) $row[0]->GRADE_POINTS;
}else{
$xaxis[] = (string) $i;
$countPerResultaat[] = (int) 0;
}
}
But the row can't be used, I really don't know how to fix this. My only solution would be to use another for-loop, which would create 100 values probably.
Thanks for helping in advance!
If I understand correctly and if $row[0]->COUNTER1 and $row[0]->GRADE_POINTS are arrays. You will just need to loop them and use in_array(). Consider this example:
$counter1 = array(20, 10, 15);
$grade_points = array(0, 3, 5);
$new_grade_points = range(min($grade_points), max($grade_points));
foreach($new_grade_points as $key => &$value) {
// check if its part of the missing index if not get the value,
// if its the missing index put 0
$value = (in_array($key, $grade_points)) ? array_shift($counter1) : 0;
}
$counter1 = array_values($new_grade_points); // now contains 20,0,0,10,0,15
$grade_points = array_keys($new_grade_points); // now contains 0,1,2,3,4,5
print_r($counter1);
Sample Output:
Array
(
[0] => 20
[1] => 0
[2] => 0
[3] => 10
[4] => 0
[5] => 15
)
I think you want to count the amount of times a grade has been given? You should just loop through as usual, and when there is no value you should/could define it as 0. After that just count how many duplicates you have in the array. That way the key of the $xaxis is the grade, and the value is the amount of times that grade has been given.
foreach($xmlBody->query[0] as $row)
{
$counter = (int) $row[0]->COUNTER1;
if(counter) $countPerResultaat[] = $counter;
else $countPerResultaat[] = 0;
}
$xaxis = array_count_values($counter);

More efficient than an if statement?

I'm making a game in PHP (don't ask lol), and the player has a location which is an integer. There's a travel page and this basically shows a 5x5 tiled map. Each tile is a different part of the player's universe. By clicking on it he can travel there.
Just to give you an idea of the integers behind the map:
11, 12, 13, 14, 15
21, 22, 23, 24, 25
31, 32, 33, 34, 35
41, 42, 43, 44, 45
51, 52, 53, 54, 55
Let's say the player starts at 33(the middle) and I wanted to charge him different rates depending on how far he traveled. So, for example, 1 tile in any direction is a 100 credits, 2 tiles is 200 and so on.
So what I came up with is this. $ol represents the player's current location and $nl is where they are travelling to...
if($ol-11==$nl || $ol-10==$nl || $ol-9==$nl || $ol+1==$nl || $ol+11==$nl || $ol+10==$nl || $ol+9==$nl || $ol-1==$nl || $ol-11==$nl ){
echo "cost 100 credits!";
}
else if($ol-22==$nl || $ol-21==$nl || $ol-20==$nl || $ol-19==$nl || $ol-18==$nl || $ol-8==$nl || $ol+2==$nl || $ol+12==$nl || $ol+22==$nl
|| $ol+21==$nl || $ol+20==$nl || $ol+19==$nl || $ol+18==$nl || $ol+8==$nl || $ol-2==$nl || $ol-12==$nl ){
echo "cost 200 credits!";
}
That's the code for 1 and 2 tile travel. As you can see it's a lengthy statement.
I basically worked out a pattern for the grid I'd set up. For example, travelling up 1 tile would always be -10 of the current tile.
Before I type out any more ridiculously long if statements, is there a neater or more efficient way to do this?
I would use a different method: As the first digit defines the row and the second digit the column, I would split the number in these two digits and use these numbers to determine how many rows and how many columns are being travelled.
So for any position:
$row = floor($tile_value / 10);
$column = $tile_value % 10;
With this it is easy to calculate distances.
Edit: A small example to measure absolute distances:
$row_org = floor($tile_org_value / 10);
$column_org = $tile_org_value % 10;
$row_new = floor($tile_new_value / 10);
$column_new = $tile_new_value % 10;
$row_diff = $row_new - $row_org;
$col_diff = $col_new - $col_org;
$distance = sqrt(pow($row_diff, 2) + pow($col_diff, 2));
As in my comment above, you cannot measure distance in units, since not all points can be reached in a straight line through points.
You need to consider these points to be points (x, y coordinates) on a graph. Then you can get the distance between any 2 points using Pythagoras.
For example, if we consider your top row as being the coordinates (1,1) (1,2) and so on, if the person travels from (1,1) to (4,3), the distance travelled is the square root of 3 (4-1) squared plus 2 (3-1) squared, i.e. sqrt(9+4) = sqrt(13)
I would probably try an array for coordinates. This will allow you to set the initial coordinates. You can then pass new coordinates to the function which will move the position and calculate the cost.
<?php
$array = array( );
//populate the array with 0's
for( $i = 1; $i <= 5; $i++ ) {
for( $j = 1; $j <= 5; $j++ ) {
$array[$i][$j] = 0;
}
}
//set beginning position
$array[3][3] = 1;
function newPosition( $array, $newX, $newY ) {
$oldX = 0;
$oldY = 0;
//locate current position
foreach($array as $key=>$subArray) {
foreach($subArray as $subKey=>$val) {
if($val === 1) {
$oldX = $key;
$oldY = $subKey;
}
}
}
//delete old position
$array[$oldX][$oldY] = 0;
//set new position
$array[$newX][$newY] = 1;
//Calculate x and y difference
$xTravel = abs($oldX - $newX);
$yTravel = abs($oldY - $newY);
//Add x and y difference
$totalTravel = $xTravel + $yTravel;
//Calculate the cost
$totalCost = $totalTravel * 100;
echo "cost $totalCost credits!\n";
return $array;
}
$array = newPosition( $array, 5, 2 );
$array = newPosition( $array, 1, 5 );
$array = newPosition( $array, 1, 5 );
$array = newPosition( $array, 3, 3 );
Output
cost 300 credits!
cost 700 credits!
cost 0 credits!
cost 400 credits!
See the demo
Your code seems legit. You could order the conditions so that the most used ones are first.

Categories