/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.service.abtesting; import java.util.Random; import java.util.Set; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.dao.ABTestEventDao; /** * Helper class to facilitate A/B testing by randomly choosing an option * based on probabilities that can be supplied from local defaults * @author Sam Bosley <sam@astrid.com> * */ public class ABChooser { public static final int NO_OPTION = -1; @Autowired private ABTests abTests; @Autowired private ABTestEventDao abTestEventDao; private final Random random; public ABChooser() { DependencyInjectionService.getInstance().inject(this); random = new Random(); } /** * Iterates through the list of all available tests and makes sure that a choice * is made for each of them */ public void makeChoicesForAllTests(boolean newUser, boolean activatedUser) { Set<String> tests = abTests.getAllTestKeys(); for (String test : tests) { makeChoiceForTest(test, newUser, activatedUser); } } /** * If a choice/variant has not yet been selected for the specified test, * make one and create the initial +0 analytics data point * * @param testKey - the preference key string of the option (defined in ABTests) */ private void makeChoiceForTest(String testKey, boolean newUser, boolean activatedUser) { int pref = readChoiceForTest(testKey); if (pref > NO_OPTION) return; int chosen = NO_OPTION; if (abTests.isValidTestKey(testKey)) { int[] optionProbs = abTests.getProbsForTestKey(testKey, newUser); String[] optionDescriptions = abTests.getDescriptionsForTestKey(testKey); chosen = chooseOption(optionProbs); setChoiceForTest(testKey, chosen); String desc = optionDescriptions[chosen]; abTestEventDao.createInitialTestEvent(testKey, desc, newUser, activatedUser); } return; } /** * Returns the chosen option if set or NO_OPTION if unset * @param testKey * @return */ public static int readChoiceForTest(String testKey) { return Preferences.getInt(testKey, NO_OPTION); } /** * Changes the choice of an A/B feature in the preferences. Useful for * the feature flipper (can manually override previous choice) * @param testKey * @param choiceIndex */ public void setChoiceForTest(String testKey, int choiceIndex) { if (abTests.isValidTestKey(testKey)) Preferences.setInt(testKey, choiceIndex); } /* * Helper method to choose an option from an int[] corresponding to the * relative weights of each option. Returns the index of the chosen option. */ private int chooseOption(int[] optionProbs) { int sum = 0; for (int opt : optionProbs) // Compute sum sum += opt; double rand = random.nextDouble() * sum; // Get uniformly distributed double between [0, sum) sum = 0; for (int i = 0; i < optionProbs.length; i++) { sum += optionProbs[i]; if (rand <= sum) return i; } return optionProbs.length - 1; } }