MarkovGrams/MarkovGrams/Utilities/WeightedRandom.cs

96 lines
2.9 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SBRL.Algorithms
{
/// <summary>
/// Picks random items from a list, according to weights assigned to them.
/// </summary>
/// <remarks>
/// Higher weights mean that an item is picked more frequently than the other items.
/// </remarks>
/// <license>Mozilla Public License version 2.0</license>
/// <origin>https://gist.github.com/sbrl/9090a8c646b8d34b6e0170ddfd197d09</origin>
/// <author>Starbeamrainbowlabs (https://starbeamrainbowlabs.com/)</author>
/// <changelog>
/// 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
/// </changelog>
public class WeightedRandom<ItemType>
{
private Random rand = new Random();
protected ConcurrentDictionary<ItemType, double> weights = new ConcurrentDictionary<ItemType, double>();
public int Count {
get {
return weights.Count;
}
}
/// <summary>
/// Creates a new weighted random number generator.
/// </summary>
/// <param name="items">The dictionary of weights and their corresponding items.</param>
public WeightedRandom(IDictionary<ItemType, double> items)
{
SetContents(items);
}
/// <summary>
/// Createse a new empty weighted random number generator.
/// Remember to populate it before using!
/// </summary>
public WeightedRandom()
{
}
public void SetContents(IDictionary<ItemType, double> items)
{
if (items.Count == 0)
throw new ArgumentException("Error: The items dictionary provided is empty!");
double totalWeight = items.Values.Sum();
Parallel.ForEach(items, (KeyValuePair<ItemType, double> 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();
}
/// <summary>
/// Picks a new random item from the list provided at initialisation, based
/// on the weights assigned to them.
/// </summary>
/// <returns>A random item, picked according to the assigned weights.</returns>
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<ItemType, double> 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}");
}
}
}