Search code examples
c#.netarraysrandomprojection

Converting random bytes to integers within a specified range


I am trying to write a function that uses the default RandomNumberGenerator implementation to generate Int32 values within a specified range.

void GenerateRandom (int [] data, int minInclusive, int maxExclusive)
{
    int size = 0;
    int length = 0;
    byte [] bytes = null;

    size = (int) Math.Ceiling(Math.Log(Math.Abs(maxExclusive - minInclusive), 2));
    length = data.Length * size;
    var bytes = new byte [length];

    using (RandomNumberGenerator generator = RandomNumberGenerator.Create())
    {
        generator.GetBytes(bytes);
    }

    // How to effectively convert this `byte []` to an `int []` within the specified range?
}

One attempt was to generate a random byte array of length (data.Length * ((int) Math.Ceiling(Math.Log(Math.Abs(maxExclusive - minInclusive), 2)))) and combine each x number of bytes to an int. Irrespective of specified range, this approach of course has the disadvantage of a huge bias towards larger values since there is little chance of multiple most significant bytes being zero.

Any input would be appreciated. Although I'm using .NET here, the platform/language doesn't matter. Looking for a conceptual hint.

Please note that I am already familiar with the Random class in .NET but am only interested in figuring out how to do this manually while being able to use the RandomNumberGenerator.


Solution

  • unsafe static int[] GenerateRandom(int length, int minInclusive, int maxExclusive)
    {
        var bytes = new byte[length * 4];
        var ints = new int[length];
    
        var ratio = uint.MaxValue / (double)(maxExclusive - minInclusive);
    
        using (RandomNumberGenerator generator = RandomNumberGenerator.Create())
        {
            generator.GetBytes(bytes);
            fixed(byte* b = bytes)
            {
                uint* i = (uint*)b;
                for(int j = 0; j < length; j++, i++)
                {
                    ints[j] = minInclusive + (int)(*i / ratio);
                }
            }
        }
    
        return ints;
    }
    

    I've run a little test:

    var ints = GenerateRandom(1000000, 0, 300);
    
    var groups = ints.GroupBy(x => x).Select(g => new { value = g.Key, count = g.Count() });
    var hist = Enumerable.Range(0, 300).Join(groups, x => x, g => g.value, (x, g) => new { value = x, count = g.count }).ToList();
    
    var max = hist.OrderByDescending(x => x.value).First();
    var min = hist.First();
    

    And results are quite random across all numbers between 0 and 300 with min.count = 3301 and max.count = 3358.