From d71d0ab0f94fef0c19de34a0c0cb4cc28fb5e448 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Tue, 20 Aug 2024 12:10:14 +0900 Subject: [PATCH] Fix: NextFloat/Double could return a value equal to max due to floating point error --- src/RandomExtensions/RandomEx.Next.cs | 29 +++++++++++++- src/RandomExtensions/Shims/MathEx.cs | 57 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/RandomExtensions/Shims/MathEx.cs diff --git a/src/RandomExtensions/RandomEx.Next.cs b/src/RandomExtensions/RandomEx.Next.cs index 4e1a68b..413502d 100644 --- a/src/RandomExtensions/RandomEx.Next.cs +++ b/src/RandomExtensions/RandomEx.Next.cs @@ -177,7 +177,19 @@ public static float NextFloat(this IRandom random, float max) public static float NextFloat(this IRandom random, float min, float max) { ThrowHelper.CheckMinMax(min, max); - return NextFloat(random) * (max - min) + min; + + var r = NextFloat(random) * (max - min) + min; + + if (r >= max) + { +#if !NET6_0_OR_GREATER + r = MathEx.BitDecrement(max); +#else + r = MathF.BitDecrement(max); +#endif + } + + return r; } /// @@ -206,7 +218,20 @@ public static double NextDouble(this IRandom random, double max) public static double NextDouble(this IRandom random, double min, double max) { ThrowHelper.CheckMinMax(min, max); - return NextDouble(random) * (max - min) + min; + + var r = NextDouble(random) * (max - min) + min; + + // correct for rounding + if (r >= max) + { +#if !NET6_0_OR_GREATER + r = MathEx.BitDecrement(max); +#else + r = Math.BitDecrement(max); +#endif + } + + return r; } /// diff --git a/src/RandomExtensions/Shims/MathEx.cs b/src/RandomExtensions/Shims/MathEx.cs new file mode 100644 index 0000000..574b309 --- /dev/null +++ b/src/RandomExtensions/Shims/MathEx.cs @@ -0,0 +1,57 @@ +namespace RandomExtensions; + +internal static class MathEx +{ +#if !NET6_0_OR_GREATER + public static double BitDecrement(double x) + { + long bits = BitConverter.DoubleToInt64Bits(x); + + if (((bits >> 32) & 0x7FF00000) >= 0x7FF00000) + { + // NaN returns NaN + // -Infinity returns -Infinity + // +Infinity returns double.MaxValue + return (bits == 0x7FF00000_00000000) ? double.MaxValue : x; + } + + if (bits == 0x00000000_00000000) + { + // +0.0 returns -double.Epsilon + return -double.Epsilon; + } + + // Negative values need to be incremented + // Positive values need to be decremented + + bits += ((bits < 0) ? +1 : -1); + return BitConverter.Int64BitsToDouble(bits); + } + + public static float BitDecrement(float x) + { + int bits = BitConverter.SingleToInt32Bits(x); + + if ((bits & 0x7F800000) >= 0x7F800000) + { + // NaN returns NaN + // -Infinity returns -Infinity + // +Infinity returns float.MaxValue + return (bits == 0x7F800000) ? float.MaxValue : x; + } + + if (bits == 0x00000000) + { + // +0.0 returns -float.Epsilon + return -float.Epsilon; + } + + // Negative values need to be incremented + // Positive values need to be decremented + + bits += ((bits < 0) ? +1 : -1); + return BitConverter.Int32BitsToSingle(bits); + } + +#endif +} \ No newline at end of file