PHP generate RGB - php

I'm facing this situation where I have an ID which comes from a database (so it can be 1, 100, 1000, ...) and I need to generate random colors, however equal ID's should result in the same color.
Any suggestion on how I can achieve this?
Thanks!

Use a cryptographic hash and clip the bytes you don't need:
function getColor($num) {
$hash = md5('color' . $num); // modify 'color' to get a different palette
return array(
hexdec(substr($hash, 0, 2)), // r
hexdec(substr($hash, 2, 2)), // g
hexdec(substr($hash, 4, 2))); //b
}
The resulting (code to generate it) looks like this for the numbers 0-20:

<?php
// someting like this?
$randomString = md5($your_id_here); // like "d73a6ef90dc6a ..."
$r = substr($randomString,0,2); //1. and 2.
$g = substr($randomString,2,2); //3. and 4.
$b = substr($randomString,4,2); //5. and 6.
?>
<style>
#topbar { border-bottom:4px solid #<?php echo $r.$g.$b; ?>; }
</style>

The obvious approach is to just convert the ID into a color (e.g. lower 8 bits are the blue, next 8 bits are Green, next 8 are Red - leave 8 bits, but I'm sure you can figure that out ;-)
Assuming this doesn't work (cos you end up with a horrible color palette:
Use an array (or hash table) to make a mapping of IDs to Colors.
If you are concerned that there are too many IDs, then you could apply some hash to the ID and use that as you key into the "id to color" mapping. In this case you are effectively saying one id always has one color, but one color can be used by many IDs.

If the array is always sorted, you can use this algorythm up to 250 items:
<?php
function getRGBColorString( $array )
{
$indexColor = round( 250 / count( $array ) );
$iterator = 1;
$arrayOfRGB = array();
foreach( $array as $item)
{
$arrayOfRGB[] = "rgb(" . ( $indexColor * $iterator ) . ", 113, 113 )";
$iterator++;
}
return $arrayOfRGB;
}
?>

Related

Line index 1 in array with index 1 in another array

I have searched for hours and cannot find the answer.
I have 2 arrays that will always be the same length
array_a(12, 13, 14);
array_b(15, 18, 20);
How would I print to 3 different variables (or arrays) like so...
$array_c(12, 15);
$array_d(13, 18);
$array_e(14, 20);
So they line up.
Thanks
Okay I figured it out. I guess I should explain what I was trying to
do better so that the code is easier to understand. I have an
application in which a user can enter room sizes for the square
footage of a room. Because all rooms are not always square, I wanted
to be able to allow the user who had a "L" shaped room to divide the
room up and enter multiple measurements for each room. So they could
add another row to a table to add the length and width for the bottom
of the "L". (I know its hard to explain) Anyway, the user could submit
2 squares for a room that is odd shaped. Since my database only has 1
row per room, I figured it would be better to add multiple measurements inside of the same field separated by a space which I could explode later if I need to. Anyway, this is what I came up with
$width = $_POST['width']; //an array
$length = $_POST['length']; //an array
$dimensions_array = array();
foreach ($width as $key => $value) {
$individual_length = $length[$key];
array_push($dimensions_array, $value.'x'.$individual_length);
}
$dimensions = implode(' ', $dimensions_array);
This returns an array called $dimensions that is (12x15 13x18 14x20)
$width = $_POST['width']; //an array
$length = $_POST['length']; //an array
$dimensions_array = array();
foreach ($width as $key => $value) {
$individual_length = $length[$key];
array_push($dimensions_array, $value.'x'.$individual_length);
}
$dimensions = implode(' ', $dimensions_array);

array picking by percent [duplicate]

I know how to generate a random number in PHP but lets say I want a random number between 1-10 but I want more 3,4,5's then 8,9,10's. How is this possible? I would post what I have tried but honestly, I don't even know where to start.
Based on #Allain's answer/link, I worked up this quick function in PHP. You will have to modify it if you want to use non-integer weighting.
/**
* 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;
}
}
}
For an efficient random number skewed consistently towards one end of the scale:
Choose a continuous random number between 0..1
Raise to a power γ, to bias it. 1 is unweighted, lower gives more of the higher numbers and vice versa
Scale to desired range and round to integer
eg. in PHP (untested):
function weightedrand($min, $max, $gamma) {
$offset= $max-$min+1;
return floor($min+pow(lcg_value(), $gamma)*$offset);
}
echo(weightedrand(1, 10, 1.5));
There's a pretty good tutorial for you.
Basically:
Sum the weights of all the numbers.
Pick a random number less than that
subtract the weights in order until the result is negative and return that number if it is.
This tutorial walks you through it, in PHP, with multiple cut and paste solutions. Note that this routine is slightly modified from what you'll find on that page, as a result of the comment below.
A function taken from the post:
/**
* weighted_random_simple()
* Pick a random item based on weights.
*
* #param array $values Array of elements to choose from
* #param array $weights An array of weights. Weight must be a positive number.
* #return mixed Selected element.
*/
function weighted_random_simple($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(1, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
/**
* #param array $weightedValues
* #return string
*/
function getRandomWeightedElement(array $weightedValues)
{
$array = array();
foreach ($weightedValues as $key => $weight) {
$array = array_merge(array_fill(0, $weight, $key), $array);
}
return $array[array_rand($array)];
}
getRandomWeightedElement(array('A'=>10, 'B'=>90));
This is very easy method. How get random weighted element. I fill array variable $key. I get $key to array $weight x. After that, use array_rand to array. And I have random value ;).
Plain and fair.
Just copy/paste and test it.
/**
* Return weighted probability
* #param (array) prob=>item
* #return key
*/
function weightedRand($stream) {
$pos = mt_rand(1,array_sum(array_keys($stream)));
$em = 0;
foreach ($stream as $k => $v) {
$em += $k;
if ($em >= $pos)
return $v;
}
}
$item['30'] = 'I have more chances than everybody :]';
$item['10'] = 'I have good chances';
$item['1'] = 'I\'m difficult to appear...';
for ($i = 1; $i <= 10; $i++) {
echo weightedRand($item).'<br />';
}
Edit: Added missing bracket at the end.
You can use weightedChoice from Non-standard PHP library. It accepts a list of pairs (item, weight) to have the possibility to work with items that can't be array keys. You can use pairs function to convert array(item => weight) to the needed format.
use function \nspl\a\pairs;
use function \nspl\rnd\weightedChoice;
$weights = pairs(array(
1 => 10,
2 => 15,
3 => 15,
4 => 15,
5 => 15,
6 => 10,
7 => 5,
8 => 5,
9 => 5,
10 => 5
));
$number = weightedChoice($weights);
In this example, 2-5 will appear 3 times more often than 7-10.
i used Brad's answar and changed it a little to fit my situation and add more flexibility
i have an array with array value
$products = [
['id'=>1,'name'=> 'product1' , 'chance'=>2] ,
['id'=>2,'name'=> 'product2' , 'chance'=>7]
]
first i shuffle the products array
shuffle($products );
then you can pass it to the function
function getRandomWeightedElement(array $products) {
$chancesSum = 0;
foreach ($products as $product){
$chancesSum += (int) $product['chance'];
}
$rand = mt_rand(1, $chancesSum);
$range = 0;
foreach ($products as $product) {
$range += (int) $product['chance'];
$compare = $rand - $range;
if ($compare <= 0){
return (int) $product['id'];
}
}}
Since I used IainMH's solution, I may as well share my PHP code:
<pre><?php
// Set total number of iterations
$total = 1716;
// Set array of random number
$arr = array(1, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5);
$arr2 = array(0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 5);
// Print out random numbers
for ($i=0; $i<$total; $i++){
// Pick random array index
$rand = array_rand($arr);
$rand2 = array_rand($arr2);
// Print array values
print $arr[$rand] . "\t" . $arr2[$rand2] . "\r\n";
}
?></pre>
I just released a class to perform weighted sorting easily.
It's based on the same algorithm mentioned in Brad's and Allain's answers, and is optimized for speed, unit-tested for uniform distribution, and supports elements of any PHP type.
Using it is simple. Instantiate it:
$picker = new Brick\Random\RandomPicker();
Then add elements as an array of weighted values (only if your elements are strings or integers):
$picker->addElements([
'foo' => 25,
'bar' => 50,
'baz' => 100
]);
Or use individual calls to addElement(). This method supports any kind of PHP values as elements (strings, numbers, objects, ...), as opposed to the array approach:
$picker->addElement($object1, $weight1);
$picker->addElement($object2, $weight2);
Then get a random element:
$element = $picker->getRandomElement();
The probability of getting one of the elements depends on its associated weight. The only restriction is that weights must be integers.
Many of the answers on this page seem to use array bloating, excessive iteration, a library, or a hard-to-read process. Of course, everyone thinks their own baby is the cutest, but I honestly think my approach is lean, simple and easy to read/modify...
Per the OP, I will create an array of values (declared as keys) from 1 to 10, with 3, 4, and 5 having double the weight of the other values (declared as values).
$values_and_weights=array(
1=>1,
2=>1,
3=>2,
4=>2,
5=>2,
6=>1,
7=>1,
8=>1,
9=>1,
10=>1
);
If you are only going to make one random selection and/or your array is relatively small* (do your own benchmarking to be sure), this is probably your best bet:
$pick=mt_rand(1,array_sum($values_and_weights));
$x=0;
foreach($values_and_weights as $val=>$wgt){
if(($x+=$wgt)>=$pick){
echo "$val";
break;
}
}
This approach involves no array modification and probably won't need to iterate the entire array (but may).
On the other hand, if you are going to make more than one random selection on the array and/or your array is sufficiently large* (do your own benchmarking to be sure), restructuring the array may be better.
The cost in memory for generating a new array will be increasingly justified as:
array size increases and
number of random selections increases.
The new array requires the replacement of "weight" with a "limit" for each value by adding the previous element's weight to the current element's weight.
Then flip the array so that the limits are the array keys and the values are the array values.
The logic is: the selected value will have the lowest limit that is >= $pick.
// Declare new array using array_walk one-liner:
array_walk($values_and_weights,function($v,$k)use(&$limits_and_values,&$x){$limits_and_values[$x+=$v]=$k;});
//Alternative declaration method - 4-liner, foreach() loop:
/*$x=0;
foreach($values_and_weights as $val=>$wgt){
$limits_and_values[$x+=$wgt]=$val;
}*/
var_export($limits_and_values);
Creates this array:
array (
1 => 1,
2 => 2,
4 => 3,
6 => 4,
8 => 5,
9 => 6,
10 => 7,
11 => 8,
12 => 9,
13 => 10,
)
Now to generate the random $pick and select the value:
// $x (from walk/loop) is the same as writing: end($limits_and_values); $x=key($limits_and_values);
$pick=mt_rand(1,$x); // pull random integer between 1 and highest limit/key
while(!isset($limits_and_values[$pick])){++$pick;} // smallest possible loop to find key
echo $limits_and_values[$pick]; // this is your random (weighted) value
This approach is brilliant because isset() is very fast and the maximum number of isset() calls in the while loop can only be as many as the largest weight (not to be confused with limit) in the array. For this case, maximum iterations = 2!
THIS APPROACH NEVER NEEDS TO ITERATE THE ENTIRE ARRAY
I used this:
mt_rand($min, mt_rand($min, $max));
it give more lower values and less higher values, since the more the value is high the more is cutted out by one of the mt_rand
The probability is linearly increasing in the lower values, forming a square diagonal (see maths lower)
PRO: easy and strightforward
CON: maybe too simple so not enough weightable or balanceable for some use case
Maths:
let i index of i-nth value from min to max,
let P(i) the probability of obtaining the i-nth value,
let N=max-min:
P(i)=(1+N-i)/sum(1,N)
Since N is equals for all terms:
P(i) is proportional to N-i
so, in facts, the probability is linearly increasing in the lower values, forming a square diagonal
Variants:
you can write variants:
mt_rand($min, mt_rand(1, mt_rand(1, $max))); //value more given in low part
mt_rand(mt_rand($min, $max), $max); //mirrored, more upper values than lower
...
function getBucketFromWeights($values) {
$total = $currentTotal = $bucket = 0;
foreach ($values as $amount) {
$total += $amount;
}
$rand = mt_rand(0, $total-1);
foreach ($values as $amount) {
$currentTotal += $amount;
if ($rand => $currentTotal) {
$bucket++;
}
else {
break;
}
}
return $bucket;
}
I ugh modified this from an answer here Picking random element by user defined weights
After I wrote this I saw someone else had an even more elegant answer. He he he he.

Hex Code Brightness PHP?

I want users on my website to be able to pick a hex colour, and I just want to display white text for dark colours and black text for light colours. Can you work out the brightness from a hex code (preferably PHP)?
$hex = "78ff2f"; //Bg color in hex, without any prefixing #!
//break up the color in its RGB components
$r = hexdec(substr($hex,0,2));
$g = hexdec(substr($hex,2,2));
$b = hexdec(substr($hex,4,2));
//do simple weighted avarage
//
//(This might be overly simplistic as different colors are perceived
// differently. That is a green of 128 might be brighter than a red of 128.
// But as long as it's just about picking a white or black text color...)
if($r + $g + $b > 382){
//bright color, use dark font
}else{
//dark color, use bright font
}
I made one similar - but based on weightings of each colour (based on the C# version of this thread)
function readableColour($bg){
$r = hexdec(substr($bg,0,2));
$g = hexdec(substr($bg,2,2));
$b = hexdec(substr($bg,4,2));
$contrast = sqrt(
$r * $r * .241 +
$g * $g * .691 +
$b * $b * .068
);
if($contrast > 130){
return '000000';
}else{
return 'FFFFFF';
}
}
echo readableColour('000000'); // Output - FFFFFF
EDIT:
Small optimisation:
Sqrt is known as an expensive math operation, which is probably neglectable in most scenarios, but anyway, it could be avoided by doing something like this.
function readableColour($bg){
$r = hexdec(substr($bg,0,2));
$g = hexdec(substr($bg,2,2));
$b = hexdec(substr($bg,4,2));
$squared_contrast = (
$r * $r * .299 +
$g * $g * .587 +
$b * $b * .114
);
if($squared_contrast > pow(130, 2)){
return '000000';
}else{
return 'FFFFFF';
}
}
echo readableColour('000000'); // Output - FFFFFF
It simply doesn't apply the sqrt, instead it powers the desired cut off contrast by two, which is a much cheaper calculation
I know this is a very old topic, but for users who came from "Google Search", this link may be what they are looking for. I've searched for something like this and I think it's a good idea to post it here:
https://github.com/mexitek/phpColors
use Mexitek\PHPColors\Color;
// Initialize my color
$myBlue = new Color("#336699");
echo $myBlue->isLight(); // false
echo $myBlue->isDark(); // true
That's it.
You need to convert the RGB values to HLS/HSL (Hue Lightness and Saturation) you can then use the Lightness to determine whether you need light text or dark text.
This page has some details on how to the conversion in PHP as well as selecting complementary colour from this.
I've only just spotted that the site is an astrology site - so apologies if anyone's offended.
If you have imagemagick extension activated, you can simply create an ImagickPixel object, call setColor with your hex value, and then call getHSL() (and get the last item of the obtained array I suppose)...
I tried a different approach to this, I used HSL (hue, saturation & lightness) lightness percentage to check if the color is dark or light. (like #chrisf said in his answer)
function:
function colorislight($hex) {
$hex = str_replace('#', '', $hex);
$r = (hexdec(substr($hex, 0, 2)) / 255);
$g = (hexdec(substr($hex, 2, 2)) / 255);
$b = (hexdec(substr($hex, 4, 2)) / 255);
$lightness = round((((max($r, $g, $b) + min($r, $g, $b)) / 2) * 100));
return ($lightness >= 50 ? true : false);
}
On the return line it checks if the lightness percentage is higher than 50% and returns true otherwise false is returned. You can easily change it to return true if the color has 30% lightness and so on. The $lightness variable can return from 0 to 100 0 being the darkest and 100 being the lightest.
how to use the function:
$color = '#111111';
if ( colorislight($color) ) {
echo 'this color is light';
}
else {
echo 'this color is dark';
}

How can I obtain the ID from the following example string?

I use the following method to pad out a ID for a property on our website:
function generateAgentRef($id,$length=5,$strPrefix='1'){
return $strPrefix . str_pad($id,$length,0,0);
}
Basically it will prefix 1 and then pad out the id with 0's until the string reaches $length.
But, I now have a requirement to revert this process. For example if I have the following IDs: 100650,100359,100651,100622,100112,100687, how can I get the ID e.g. 650, 359, 651, 622, 112, 687?
Hope this explains what I'm trying to achieve.
The ID in the database will never start with 0, so I was thinking of iterating over the components of the string and detecting when I hit something other than 0 and then splitting the string.
substract 100000 from the generated ref and intval() it could work if the length is 6 numbers exactly.
try using this
$a = substr($num,3);
here $num is the id you get
$a will be your desired number i.e 100659 shortened to 659
Expanding on your initial function
function getAgentId($id, $length = 5, $strPrefix = 1){
return $id - generateAgentRef(0, $length, $strPrefix);
}
$id = generateAgentRef(255);
echo $id, PHP_EOL; // 100255
echo getAgentId($id), PHP_EOL; //255

Generating random results by weight in PHP?

I know how to generate a random number in PHP but lets say I want a random number between 1-10 but I want more 3,4,5's then 8,9,10's. How is this possible? I would post what I have tried but honestly, I don't even know where to start.
Based on #Allain's answer/link, I worked up this quick function in PHP. You will have to modify it if you want to use non-integer weighting.
/**
* 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;
}
}
}
For an efficient random number skewed consistently towards one end of the scale:
Choose a continuous random number between 0..1
Raise to a power γ, to bias it. 1 is unweighted, lower gives more of the higher numbers and vice versa
Scale to desired range and round to integer
eg. in PHP (untested):
function weightedrand($min, $max, $gamma) {
$offset= $max-$min+1;
return floor($min+pow(lcg_value(), $gamma)*$offset);
}
echo(weightedrand(1, 10, 1.5));
There's a pretty good tutorial for you.
Basically:
Sum the weights of all the numbers.
Pick a random number less than that
subtract the weights in order until the result is negative and return that number if it is.
This tutorial walks you through it, in PHP, with multiple cut and paste solutions. Note that this routine is slightly modified from what you'll find on that page, as a result of the comment below.
A function taken from the post:
/**
* weighted_random_simple()
* Pick a random item based on weights.
*
* #param array $values Array of elements to choose from
* #param array $weights An array of weights. Weight must be a positive number.
* #return mixed Selected element.
*/
function weighted_random_simple($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(1, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
/**
* #param array $weightedValues
* #return string
*/
function getRandomWeightedElement(array $weightedValues)
{
$array = array();
foreach ($weightedValues as $key => $weight) {
$array = array_merge(array_fill(0, $weight, $key), $array);
}
return $array[array_rand($array)];
}
getRandomWeightedElement(array('A'=>10, 'B'=>90));
This is very easy method. How get random weighted element. I fill array variable $key. I get $key to array $weight x. After that, use array_rand to array. And I have random value ;).
Plain and fair.
Just copy/paste and test it.
/**
* Return weighted probability
* #param (array) prob=>item
* #return key
*/
function weightedRand($stream) {
$pos = mt_rand(1,array_sum(array_keys($stream)));
$em = 0;
foreach ($stream as $k => $v) {
$em += $k;
if ($em >= $pos)
return $v;
}
}
$item['30'] = 'I have more chances than everybody :]';
$item['10'] = 'I have good chances';
$item['1'] = 'I\'m difficult to appear...';
for ($i = 1; $i <= 10; $i++) {
echo weightedRand($item).'<br />';
}
Edit: Added missing bracket at the end.
You can use weightedChoice from Non-standard PHP library. It accepts a list of pairs (item, weight) to have the possibility to work with items that can't be array keys. You can use pairs function to convert array(item => weight) to the needed format.
use function \nspl\a\pairs;
use function \nspl\rnd\weightedChoice;
$weights = pairs(array(
1 => 10,
2 => 15,
3 => 15,
4 => 15,
5 => 15,
6 => 10,
7 => 5,
8 => 5,
9 => 5,
10 => 5
));
$number = weightedChoice($weights);
In this example, 2-5 will appear 3 times more often than 7-10.
i used Brad's answar and changed it a little to fit my situation and add more flexibility
i have an array with array value
$products = [
['id'=>1,'name'=> 'product1' , 'chance'=>2] ,
['id'=>2,'name'=> 'product2' , 'chance'=>7]
]
first i shuffle the products array
shuffle($products );
then you can pass it to the function
function getRandomWeightedElement(array $products) {
$chancesSum = 0;
foreach ($products as $product){
$chancesSum += (int) $product['chance'];
}
$rand = mt_rand(1, $chancesSum);
$range = 0;
foreach ($products as $product) {
$range += (int) $product['chance'];
$compare = $rand - $range;
if ($compare <= 0){
return (int) $product['id'];
}
}}
Since I used IainMH's solution, I may as well share my PHP code:
<pre><?php
// Set total number of iterations
$total = 1716;
// Set array of random number
$arr = array(1, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5);
$arr2 = array(0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 5);
// Print out random numbers
for ($i=0; $i<$total; $i++){
// Pick random array index
$rand = array_rand($arr);
$rand2 = array_rand($arr2);
// Print array values
print $arr[$rand] . "\t" . $arr2[$rand2] . "\r\n";
}
?></pre>
I just released a class to perform weighted sorting easily.
It's based on the same algorithm mentioned in Brad's and Allain's answers, and is optimized for speed, unit-tested for uniform distribution, and supports elements of any PHP type.
Using it is simple. Instantiate it:
$picker = new Brick\Random\RandomPicker();
Then add elements as an array of weighted values (only if your elements are strings or integers):
$picker->addElements([
'foo' => 25,
'bar' => 50,
'baz' => 100
]);
Or use individual calls to addElement(). This method supports any kind of PHP values as elements (strings, numbers, objects, ...), as opposed to the array approach:
$picker->addElement($object1, $weight1);
$picker->addElement($object2, $weight2);
Then get a random element:
$element = $picker->getRandomElement();
The probability of getting one of the elements depends on its associated weight. The only restriction is that weights must be integers.
Many of the answers on this page seem to use array bloating, excessive iteration, a library, or a hard-to-read process. Of course, everyone thinks their own baby is the cutest, but I honestly think my approach is lean, simple and easy to read/modify...
Per the OP, I will create an array of values (declared as keys) from 1 to 10, with 3, 4, and 5 having double the weight of the other values (declared as values).
$values_and_weights=array(
1=>1,
2=>1,
3=>2,
4=>2,
5=>2,
6=>1,
7=>1,
8=>1,
9=>1,
10=>1
);
If you are only going to make one random selection and/or your array is relatively small* (do your own benchmarking to be sure), this is probably your best bet:
$pick=mt_rand(1,array_sum($values_and_weights));
$x=0;
foreach($values_and_weights as $val=>$wgt){
if(($x+=$wgt)>=$pick){
echo "$val";
break;
}
}
This approach involves no array modification and probably won't need to iterate the entire array (but may).
On the other hand, if you are going to make more than one random selection on the array and/or your array is sufficiently large* (do your own benchmarking to be sure), restructuring the array may be better.
The cost in memory for generating a new array will be increasingly justified as:
array size increases and
number of random selections increases.
The new array requires the replacement of "weight" with a "limit" for each value by adding the previous element's weight to the current element's weight.
Then flip the array so that the limits are the array keys and the values are the array values.
The logic is: the selected value will have the lowest limit that is >= $pick.
// Declare new array using array_walk one-liner:
array_walk($values_and_weights,function($v,$k)use(&$limits_and_values,&$x){$limits_and_values[$x+=$v]=$k;});
//Alternative declaration method - 4-liner, foreach() loop:
/*$x=0;
foreach($values_and_weights as $val=>$wgt){
$limits_and_values[$x+=$wgt]=$val;
}*/
var_export($limits_and_values);
Creates this array:
array (
1 => 1,
2 => 2,
4 => 3,
6 => 4,
8 => 5,
9 => 6,
10 => 7,
11 => 8,
12 => 9,
13 => 10,
)
Now to generate the random $pick and select the value:
// $x (from walk/loop) is the same as writing: end($limits_and_values); $x=key($limits_and_values);
$pick=mt_rand(1,$x); // pull random integer between 1 and highest limit/key
while(!isset($limits_and_values[$pick])){++$pick;} // smallest possible loop to find key
echo $limits_and_values[$pick]; // this is your random (weighted) value
This approach is brilliant because isset() is very fast and the maximum number of isset() calls in the while loop can only be as many as the largest weight (not to be confused with limit) in the array. For this case, maximum iterations = 2!
THIS APPROACH NEVER NEEDS TO ITERATE THE ENTIRE ARRAY
I used this:
mt_rand($min, mt_rand($min, $max));
it give more lower values and less higher values, since the more the value is high the more is cutted out by one of the mt_rand
The probability is linearly increasing in the lower values, forming a square diagonal (see maths lower)
PRO: easy and strightforward
CON: maybe too simple so not enough weightable or balanceable for some use case
Maths:
let i index of i-nth value from min to max,
let P(i) the probability of obtaining the i-nth value,
let N=max-min:
P(i)=(1+N-i)/sum(1,N)
Since N is equals for all terms:
P(i) is proportional to N-i
so, in facts, the probability is linearly increasing in the lower values, forming a square diagonal
Variants:
you can write variants:
mt_rand($min, mt_rand(1, mt_rand(1, $max))); //value more given in low part
mt_rand(mt_rand($min, $max), $max); //mirrored, more upper values than lower
...
function getBucketFromWeights($values) {
$total = $currentTotal = $bucket = 0;
foreach ($values as $amount) {
$total += $amount;
}
$rand = mt_rand(0, $total-1);
foreach ($values as $amount) {
$currentTotal += $amount;
if ($rand => $currentTotal) {
$bucket++;
}
else {
break;
}
}
return $bucket;
}
I ugh modified this from an answer here Picking random element by user defined weights
After I wrote this I saw someone else had an even more elegant answer. He he he he.

Categories