The class can be used with just about any collection and you can pass in a delegate/lamda to determine what the weight is on each collection object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
// | |
// THIS IS ALL JUST TEST CODE | |
// | |
// THE ACTUAL WEIGHTED RANDOM CODE IS IN THE WeightedRandom.cs file | |
// | |
namespace PaulTechGuy.Weights | |
{ | |
class Program | |
{ | |
class Foo | |
{ | |
public string Name { get; set; } | |
public int Weight { get; set; } | |
public Foo(string name, int weight) | |
{ | |
Name = name; | |
Weight = weight; | |
} | |
} | |
static void Main(string[] args) | |
{ | |
new Program().Run(args); | |
} | |
private void Run(string[] args) | |
{ | |
// test data | |
Foo[] items = new Foo[] | |
{ | |
// there are six individual weights from 10 to 60) | |
new Foo("Sally Target=17% (1/6th)", 10), | |
new Foo("Fred Target=30% (1/3th)", 30), // or 2/6 | |
new Foo("John Target=50% (1/2th)", 60), // or 3/6 | |
}; | |
// here is the magic; declare your collection Func weight delegate and | |
// create the WeightedRandom object; after that you just call Next() to | |
// get your next random object based on weights | |
Func<Foo, int> fweight = f => f.Weight; | |
WeightedRandom<Foo> foos = new WeightedRandom<Foo>(items, fweight, new Random()); | |
// spit out a bunch of test results | |
for (int times = 0; times < 5; ++times) | |
{ | |
// generate a bunch of random test data and store counts | |
Dictionary<string, int> statistics = new Dictionary<string, int>(); | |
for (int i = 0; i < 1000; ++i) | |
{ | |
Foo foo = foos.Next(); | |
if (!statistics.ContainsKey(foo.Name)) | |
{ | |
statistics.Add(foo.Name, 0); | |
} | |
statistics[foo.Name] += 1; | |
} | |
// print out results | |
double total = statistics.Sum(s => s.Value); | |
foreach (var kvp in statistics) | |
{ | |
Console.WriteLine("{0} => {1} (Actual {2}%)", kvp.Key, kvp.Value, Math.Round(kvp.Value / total * 100)); | |
} | |
Console.WriteLine(); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics.Contracts; | |
using System.Linq; | |
namespace PaulTechGuy.Weights | |
{ | |
public class WeightedRandom<T> | |
{ | |
private readonly List<IGrouping<int, T>> _itemMap; | |
private readonly Random _random; | |
private readonly int _maxWeight; | |
public WeightedRandom(T[] items, Func<T, int> weight, Random rnd) | |
{ | |
Contract.Requires(items.Length > 0); | |
_random = rnd; | |
// map the items by weight and sort by weight | |
_itemMap = items | |
.GroupBy(weight) | |
.OrderBy(g => weight) | |
.ToList(); | |
// weights are sorted in ascending order; max weight will be last element | |
_maxWeight = _itemMap.ElementAt(_itemMap.Count - 1).Key; | |
} | |
public T Next() | |
{ | |
// select a random weight | |
int randomWeight = _random.Next(_maxWeight + 1); | |
// get the first group where our random weight is less then element weight | |
IGrouping<int, T> items = _itemMap | |
.FirstOrDefault(b => randomWeight <= b.Key); | |
// since we based our randomWeight on the maxWeight, we better have found one | |
if (items == null) | |
{ | |
throw new InvalidOperationException("Internal bug: should have found at least the maxWeight element"); | |
} | |
// there may be more than one element with this weight; take a random one | |
T item = items.ElementAt(_random.Next(items.Count())); | |
return item; | |
} | |
} | |
} |
No comments:
Post a Comment