package com.ewjordan.util.random;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
public class Spinner {
static public void main(String[] args) {
testSpinner(0.1f);
testSpinner(0.1f, 0.2f,0.3f, 0.4f,0.1f, 0.2f,0.3f, 0.4f,0.1f, 0.2f,0.3f, 0.4f,0.1f, 0.2f,0.3f, 0.4f,0.1f, 0.2f,0.3f, 0.4f);
testSpinner(0.5f, 0.5f);
testSpinner(0.5f, 0.5f, 0.5f);
testSpinner(0.1f, 0.2f, 0.3f);
testSpinner(0.1f, 0.2f, 0.3f, 0.4f, 0.5f);
try {
testSpinner();
} catch (IllegalArgumentException e) {
e.printStackTrace(); //zero prob
}
try {
testSpinner(-1.0f, -2.0f);
} catch (IllegalArgumentException e) {
e.printStackTrace(); //negative prob
}
try {
testSpinner(1.0f, -0.5f);
} catch (IllegalArgumentException e) {
e.printStackTrace(); //negative prob
}
}
static private void testSpinner(float ... probs) {
final int N_TRIES = 10000000;
int[] countsNorm = new int[probs.length];
int[] countsNoAux = new int[probs.length];
int[] countsAux = new int[probs.length];
float[] aux = new float[probs.length];
long ms = System.currentTimeMillis();
for (int i=0; i<N_TRIES; ++i) {
countsNorm[normalizedSpinner(probs)] += 1;
countsNoAux[spinner(probs)] += 1;
countsAux[spinner(aux,probs)] += 1;
}
System.out.println("Probs\tNorm\tNoAux\tAux");
for (int i=0; i<probs.length; ++i) {
float normProb = countsNorm[i] / (float)N_TRIES;
float noAuxProb = countsNoAux[i] / (float)N_TRIES;
float auxProb = countsAux[i] / (float)N_TRIES;
System.out.println(probs[i]+"\t"+normProb+"\t"+noAuxProb+"\t"+auxProb);
}
long endms = System.currentTimeMillis();
float randPerSec = 3000 * (N_TRIES / (float)(endms-ms));
System.out.println("Took " + (endms-ms) + " ms, or " + ((endms-ms)/(float)(N_TRIES*3)) + " ms per randomization, for " + randPerSec + " randomizations per sec");
}
/**
* Spin a spinner with the given relative probabilities, get the index that results. This
* method does not alter the passed array.
* <pre>
* normalizedSpinner(10, 20, 30) => 1/6 chance of 0, 1/3 chance of 1, 1/2 chance of 2
* normalizedSpinner(1,1,2) => 1/4 chance of 0, 1/4 chance of 1, 1/2 chance of 2
* </pre>
* Note: during intermediate steps, this method will cause two float[] creations, the
* same length as the probabilities array. If creation is a concern, use {@link #spinner(float[], float[])}
* to pass your own pre-allocated intermediate array and handle the probability normalization yourself.
* Also remember that unless probabilities is passed as an array, Java's varargs will create another
* intermediate array.
*
* @param probabilities a list of relative probabilities
* @return the index of the event that is randomly chosen based on the provided probabilities
*/
public static int normalizedSpinner(float ... probabilities) {
float sum = 0.0f;
for (int i=0; i<probabilities.length; ++i) {
sum += probabilities[i];
}
if (sum == 0.0f) {
throw new IllegalArgumentException("At least one probability must be non-zero.");
} else if (sum < 0.0f) {
throw new IllegalArgumentException("Probabilities may not be negative.");
}
float[] normProbs = new float[probabilities.length];
for (int i=0; i<probabilities.length; ++i) {
normProbs[i] = probabilities[i] / sum;
}
return spinner(normProbs);
}
/**
* Spin a spinner with the given probabilities, get the index that results.
* Sum of probabilities should add up to 1f.
*
* If the sum of probabilities is less than 1, the last index gets all the
* remaining probability.
* e.g. spinner(0.5f, 0.3f) => 50% chance of 0, 50% chance of 1
*
* If the sum of probabilities is greater than 1, the probabilities are clipped
* at 1.
*
* e.g. spinner(0.5f, 0.3f, 0.5f) => 50% chance of 0, 30% chance of 1, 20% chance of 2 (extra 30% discarded)
*
* This method creates an intermediate float array the same length as probabilities; if garbage
* creation is a concern, use {@link #spinner(float[], float[])} instead, and provide a pre-allocated
* array of the correct length as the first parameter.
*
* @param probabilities a list of probabilities that should add up to 1
* @return the index of the event that is randomly chosen based on the provided probabilities
*/
public static int spinner(float ... probabilities) {
float[] mins = new float[probabilities.length];
return spinner(mins, probabilities);
}
/**
* Used instead of {@link #spinner(float...)} to avoid garbage creation by manually supplying
* intermediate array.
*
* @param auxiliaryArray a pre-allocated array to use for intermediate step that should
* have at least the same length as the probabilities array (used to avoid intermediate allocations)
* @param probabilities a list of probabilities that should add up to 1
* @return the index of the event that is chosen
* @see Spinner#spinner(float...)
*/
public static int spinner(float[] auxiliaryArray, float[] probabilities) {
if (auxiliaryArray.length < probabilities.length){
throw new IllegalArgumentException("Auxiliary array must have at least the same length as probabilities array.");
}
float sum = 0.0f;
float[] mins = auxiliaryArray;
for (int i = 0; i < probabilities.length; ++i) {
if (probabilities[i] < 0.0f) {
throw new IllegalArgumentException("Probabilities must be positive; received " + probabilities[i] +
" as parameter " + i + ".");
}
mins[i] = sum;
sum += probabilities[i];
}
double randomNumber = Math.random();
for (int i = probabilities.length - 1; i > 0; --i) {
if (randomNumber >= mins[i]) {
return i;
}
}
return 0;
}
/**
* Spin a spinner with the given relative probabilities, get the index that results. This
* method does not alter the passed array.
* <pre>
* normalizedSpinner(10, 20, 30) => 1/6 chance of 0, 1/3 chance of 1, 1/2 chance of 2
* normalizedSpinner(1,1,2) => 1/4 chance of 0, 1/4 chance of 1, 1/2 chance of 2
* </pre>
* Note: during intermediate steps, this method will cause two double[] creations, the
* same length as the probabilities array. If creation is a concern, use {@link #spinner(double[], double[])}
* to pass your own pre-allocated intermediate array and handle the probability normalization yourself.
* Also remember that unless probabilities is passed as an array, Java's varargs will create another
* intermediate array.
*
* @param probabilities a list of relative probabilities
* @return the index of the event that is randomly chosen based on the provided probabilities
*/
public static int normalizedSpinner(double ... probabilities) {
double sum = 0.0;
for (int i=0; i<probabilities.length; ++i) {
sum += probabilities[i];
}
if (sum == 0.0) {
double[] arr = new double[probabilities.length];
Arrays.fill(arr, 1);
return normalizedSpinner(arr);
} else if (sum < 0.0) {
throw new IllegalArgumentException("Probabilities may not be negative.");
}
double[] normProbs = new double[probabilities.length];
for (int i=0; i<probabilities.length; ++i) {
normProbs[i] = probabilities[i] / sum;
}
return spinner(normProbs);
}
/**
* Spin a spinner with the given probabilities, get the index that results.
* Sum of probabilities should add up to 1f.
*
* If the sum of probabilities is less than 1, the last index gets all the
* remaining probability.
* e.g. spinner(0.5f, 0.3f) => 50% chance of 0, 50% chance of 1
*
* If the sum of probabilities is greater than 1, the probabilities are clipped
* at 1.
*
* e.g. spinner(0.5f, 0.3f, 0.5f) => 50% chance of 0, 30% chance of 1, 20% chance of 2 (extra 30% discarded)
*
* This method creates an intermediate double array the same length as probabilities; if garbage
* creation is a concern, use {@link #spinner(double[], double[])} instead, and provide a pre-allocated
* array of the correct length as the first parameter.
*
* @param probabilities a list of probabilities that should add up to 1
* @return the index of the event that is randomly chosen based on the provided probabilities
*/
public static int spinner(double ... probabilities) {
double[] mins = new double[probabilities.length];
return spinner(mins, probabilities);
}
/**
* Used instead of {@link #spinner(double...)} to avoid garbage creation by manually supplying
* intermediate array.
*
* @param auxiliaryArray a pre-allocated array to use for intermediate step that should
* have at least the same length as the probabilities array (used to avoid intermediate allocations)
* @param probabilities a list of probabilities that should add up to 1
* @return the index of the event that is chosen
* @see Spinner#spinner(double...)
*/
public static int spinner(double[] auxiliaryArray, double[] probabilities) {
if (auxiliaryArray.length < probabilities.length){
throw new IllegalArgumentException("Auxiliary array must have at least the same length as probabilities array.");
}
double sum = 0.0;
double[] mins = auxiliaryArray;
for (int i = 0; i < probabilities.length; ++i) {
if (probabilities[i] < 0.0) {
throw new IllegalArgumentException("Probabilities must be positive; received " + probabilities[i] +
" as parameter " + i + ".");
}
mins[i] = sum;
sum += probabilities[i];
}
double randomNumber = Math.random();
for (int i = probabilities.length - 1; i > 0; --i) {
if (randomNumber >= mins[i]) {
return i;
}
}
return 0;
}
public static int spinner(List<Double> probabilities) {
double[] mins = new double[probabilities.size()];
for (int i=0; i<probabilities.size(); ++i) {
mins[i] = probabilities.get(i);
}
return spinner(mins);
}
public static int normalizedSpinner(List<Double> probabilities) {
double[] mins = new double[probabilities.size()];
for (int i=0; i<probabilities.size(); ++i) {
mins[i] = probabilities.get(i);
}
return normalizedSpinner(mins);
}
}