diff --git a/Core/Utils/RandomUtil.cs b/Core/Utils/RandomUtil.cs
index 50a9ebf2..5a99eddc 100644
--- a/Core/Utils/RandomUtil.cs
+++ b/Core/Utils/RandomUtil.cs
@@ -9,6 +9,12 @@ public class RandomUtil
{
private readonly Random _random = new();
+ ///
+ /// The IEEE-754 standard for double-precision floating-point numbers limits the number of digits (including both
+ /// integer + fractional parts) to about 15–17 significant digits. 15 is a safe upper bound, so we'll use that.
+ ///
+ public const int MaxSignificantDigits = 15;
+
///
/// Generates a random integer between the specified minimum and maximum values, inclusive.
///
@@ -39,7 +45,7 @@ public class RandomUtil
}
///
- /// 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).
///
/// The minimum value of the range (inclusive).
/// The maximum value of the range (exclusive).
@@ -48,6 +54,17 @@ public class RandomUtil
{
return (float)GetSecureRandomNumber() * (max - min) + min;
}
+
+ ///
+ /// Generates a random floating-point number within the specified range ~15-17 digits (8 bytes).
+ ///
+ /// The minimum value of the range (inclusive).
+ /// The maximum value of the range (exclusive).
+ /// A random floating-point number between `min` (inclusive) and `max` (exclusive).
+ public double GetDouble(double min, double max)
+ {
+ return GetSecureRandomNumber() * (max - min) + min;
+ }
///
/// Generates a random boolean value.
@@ -110,9 +127,9 @@ public class RandomUtil
}
///
- /// Returns a random string from the provided collection of strings.
+ /// Returns a random type T from the provided collection of type T.
///
- ///
+ /// The collection to get the random element from
/// The type of elements in the collection.
/// A random element from the collection.
/// This was formerly getArrayValue() in the node server
@@ -132,6 +149,134 @@ public class RandomUtil
{
return GetCollectionValue(dictionary.Keys);
}
+
+ ///
+ /// Gets a random val from the given dictionary
+ ///
+ /// The dictionary from which to retrieve a value.
+ /// Type of key
+ /// Type of Value
+ /// A random TVal representing one of the values of the dictionary.
+ public TVal GetVal(Dictionary dictionary) where TKey : notnull
+ {
+ return GetCollectionValue(dictionary.Values);
+ }
+
+ ///
+ /// Generates a normally distributed random number using the Box-Muller transform.
+ ///
+ /// The mean (μ) of the normal distribution.
+ /// The standard deviation (σ) of the normal distribution.
+ /// The current attempt count to generate a valid number (default is 0).
+ /// A normally distributed random number.
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ ///
+ /// Generates a random integer between the specified range.
+ ///
+ /// The lower bound of the range (inclusive).
+ /// The upper bound of the range (exclusive). If not provided, the range will be from 0 to `low`.
+ /// A random integer within the specified range.
+ 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);
+ }
+
+ ///
+ /// Generates a random number between two given values with optional precision.
+ ///
+ /// The first value to determine the range.
+ /// The second value to determine the range. If not provided, 0 is used.
+ ///
+ /// 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.
+ ///
+ ///
+ 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;
+ }
///
/// 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
///
/// The number to analyze.
/// The number of decimal places, or 0 if none exist.
- 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;
}
}
\ No newline at end of file
diff --git a/UnitTests/Tests/Utils/RandomUtilTests.cs b/UnitTests/Tests/Utils/RandomUtilTests.cs
index 81a91517..be9b3041 100644
--- a/UnitTests/Tests/Utils/RandomUtilTests.cs
+++ b/UnitTests/Tests/Utils/RandomUtilTests.cs
@@ -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.");
+ }
}
}
}
\ No newline at end of file