Method conversion to use any Enum

My problem:

I want to convert the randomBloodType () method to a static method that can accept any enum type. I want my method to use any type of enum, be it BloodType, DaysOfTheWeek, etc., and follow the operations shown below.

Some details about what the method does:

The method currently selects a random item from the BloodType enumeration based on the values ​​assigned to each item. An item with a higher value has a higher probability of being selected.

code:

    public enum BloodType
    {
        // BloodType = Probability
        ONeg = 4,
        OPos = 36,
        ANeg = 3,
        APos = 28,
        BNeg = 1,
        BPos = 20,
        ABNeg = 1,
        ABPos = 5
    };

    public BloodType randomBloodType()
    {
        // Get the values of the BloodType enum and store it in a array
        BloodType[] bloodTypeValues = (BloodType[])Enum.GetValues(typeof(BloodType));
        List<BloodType> bloodTypeList = new List<BloodType>();

        // Create a list where each element occurs the approximate number of 
        // times defined as its value(probability)
        foreach (BloodType val in bloodTypeValues)
        {
            for(int i = 0; i < (int)val; i++)
            {
                bloodTypeList.Add(val);
            }
        }

        // Sum the values
        int sum = 0;
        foreach (BloodType val in bloodTypeValues)
        {
            sum += (int)val;
        }

        //Get Random value
        Random rand = new Random();
        int randomValue = rand.Next(sum);

        return bloodTypeList[randomValue];

    }

      

What I have tried so far:

I have tried using generics. They worked for the most part, but I was unable to cast enum elements to int values. I've included a sample code section that gave me the problems below.

    foreach (T val in bloodTypeValues)
    {
        sum += (int)val; // This line is the problem.
    }

      

I also tried using Enum e as a method parameter. I have not been able to declare the type of an array of enumeration items using this method.

+3


source to share


3 answers


Assuming your enum values ​​are of type int

(you can adjust this accordingly if they are long

, short

or whatever):

static TEnum RandomEnumValue<TEnum>(Random rng)
{
    var vals = Enum
        .GetNames(typeof(TEnum))
        .Aggregate(Enumerable.Empty<TEnum>(), (agg, curr) =>
        {
            var value = Enum.Parse(typeof (TEnum), curr);
            return agg.Concat(Enumerable.Repeat((TEnum)value,(int)value)); // For int enums
        })
        .ToArray();

    return vals[rng.Next(vals.Length)];
}

      

This is how you use it:

var rng = new Random();
var randomBloodType = RandomEnumValue<BloodType>(rng);

      


People seem to have their panties in a node with multiple indistinguishable enum values ​​in the input enum (for which I still believe the above code provides the expected behavior). Note that there is no answer here, not even Peter Duniho, which will allow you to distinguish enum entries when they have the same value, so I'm not sure why this is considered a metric for any potential solutions.

However, an alternative approach that does not use the enumeration values ​​as probabilities is to use an attribute to indicate the probability:



public enum BloodType
{
    [P=4]
    ONeg,
    [P=36]
    OPos,
    [P=3]
    ANeg,
    [P=28]
    APos,
    [P=1]
    BNeg,
    [P=20]
    BPos,
    [P=1]
    ABNeg,
    [P=5]
    ABPos
}

      

This is what the attribute used above looks like:

