More RandomUtil work

This commit is contained in:
Cj
2025-01-08 00:00:37 -05:00
parent a85bc22568
commit da764079f3
2 changed files with 221 additions and 14 deletions
+154 -7
View File
@@ -9,6 +9,12 @@ public class RandomUtil
{
private readonly Random _random = new();
/// <summary>
/// The IEEE-754 standard for double-precision floating-point numbers limits the number of digits (including both
/// integer + fractional parts) to about 1517 significant digits. 15 is a safe upper bound, so we'll use that.
/// </summary>
public const int MaxSignificantDigits = 15;
/// <summary>
/// Generates a random integer between the specified minimum and maximum values, inclusive.
/// </summary>
@@ -39,7 +45,7 @@ public class RandomUtil
}
/// <summary>
/// Generates a random floating-point number within the specified range.
/// Generates a random floating-point number within the specified range ~6-9 digits (4 bytes).
/// </summary>
/// <param name="min">The minimum value of the range (inclusive).</param>
/// <param name="max">The maximum value of the range (exclusive).</param>
@@ -48,6 +54,17 @@ public class RandomUtil
{
return (float)GetSecureRandomNumber() * (max - min) + min;
}
/// <summary>
/// Generates a random floating-point number within the specified range ~15-17 digits (8 bytes).
/// </summary>
/// <param name="min">The minimum value of the range (inclusive).</param>
/// <param name="max">The maximum value of the range (exclusive).</param>
/// <returns>A random floating-point number between `min` (inclusive) and `max` (exclusive).</returns>
public double GetDouble(double min, double max)
{
return GetSecureRandomNumber() * (max - min) + min;
}
/// <summary>
/// Generates a random boolean value.
@@ -110,9 +127,9 @@ public class RandomUtil
}
/// <summary>
/// Returns a random string from the provided collection of strings.
/// Returns a random type T from the provided collection of type T.
/// </summary>
/// <param name="collection"></param>
/// <param name="collection">The collection to get the random element from</param>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <returns>A random element from the collection.</returns>
/// <remarks>This was formerly getArrayValue() in the node server</remarks>
@@ -132,6 +149,134 @@ public class RandomUtil
{
return GetCollectionValue(dictionary.Keys);
}
/// <summary>
/// Gets a random val from the given dictionary
/// </summary>
/// <param name="dictionary">The dictionary from which to retrieve a value.</param>
/// <typeparam name="TKey">Type of key</typeparam>
/// <typeparam name="TVal">Type of Value</typeparam>
/// <returns>A random TVal representing one of the values of the dictionary.</returns>
public TVal GetVal<TKey, TVal>(Dictionary<TKey, TVal> dictionary) where TKey : notnull
{
return GetCollectionValue(dictionary.Values);
}
/// <summary>
/// Generates a normally distributed random number using the Box-Muller transform.
/// </summary>
/// <param name="mean">The mean (μ) of the normal distribution.</param>
/// <param name="sigma">The standard deviation (σ) of the normal distribution.</param>
/// <param name="attempt">The current attempt count to generate a valid number (default is 0).</param>
/// <returns>A normally distributed random number.</returns>
/// <remarks>
/// This function uses the Box-Muller transform to generate a normally distributed random number.
/// If the generated number is less than 0, it will recursively attempt to generate a valid number up to 100 times.
/// If it fails to generate a valid number after 100 attempts, it will return a random float between 0.01 and twice the mean.
/// </remarks>
public double GetNormallyDistributedRandomNumber(double mean, double sigma, int attempt = 0)
{
double u, v;
do
{
u = GetSecureRandomNumber();
} while (u == 0);
do
{
v = GetSecureRandomNumber();
} while (v == 0);
// Apply the Box-Muller transform
var w = Math.Sqrt(-2.0 * Math.Log(u)) * Math.Cos(2.0 * Math.PI * v);
var valueDrawn = mean + w * sigma;
// Check if the generated value is valid
if (valueDrawn < 0)
{
return attempt > 100
? GetDouble(0.01f, mean * 2f)
: GetNormallyDistributedRandomNumber(mean, sigma, attempt + 1);
}
return valueDrawn;
}
/// <summary>
/// Generates a random integer between the specified range.
/// </summary>
/// <param name="low">The lower bound of the range (inclusive).</param>
/// <param name="high">The upper bound of the range (exclusive). If not provided, the range will be from 0 to `low`.</param>
/// <returns>A random integer within the specified range.</returns>
public int RandInt(int low, int? high)
{
// Return a random integer from 0 to low if high is not provided
if (high is null)
{
return _random.Next(0, low);
}
// Return low directly when low and high are equal
return low == high
? low
: _random.Next(low, (int)high);
}
/// <summary>
/// Generates a random number between two given values with optional precision.
/// </summary>
/// <param name="val1">The first value to determine the range.</param>
/// <param name="val2">The second value to determine the range. If not provided, 0 is used.</param>
/// <param name="precision">
/// The number of decimal places to round the result to. Must be a positive integer between 0
/// and MaxSignificantDigits(15), inclusive. If not provided, precision is determined by the input values.
/// </param>
/// <returns></returns>
public double RandNum(double val1, double val2 = 0, byte? precision = null)
{
if (!double.IsFinite(val1) || !double.IsFinite(val2))
{
throw new ArgumentException("RandNum() parameters 'value1' and 'value2' must be finite numbers.");
}
// Determine the range
var min = Math.Min(val1, val2);
var max = Math.Max(val1, val2);
// Validate and adjust precision
if (precision is not null)
{
if (precision > MaxSignificantDigits)
{
throw new ArgumentOutOfRangeException(
nameof(precision), "Must be less than 16");
}
// Calculate the number of whole-number digits in the maximum absolute value of the range
var maxAbsoluteValue = Math.Max(Math.Abs(min), Math.Abs(max));
var wholeNumberDigits = (int)Math.Floor(Math.Log10(maxAbsoluteValue)) + 1;
var maxAllowedPrecision = Math.Max(0, MaxSignificantDigits - wholeNumberDigits);
if (precision > maxAllowedPrecision)
{
throw new ArgumentException(
$"RandNum() precision of {precision} exceeds the allowable precision ({maxAllowedPrecision}) for the given values."
);
}
}
var result = GetSecureRandomNumber() * (max - min) + min;
// Determine effective precision
var maxPrecision = Math.Max(GetNumberPrecision(val1), GetNumberPrecision(val2));
var effectivePrecision = precision ?? maxPrecision;
var factor = Math.Pow(2, effectivePrecision);
return Math.Round(result * factor) / factor;
}
/// <summary>
/// Generates a secure random number between 0 (inclusive) and 1 (exclusive).
@@ -158,8 +303,6 @@ public class RandomUtil
const ulong maxInt = 1UL << 48;
Console.WriteLine(integer);
return (double)Math.Abs(integer) / maxInt;
}
@@ -168,8 +311,12 @@ public class RandomUtil
/// </summary>
/// <param name="num">The number to analyze.</param>
/// <returns>The number of decimal places, or 0 if none exist.</returns>
private static int GetNumberPrecision(double num)
public int GetNumberPrecision(double num)
{
return num.ToString().Split('.')[1]?.Length ?? 0;
var parts = num.ToString($"G{MaxSignificantDigits}").Split('.');
return parts.Length > 1
? parts[1].Length
: 0;
}
}
+67 -7
View File
@@ -17,7 +17,7 @@ public sealed class RandomUtilTests
if (result < 0 || result > 10)
{
Assert.Fail($"GetInt() out of range. Expected range [0, 10] but was {result}.");
Assert.Fail($"GetInt(0, 10) out of range. Expected range [0, 10] but was {result}.");
}
}
}
@@ -32,7 +32,7 @@ public sealed class RandomUtilTests
if (result < 1 || result > 9)
{
Assert.Fail($"GetInt() out of range. Expected range [1, 9] but was {result}.");
Assert.Fail($"GetInt(10) out of range. Expected range [1, 9] but was {result}.");
}
}
}
@@ -47,7 +47,7 @@ public sealed class RandomUtilTests
if (result < 0f || result >= 9f)
{
Assert.Fail($"GetFloat() out of range. Expected range [0.0f, 8.99f] but was {result}.");
Assert.Fail($"GetFloat(0f, 10f) out of range. Expected range [0.0f, 9.999f] but was {result}.");
}
}
}
@@ -62,7 +62,7 @@ public sealed class RandomUtilTests
expected,
result,
0.0001f,
$"GetPercentOfValue() out of range. Expected: {expected}. Actual: {result}.");
$"GetPercentOfValue(45.5f, 100f) out of range. Expected: {expected}. Actual: {result}.");
}
[TestMethod]
@@ -75,7 +75,7 @@ public sealed class RandomUtilTests
expected,
result,
0.0001f,
$"ReduceValueByPercent() out of range. Expected: {expected}. Actual: {result}.");
$"ReduceValueByPercent(100f, 45.5f) out of range. Expected: {expected}. Actual: {result}.");
}
[TestMethod]
@@ -89,7 +89,7 @@ public sealed class RandomUtilTests
Assert.AreEqual(
expectedTrue,
resultTrue,
$"GetChance100() out of range. Expected: {expectedTrue}. Actual: {resultTrue}.");
$"GetChance100(100f) out of range. Expected: {expectedTrue}. Actual: {resultTrue}.");
}
for (var i = 0; i < 100; i++)
@@ -100,7 +100,67 @@ public sealed class RandomUtilTests
Assert.AreEqual(
expectedFalse,
resultFalse,
$"GetChance100() out of range. Expected: {expectedFalse}. Actual: {resultFalse}.");
$"GetChance100(0f) out of range. Expected: {expectedFalse}. Actual: {resultFalse}.");
}
}
// TODO: Missing methods between these two
[TestMethod]
public void RandIntTest()
{
for (var i = 0; i < 100; i++)
{
var result = _randomUtil.RandInt(0, 10);
if (result < 0 || result > 9)
{
Assert.Fail($"RandInt(0, 10) out of range. Expected range [0, 9] but was {result}.");
}
}
for (var i = 0; i < 100; i++)
{
var result = _randomUtil.RandInt(10, null);
if (result < 0 || result > 9)
{
Assert.Fail($"RandInt(10, null) out of range. Expected range [0, 9] but was {result}.");
}
}
}
[TestMethod]
public void RandNumTest()
{
for (var i = 0; i < 100; i++)
{
var result = _randomUtil.RandNum(0, 10);
if (result < 0 || result > 9)
{
Assert.Fail($"RandNum(0, 10) out of range. Expected range [0, 9.999d] but was {result}.");
}
if (_randomUtil.GetNumberPrecision(result) > RandomUtil.MaxSignificantDigits)
{
Assert.Fail($"RandNum(0, 10) precision of {result} exceeds the allowable precision ({RandomUtil.MaxSignificantDigits}) for the given values.");
}
}
for (var i = 0; i < 100; i++)
{
var result = _randomUtil.RandNum(10);
if (result < 0 || result > 9)
{
Assert.Fail($"RandNum(10) out of range. Expected range [0, 9.999d] but was {result}.");
}
if (_randomUtil.GetNumberPrecision(result) > RandomUtil.MaxSignificantDigits)
{
Assert.Fail($"RandNum(10) precision of {result} exceeds the allowable precision ({RandomUtil.MaxSignificantDigits}) for the given values.");
}
}
}
}