using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace SBRL.Algorithms { /// /// Picks random items from a list, according to weights assigned to them. /// /// /// Higher weights mean that an item is picked more frequently than the other items. /// /// Mozilla Public License version 2.0 /// https://gist.github.com/sbrl/9090a8c646b8d34b6e0170ddfd197d09 /// Starbeamrainbowlabs (https://starbeamrainbowlabs.com/) /// /// v0.1 - 20th May 2017: /// - Creation! :D /// v0.2 - 17th Februrary 2018: /// - Add Count property /// - Add SetContents and ClearContents methods /// - Add empty constructor /// - Next() will now throw an InvalidOperationException if the generator's internal weights list is empty /// public class WeightedRandom { private Random rand = new Random(); protected ConcurrentDictionary weights = new ConcurrentDictionary(); public int Count { get { return weights.Count; } } /// /// Creates a new weighted random number generator. /// /// The dictionary of weights and their corresponding items. public WeightedRandom(IDictionary items) { SetContents(items); } /// /// Createse a new empty weighted random number generator. /// Remember to populate it before using! /// public WeightedRandom() { } public void SetContents(IDictionary items) { if (items.Count == 0) throw new ArgumentException("Error: The items dictionary provided is empty!"); double totalWeight = items.Values.Sum(); Parallel.ForEach(items, (KeyValuePair itemData) => { if (!weights.TryAdd(itemData.Key, itemData.Value / totalWeight)) throw new Exception("WeightedRandom: Failed to add new weight definition to weights ConcurrentDictionary!"); }); } public void ClearContents() { weights.Clear(); } /// /// Picks a new random item from the list provided at initialisation, based /// on the weights assigned to them. /// /// A random item, picked according to the assigned weights. public ItemType Next() { if (weights.Count == 0) throw new InvalidOperationException("Error: No weights specified! Add some with SetContents() before generating random numbers."); double target = rand.NextDouble(); double lower = 0; double higher = 0; foreach (KeyValuePair weightData in weights) { higher += weightData.Value; if (target >= lower && target <= higher) return weightData.Key; lower += weightData.Value; } throw new Exception($"Error: Unable to find the weight that matches {target}"); } } }