[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class PAttribute : Attribute
{
    public int Weight { get; private set; }

    public PAttribute(int weight)
    {
        Weight = weight;
    }
}

      

and finally this is what the method for getting the random value of the enum would like:

static TEnum RandomEnumValue<TEnum>(Random rng)
{
    var vals = Enum
        .GetNames(typeof(TEnum))
        .Aggregate(Enumerable.Empty<TEnum>(), (agg, curr) =>
        {
            var value = Enum.Parse(typeof(TEnum), curr);

            FieldInfo fi = typeof (TEnum).GetField(curr);
            var weight = ((PAttribute)fi.GetCustomAttribute(typeof(PAttribute), false)).Weight;

            return agg.Concat(Enumerable.Repeat((TEnum)value, weight)); // For int enums
        })
        .ToArray();

    return vals[rng.Next(vals.Length)];
}

      

(Note: if this code is performance critical, you may need to tweak it and add caching for the reflection data).

0


source


( Note: I apologize in advance for the long answer. My actual proposed solution is not that long, but there are still a number of problems with the proposed solutions, and I want to try to go through them thoroughly, provide context for my own proposed solution).


In my opinion, although you have actually accepted one answer and may be tempted to use one of them, none of the answers provided so far are correct or helpful.

Ben Voigt's comment has already pointed out two major flaws with your specs, as pointed out, both stem from the fact that you are encoding the weight of an enum value in the value itself:

  • You bind the underlying type of the enum to code, which then must interpret that type.
  • Two enumeration values ​​that have the same weight are indistinguishable from each other.

Both of these issues can be resolved. Indeed, while the answer you accepted (why?) Cannot solve the first problem, the one provided by Dweeberly addresses it with a Convert.ToInt32()

(which can convert from long

to int

just fine, so long since the values ​​are small enough).

But the second question is much more difficult to solve. The answer from Asad tries to solve this by starting with enumeration names and parsing their values. And that actually results in indexing the final array containing the corresponding entries for each name separately. But code that actually uses an enumeration has no way of distinguishing between the two; it is indeed as if the two names were the only enumeration value, and this single enumeration probability weight is the sum of the value used for the two different names.

those. in your example, while enumeration entries, for example, BNeg

and ABNeg

will be selected separately, the code that gets these randomly selected values ​​has no way of knowing if BNeg

or was selected ABNeg

. As far as is known, these are only two different names for the same meaning.

Now even this problem can be solved (but not in the way Assad is trying & hellip, his answer is still broken). If you, for example, could encode the probabilities into a value while keeping the values ​​unique for each name, you could decode those probabilities when you make a random selection, and that would work. For example:

enum BloodType
{
    // BloodType = Probability
    ONeg = 4 * 100 + 0,
    OPos = 36 * 100 + 1,
    ANeg = 3 * 100 + 2,
    APos = 28 * 100 + 3,
    BNeg = 1 * 100 + 4,
    BPos = 20 * 100 + 5,
    ABNeg = 1 * 100 + 6,
    ABPos = 5 * 100 + 7,
};

      

By declaring your enum values ​​this way, you can divide the enum value by 100 in your selection code to get its probability weight, which you can then use, as shown in various examples. At the same time, each enumeration name has a unique meaning.

But even with this problem, you still have problems with the choice of encoding and the representation of probabilities. For example, in the above, you cannot have an enumeration with more than 100 values ​​and not a weight greater than (2 ^ 31 - 1) / 100; if you want an enumeration that has more than 100 values, you need a larger multiplier, but that will further lower your weight values.

In many scenarios (maybe all you care about) this won't be a problem. The rooms are small enough to be all right. But this seems like a serious limitation in what seems like a situation where you want the solution to be as general as possible.

And that's not it. Even if the encoding remains within reasonable limits, you have another significant limit: a random selection process requires an array large enough to hold as many values ​​as its weight for each enumeration value. Again, if the values ​​are small, this is probably not a big problem. But this severely limits your implementation's ability to generalize.


So what to do?

I understand the temptation to try and keep each enum type self-contained; there are some obvious benefits to doing this. But there are some serious drawbacks that follow from this, and if you do ever try to use it in a generic way, changes to the suggested solutions will tie your code together in a way that IMHO negates most, if not all, of the advantage of keeping enum types autonomously ( first of all: if you find that you need to change the implementation to accommodate any new enum type you will have to go back and edit all the other enum types you are using and hellip; the type looks self-contained, in fact they are all tight connected to each other).



In my opinion, a much better approach would be to ditch the idea that the enumeration type itself would encode probability weights. Just accept that it will be announced separately in some way.

Also, IMHO it would be better to avoid the memory intensive approach suggested in your original question and reflected in the other two answers. Yes, that's fine for the small values ​​you are dealing with. But this is an unnecessary limitation, simplifying only one small part of the logic, making it difficult and limiting in other ways.

I propose the following solution where the enumeration values ​​can be whatever you want, the base type of the enum can be whatever you want, and the algorithm uses memory in proportion only to the number of unique enumeration values, not proportionally to the sum of all the probability weights.

In this solution, I also address potential performance issues by caching the invariant data structures used to select random values. This may or may not be useful in your case, depending on how often you generate these random values. But IMHO is a good idea; the initial cost of generating these data structures is so high that if values ​​are chosen with any regularity at all, it will dominate at runtime. Even if it works great today, why take the risk? (Again, especially considering that you seem to want a generic solution.)

Here's a basic solution:

static T NextRandomEnumValue<T>()
{
    KeyValuePair<T, int>[] aggregatedWeights = GetWeightsForEnum<T>();
    int weightedValue =
            _random.Next(aggregatedWeights[aggregatedWeights.Length - 1].Value),

        index = Array.BinarySearch(aggregatedWeights,
            new KeyValuePair<T, int>(default(T), weightedValue),
            KvpValueComparer<T, int>.Instance);

    return aggregatedWeights[index < 0 ? ~index : index + 1].Key;
}

static KeyValuePair<T, int>[] GetWeightsForEnum<T>()
{
    object temp;

    if (_typeToAggregatedWeights.TryGetValue(typeof(T), out temp))
    {
        return (KeyValuePair<T, int>[])temp;
    }

    if (!_typeToWeightMap.TryGetValue(typeof(T), out temp))
    {
        throw new ArgumentException("Unsupported enum type");
    }

    KeyValuePair<T, int>[] weightMap = (KeyValuePair<T, int>[])temp;
    KeyValuePair<T, int>[] aggregatedWeights =
        new KeyValuePair<T, int>[weightMap.Length];
    int sum = 0;

    for (int i = 0; i < weightMap.Length; i++)
    {
        sum += weightMap[i].Value;
        aggregatedWeights[i] = new KeyValuePair<T,int>(weightMap[i].Key, sum);
    }

    _typeToAggregatedWeights[typeof(T)] = aggregatedWeights;

    return aggregatedWeights;
}

readonly static Random _random = new Random();

// Helper method to reduce verbosity in the enum-to-weight array declarations
static KeyValuePair<T1, T2> CreateKvp<T1, T2>(T1 t1, T2 t2)
{
    return new KeyValuePair<T1, T2>(t1, t2);
}

readonly static KeyValuePair<BloodType, int>[] _bloodTypeToWeight =
{
    CreateKvp(BloodType.ONeg, 4),
    CreateKvp(BloodType.OPos, 36),
    CreateKvp(BloodType.ANeg, 3),
    CreateKvp(BloodType.APos, 28),
    CreateKvp(BloodType.BNeg, 1),
    CreateKvp(BloodType.BPos, 20),
    CreateKvp(BloodType.ABNeg, 1),
    CreateKvp(BloodType.ABPos, 5),
};

readonly static Dictionary<Type, object> _typeToWeightMap =
    new Dictionary<Type, object>()
    {
        { typeof(BloodType), _bloodTypeToWeight },
    };

readonly static Dictionary<Type, object> _typeToAggregatedWeights =
    new Dictionary<Type, object>();

      

Note that the job of actually picking a random value is simply picking a non-negative random integer less than the sum of the weights and then using binary search to find the corresponding enumeration value.

After each type of enumeration, the code will build a table of values ​​and weights to be used for the binary search. This result is stored in the dictionary cache _typeToAggregatedWeights

.

There are also objects that need to be declared and that will be used at runtime to create this table. Please note that _typeToWeightMap

only this method is 100% supported. If you wanted to write a different named method for each specific type that you wanted to support, it could still use one generic method to implement initialization and selection, but the named method would know the correct object (for example _bloodTypeToWeight

) to use for initialization.

Alternatively, another way to avoid _typeToWeightMap

, maintaining the general method of 100% will be type _typeToAggregatedWeights

-type Dictionary<Type, Lazy<object>>

and have values dictionary (objects Lazy<object>

), obviously refers to the weight corresponding to the array type.

In other words, there are many variations on this theme that will work just fine. But they will all have the same structure as above; the semantics will be the same and the performance differences will be minor.

One thing you will notice is that a custom implementation is required for binary search IComparer<T>

. Here:

class KvpValueComparer<TKey, TValue> :
    IComparer<KeyValuePair<TKey, TValue>> where TValue : IComparable<TValue>
{
    public readonly static KvpValueComparer<TKey, TValue> Instance =
        new KvpValueComparer<TKey, TValue>();

    private KvpValueComparer() { }

    public int Compare(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
    {
        return x.Value.CompareTo(y.Value);
    }
}

      

This allows the method to Array.BinarySearch()

fix the comparison of array elements, allowing a single array to contain both enumeration values ​​and their aggregated weights, but restricting binary search comparisons to weights only.

+2


source


Some of these you can do, and some of them are not easy. I believe the following extension method will do what you describe.

static public class Util {
    static Random rnd = new Random();
    static public int PriorityPickEnum(this Enum e) {
        // The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong
        // However, Random only supports a int (or double) as a max value.  Either way
        // it doesn't have the range for uint, long and ulong.
        //
        // sum enum 
        int sum = 0;
        foreach (var x in Enum.GetValues(e.GetType())) {
            sum += Convert.ToInt32(x);
            }

        var i = rnd.Next(sum); // get a random value, it will form a ratio i / sum

        // enums may not have a uniform (incremented) value range (think about flags)
        // therefore we have to step through to get to the range we want,
        // this is due to the requirement that return value have a probability
        // proportional to it value.  Note enum values must be sorted for this to work.
        foreach (var x in Enum.GetValues(e.GetType()).OfType<Enum>().OrderBy(a => a)) {
            i -= Convert.ToInt32(x);
            if (i <= 0) return Convert.ToInt32(x);
            }
        throw new Exception("This doesn't seem right");
        }
    }

      

Here's an example using this extension:

        BloodType bt = BloodType.ABNeg;
        for (int i = 0; i < 100; i++) {
            var v = (BloodType) bt.PriorityPickEnum();
            Console.WriteLine("{0}:  {1}({2})", i, v, (int) v);
            }

      

This should work well for enumerating byte type, sbyte, ushort, short and int. As soon as you go outside of int (uint, long, ulong), the problem is in the Random class. You can tweak the code to use doubles generated by Random that will cover the uint, but the Random class just doesn't have a range to cover long and long ones. You can of course use / find / write another Random class if that matters.

-1


source







All Articles