Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Coding pattern for random percentage branching?

So let's say we have a code block that we want to execute 70% of times and another one 30% of times.

if(Math.random() < 0.7)     70percentmethod(); else     30percentmethod(); 

Simple enough. But what if we want it to be easily expandable to say, 30%/60%/10% etc.? Here it would require adding and changing all the if statements on change which isn't exactly great to use, slow and mistake inducing.

So far I've found large switches to be decently useful for this use case, for example:

switch(rand(0, 10)){     case 0:     case 1:     case 2:     case 3:     case 4:     case 5:     case 6:     case 7:70percentmethod();break;     case 8:     case 9:     case 10:30percentmethod();break; } 

Which can be very easily changed to:

switch(rand(0, 10)){     case 0:10percentmethod();break;     case 1:     case 2:     case 3:     case 4:     case 5:     case 6:     case 7:60percentmethod();break;     case 8:     case 9:     case 10:30percentmethod();break; } 

But these have their drawbacks as well, being cumbersome and split onto a predetermined amount of divisions.

Something ideal would be based on a "frequency number" system I guess, like so:

(1,a),(1,b),(2,c) -> 25% a, 25% b, 50% c 

then if you added another one:

(1,a),(1,b),(2,c),(6,d) -> 10% a, 10% b, 20% c, 60% d 

So simply adding up the numbers, making the sum equal 100% and then split that.

I suppose it wouldn't be that much trouble to make a handler for it with a customized hashmap or something, but I'm wondering if there's some established way/pattern or lambda for it before I go all spaghetti on this.

like image 359
Moff Kalast Avatar asked Aug 23 '17 09:08

Moff Kalast


2 Answers

EDIT: See edit at end for more elegant solution. I'll leave this in though.

You can use a NavigableMap to store these methods mapped to their percentages.

NavigableMap<Double, Runnable> runnables = new TreeMap<>();  runnables.put(0.3, this::30PercentMethod); runnables.put(1.0, this::70PercentMethod);  public static void runRandomly(Map<Double, Runnable> runnables) {     double percentage = Math.random();     for (Map.Entry<Double, Runnable> entry : runnables){         if (entry.getKey() < percentage) {             entry.getValue().run();             return; // make sure you only call one method         }     }     throw new RuntimeException("map not filled properly for " + percentage); }  // or, because I'm still practicing streams by using them for everything public static void runRandomly(Map<Double, Runnable> runnables) {     double percentage = Math.random();     runnables.entrySet().stream()         .filter(e -> e.getKey() < percentage)         .findFirst().orElseThrow(() ->                  new RuntimeException("map not filled properly for " + percentage))         .run(); } 

The NavigableMap is sorted (e.g. HashMap gives no guarantees of the entries) by keys, so you get the entries ordered by their percentages. This is relevant because if you have two items (3,r1),(7,r2), they result in the following entries: r1 = 0.3 and r2 = 1.0 and they need to be evaluated in this order (e.g. if they are evaluated in the reverse order the result would always be r2).

As for the splitting, it should go something like this: With a Tuple class like this

static class Pair<X, Y> {     public Pair(X f, Y s)     {         first = f;         second = s;     }      public final X first;     public final Y second; } 

You can create a map like this

// the parameter contains the (1,m1), (1,m2), (3,m3) pairs private static Map<Double,Runnable> splitToPercentageMap(Collection<Pair<Integer,Runnable>> runnables) {      // this adds all Runnables to lists of same int value,     // overall those lists are sorted by that int (so least probable first)     double total = 0;     Map<Integer,List<Runnable>> byNumber = new TreeMap<>();     for (Pair<Integer,Runnable> e : runnables)     {         total += e.first;         List<Runnable> list = byNumber.getOrDefault(e.first, new ArrayList<>());         list.add(e.second);         byNumber.put(e.first, list);     }      Map<Double,Runnable> targetList = new TreeMap<>();     double current = 0;     for (Map.Entry<Integer,List<Runnable>> e : byNumber.entrySet())     {         for (Runnable r : e.getValue())         {             double percentage = (double) e.getKey() / total;             current += percentage;             targetList.put(current, r);         }     }      return targetList; } 

And all of this added to a class

class RandomRunner {     private List<Integer, Runnable> runnables = new ArrayList<>();     public void add(int value, Runnable toRun) {         runnables.add(new Pair<>(value, toRun));     }     public void remove(Runnable toRemove) {         for (Iterator<Pair<Integer, Runnable>> r = runnables.iterator();             r.hasNext(); ) {             if (toRemove == r.next().second) {                r.remove();                break;             }         }     }     public void runRandomly() {         // split list, use code from above     } } 

EDIT :
Actually, the above is what you get if you get an idea stuck in your head and don't question it properly. Keeping the RandomRunner class interface, this is much easier:

class RandomRunner {     List<Runnable> runnables = new ArrayList<>();     public void add(int value, Runnable toRun) {         // add the methods as often as their weight indicates.         // this should be fine for smaller numbers;         // if you get lists with millions of entries, optimize         for (int i = 0; i < value; i++) {             runnables.add(toRun);         }     }     public void remove(Runnable r) {         Iterator<Runnable> myRunnables = runnables.iterator();         while (myRunnables.hasNext()) {             if (myRunnables.next() == r) {                 myRunnables.remove();             }     }     public void runRandomly() {         if (runnables.isEmpty()) return;         // roll n-sided die         int runIndex = ThreadLocalRandom.current().nextInt(0, runnables.size());         runnables.get(runIndex).run();     } } 
like image 55
daniu Avatar answered Oct 08 '22 17:10

daniu


All these answers seem quite complicated, so I'll just post the keep-it-simple alternative:

double rnd = Math.random() if((rnd -= 0.6) < 0)     60percentmethod(); else if ((rnd -= 0.3) < 0)     30percentmethod(); else     10percentmethod(); 

Doesn't need changing other lines and one can quite easily see what happens, without digging into auxiliary classes. A small downside is that it doesn't enforce that percentages sum to 100%.

like image 39
jpa Avatar answered Oct 08 '22 16:10

jpa