Implement the method nextNum() and a minimal but effective set of unit tests. As a quick check, given Random Numbers are [-1, 0, 1, 2, 3] and Probabilities are [0.01, 0.3, 0.58, 0.1, 0.01] if we call nextNum() 100 times we may get the following results. As the results are random, these particular results are unlikely.
-1: 1 times
0: 22 times
1: 57 times
2: 20 times
3: 0 times
public class RandomGen {
// Values that may be returned by nextNum()
private int[] randomNums;
// Probability of the occurence of randomNums
private float[] probabilities;
/**
Returns one of the randomNums. When this method is called
multiple times over a long period, it should return the
numbers roughly with the initialized probabilities.
*/
public int nextNum() {
}
}
Solution:
package com.denizstij.rand;
import java.util.Arrays;
import java.util.Random;
/**
* @author Deniz Turan, http://denizstij.blogspot.co.uk denizstij AT gmail.com
*/
public class RandomGen {
// Values that may be returned by nextNum()
private int[] randomNums;
// Probability of the occurence of randomNums
private float[] probabilities;
private float[] cumProbabilities;
private int[] validRandomNums;
private int totalNonZeroProbElement=0;
private Random random;
public RandomGen(int[] randomNums,float[] probabilities){
validateProcessInputs(randomNums,probabilities);
random= new Random();
}
public RandomGen(int[] randomNums,float[] probabilities, long seed){
validateProcessInputs(randomNums,probabilities);
random= new Random(seed);
}
private void validateProcessInputs(int[] randomNums,float[] probabilities){
if (randomNums==null || probabilities==null || randomNums.length!= probabilities.length || probabilities.length==0){
throw new IllegalArgumentException("RandomNums and probabilities must be non empty and same size");
}
int len=probabilities.length;
cumProbabilities= new float[len];
validRandomNums= new int[len];
for (int i=0;i < len;i++) {
float prob = probabilities[i];
int randomNum = randomNums[i];
if (MathUtil.isLess(prob,0.0f)){
throw new IllegalArgumentException("Probability can not be negative");
}
if (MathUtil.isGreater(prob,1.0f)){
throw new IllegalArgumentException("Probability can not be greater than 1");
}
if (MathUtil.checkAnyIsNanOrInfinite(prob)){
throw new IllegalArgumentException("Nan or Infite prob is not accepted");
}
// not processing elements with zero probabilities as they can not occur
if (MathUtil.isEqual(prob, 0.0f)){
continue;
}
// store valid random number
validRandomNums[totalNonZeroProbElement]=randomNum;
if (totalNonZeroProbElement==0){
cumProbabilities[totalNonZeroProbElement]=prob;
} else {
cumProbabilities[totalNonZeroProbElement]=prob+cumProbabilities[totalNonZeroProbElement-1];
}
if (MathUtil.isGreater(cumProbabilities[totalNonZeroProbElement],1.0f)){
throw new IllegalArgumentException("Cumilative total probability can not be greater than 1");
}
totalNonZeroProbElement++;
}
if (totalNonZeroProbElement==0){
throw new IllegalArgumentException("All probabilities are zero :(");
}
if (!MathUtil.isEqual(cumProbabilities[totalNonZeroProbElement-1],1.0f)){
throw new IllegalArgumentException("Total probability must be 1");
}
// let's shrink arrays to save memory
this.validRandomNums=Arrays.copyOf(validRandomNums, totalNonZeroProbElement);
this.cumProbabilities=Arrays.copyOf(cumProbabilities, totalNonZeroProbElement);
this.randomNums=randomNums;
this.probabilities=probabilities;
// some logging
System.out.println("RandomNums :"+Arrays.toString(this.randomNums));
System.out.println("probabilities :"+Arrays.toString(this.probabilities));
System.out.println("totalNonZeroProbElement:"+totalNonZeroProbElement);
System.out.println("validRandomNums :"+Arrays.toString(validRandomNums));
System.out.println("cumProbabilities :"+Arrays.toString(cumProbabilities));
}
/**
Returns one of the randomNums. When this method is called
multiple times over a long period, it should return the
numbers roughly with the initialized probabilities.
*/
public int nextNum() {
float nextFloat = random.nextFloat();
return nextNum(nextFloat);
}
/**
Returns one of the randomNums. When this method is called
multiple times over a long period, it should return the
numbers roughly with the initialized probabilities.
*/
// For testing purposes
protected int nextNum(float nextFloat) {
int index = Arrays.binarySearch(cumProbabilities,nextFloat);
// if not found in the cum probability array, we need to infer index from insertation index
if (index<0 code="" index="-1*index-1;" int="" nextrandomval="" return="">0>
package com.denizstij.rand;
/**
* @author Deniz Turan, http://denizstij.blogspot.co.uk denizstij@gmail.com
*/
public class MathUtil {
public static final float FLOAT_COMPARE_RESOLUTION=0.00001f;
public static boolean isEqual(float val1, float val2) {
if (Math.abs(val1-val2)<FLOAT_COMPARE_RESOLUTION){
return true;
}
return false;
}
public static boolean isLess(float val1, float val2) {
if (isEqual(val1,val2)){
return false;
}
return val1<val2;
}
public static boolean isLessEqual(float val1, float val2) {
if (isEqual(val1,val2)){
return true;
}
return val1<val2;
}
public static boolean isGreater(float val1, float val2) {
if (isEqual(val1,val2)){
return false;
}
return val1>val2;
}
public static boolean isGreaterEqual(float val1, float val2) {
if (isEqual(val1,val2)){
return true;
}
return val1>val2;
}
public static boolean checkAnyIsNanOrInfinite(float ...values){
for (float val : values) {
if (Float.isNaN(val) || Float.isInfinite(val)){
return true;
}
}
return false;
}
}
Junit Test:
package com.denizstij.rand.test;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.junit.Assert;
import org.junit.Test;
import com.denizstij.rand.RandomGen;
/**
* @author Deniz Turan, http://denizstij.blogspot.co.uk denizstij@gmail.com
*/
public class RandomGenTest {
private static final float SIM_PROB_THRESHOLD = 0.001f;
@Test(expected=IllegalArgumentException.class)
public void testNullInputs() {
int[] randomNums=null;
float[] probabilities=null;
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testEmptyArray() {
int[] randomNums= new int[0];
float[] probabilities=new float[0];
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testDifferentSizeNumsAndProbs() {
int[] randomNums= {1};
float[] probabilities={0.3f,0.4f};
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testProbsAndNumberArrayDifferentSize() {
int[] randomNums= {1};
float[] probabilities={0.3f,0.4f};
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testProbsIsNotNegative() {
int[] randomNums= {1,2};
float[] probabilities={0.3f,-0.4f};
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testProbsAllZero() {
int[] randomNums= {1,2};
float[] probabilities={0.0f,0.0f};
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testProbArrayHasNaN() {
int[] randomNums= {1,2};
float[] probabilities={Float.NaN,0.4f};
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testProbsIsNotGreaterThanOne() {
int[] randomNums= {1,2};
float[] probabilities={0.3f,1.4f};
new RandomGen(randomNums, probabilities);
}
@Test(expected=IllegalArgumentException.class)
public void testProbsTotalIsNotOne() {
int[] randomNums= {1,2};
float[] probabilities={0.3f,0.4f};
new RandomGen(randomNums, probabilities);
}
@Test
public void testCumulativeProbLogic() {
int[] randomNums= {-1,-2,-4,3410,893,9,343,10};
float[] probabilities={0.0f,0.0f,0.0f,0.3f,0.5f,0.0f,0.2f,0.0f};
//validRandomNums :[3410, 893, 343]
//cumProbabilities :[0.3, 0.8, 1.0]
RandomGenWrapper randomGen = new RandomGenWrapper(randomNums, probabilities);
// checking random number:3410 which has cum prob of 0<= and <=0.3
int nextRandomNum = randomGen.nextNum(0.2f);
Assert.assertTrue(nextRandomNum==3410);
nextRandomNum = randomGen.nextNum(0.299f);
Assert.assertTrue(nextRandomNum==3410);
nextRandomNum = randomGen.nextNum(0.3f);
Assert.assertTrue(nextRandomNum==3410);
// checking random number:893 which has cum prob of 0.3< and <=0.8
nextRandomNum = randomGen.nextNum(0.30001f);
Assert.assertTrue(nextRandomNum==893);
nextRandomNum = randomGen.nextNum(0.51f);
Assert.assertTrue(nextRandomNum==893);
nextRandomNum = randomGen.nextNum(0.799999f);
Assert.assertTrue(nextRandomNum==893);
nextRandomNum = randomGen.nextNum(0.8f);
Assert.assertTrue(nextRandomNum==893);
// checking random number:343 which has cum prob of 0.8< and <=1
nextRandomNum = randomGen.nextNum(0.800001f);
Assert.assertTrue(nextRandomNum==343);
nextRandomNum = randomGen.nextNum(0.9f);
Assert.assertTrue(nextRandomNum==343);
nextRandomNum = randomGen.nextNum(0.9999999f);
Assert.assertTrue(nextRandomNum==343);
nextRandomNum = randomGen.nextNum(1);
Assert.assertTrue(nextRandomNum==343);
}
@Test
public void testNonOccuringEventsWithProbZero() {
// -1 event should never happen here
int[] randomNums= {5,-1,2,4};
float[] probabilities={0.1f,0.0f,0.6f,0.3f};
RandomGen randomGen = new RandomGen(randomNums, probabilities);
//lets try several times if this true
int maxTry=100000;
for (int i=0;i<maxTry;i++){
int nextNum = randomGen.nextNum();
// make sure we never get -1 which has prob 0
Assert.assertNotEquals(nextNum, -1);
}
}
@Test
public void testAlwaysOccuringEventsWithProbOne() {
// -1 event should always happen here
int[] randomNums= {5,-1,2,4};
float[] probabilities={0.0f,1.0f,0.0f,0.0f};
RandomGen randomGen = new RandomGen(randomNums, probabilities);
//lets try several times if this true
int maxTry=100000;
for (int i=0;i<maxTry;i++){
int nextNum = randomGen.nextNum();
// make sure we always get -1 which has prob 1
Assert.assertEquals(nextNum, -1);
}
}
/*
* Generate huge amount of random next number, given probability and random number arrays.
* Then, frequency of each random number is estimated.
* Finally it is asserted that frequency is close to the input probability within a threshold (0.001)
*/
@Test
public void testProvidedQuickCheck() {
int[] randomNums= {-1,0,1,2,3};
float[] probabilities={0.01f, 0.3f, 0.58f, 0.1f, 0.01f};
float[] simFrequency= new float[probabilities.length];
Map <Integer,Long> counterMap= new LinkedHashMap<Integer,Long>();
// reset counter map
for (int randomNum : randomNums) {
counterMap.put(randomNum, 0L);
}
RandomGen randomGen = new RandomGen(randomNums, probabilities,1234L);
//lets generate many times random numbers
int maxTry=10000000;
for (int i=0;i<maxTry;i++){
int nextNum = randomGen.nextNum();
Long counter = counterMap.get(nextNum);
counter++;
counterMap.put(nextNum, counter);
}
// estimate frequency
int index=0;
for (Entry<Integer, Long> entry : counterMap.entrySet()) {
Long counter = entry.getValue();
// simulation frequencies
simFrequency[index]=counter/(float)maxTry;
float simProbDifference = Math.abs(simFrequency[index]-probabilities[index]);
//
Assert.assertTrue(simProbDifference<SIM_PROB_THRESHOLD);
index++;
}
System.out.println("Counter Map "+counterMap);
System.out.println("Freq map:"+Arrays.toString(simFrequency));
}
private class RandomGenWrapper extends RandomGen{
public RandomGenWrapper(int[] randomNums, float[] probabilities) {
super(randomNums, probabilities);
}
public int nextNum(float nextFloat){
return super.nextNum(nextFloat);
}
}
}
1 comment:
nextNum(float nextFloat) {} doesn't finish
Post a Comment