From 20e6a408d668d46c8dd7a294a462c6935e087c4d Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Fri, 14 Aug 2020 11:13:43 -0500 Subject: [PATCH] Added unit test class Issue319 for #319 with test cases decimal_with_very_large_range_succeeds to Bogus.Tests. Updated Randomizer.Decimal to make the test pass. --- Source/Bogus.Tests/GitHubIssues/Issue319.cs | 48 +++++++++++++++++++ Source/Bogus/Randomizer.cs | 52 ++++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 Source/Bogus.Tests/GitHubIssues/Issue319.cs diff --git a/Source/Bogus.Tests/GitHubIssues/Issue319.cs b/Source/Bogus.Tests/GitHubIssues/Issue319.cs new file mode 100644 index 00000000..e502522e --- /dev/null +++ b/Source/Bogus.Tests/GitHubIssues/Issue319.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Reflection; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Bogus.Tests.GitHubIssues +{ + public class Issue319 : SeededTest + { + class TestDataProvider : DataAttribute + { + public override IEnumerable GetData(MethodInfo testMethod) + { + yield return new object[] { 0m, decimal.MaxValue }; + yield return new object[] { decimal.MinValue, decimal.MaxValue }; + yield return new object[] { decimal.MinValue, 0m }; + // A range whose size exceeds decimal.MaxValue but which doesn't have decimal.MinValue or decimal.MaxValue as a bound. + yield return new object[] { decimal.MinValue * 0.6m, decimal.MaxValue * 0.6m }; + } + } + + ITestOutputHelper _output; + + public Issue319(ITestOutputHelper output) + => _output = output; + + [Theory, TestDataProvider] + public void decimal_with_very_large_range_succeeds(decimal min, decimal max) + { + var randomizer = new Randomizer(); + + for (int iteration = 0; iteration < 300; iteration++) + { + try + { + randomizer.Decimal(min, max).Should().BeInRange(min, max); + } + catch + { + _output.WriteLine("Test failed on iteration {0}", iteration); + throw; + } + } + } + } +} diff --git a/Source/Bogus/Randomizer.cs b/Source/Bogus/Randomizer.cs index dc85796e..66ca43b2 100644 --- a/Source/Bogus/Randomizer.cs +++ b/Source/Bogus/Randomizer.cs @@ -194,6 +194,14 @@ public double Double(double min = 0.0d, double max = 1.0d) /// Maximum, default 1.0 public decimal Decimal(decimal min = 0.0m, decimal max = 1.0m) { + if (min > max) + { + decimal tmp = min; + + min = max; + max = tmp; + } + // Decimal: 128 bits wide // bit 0: sign bit // bit 1-10: not used @@ -221,10 +229,50 @@ public decimal Decimal(decimal min = 0.0m, decimal max = 1.0m) decimal result = new decimal(lowBits, middleBits, highBits, isNegative: false, Scale); - // Step 2: Scale the value and adjust it to the desired range. This may decrease + // Step 2: Figure out how much of the scale we can keep without causing an overflow. + // Note that the range can actually exceed decimal.MaxValue, e.g. if max is itself + // decimal.MaxValue and min is negative. So, we work with half the range, using this + // scale factor that is as close to 0.5 as possible without causing the result of + // decimal.MaxValue * ScaleFactor to round up. If it rounds up, then the result of + // decimal.MaxValue * ScaleFactor - decimal.MinValue * ScaleFactor will still be + // larger than decimal.MaxValue. + const decimal OneHalfScaleFactor = 0.4999999999999999999999999999m; + + decimal halfRange = max * OneHalfScaleFactor - min * OneHalfScaleFactor; + + // Two reasons we're forced to use a scaled multiplier: + // + // 1. The range (max - min) is itself too large to store in decimal.MaxValue. + // 2. The result of result * (max - min) is too large to store in decimal.MaxValue. + // + // Check condition 1: + bool useScaledMultiplier = (halfRange >= decimal.MaxValue * OneHalfScaleFactor); + + decimal multiplier = halfRange; + decimal divisor = 3.9614081257132168796771975168m; + + // Check condition 2: + if (result >= 1.0m) + { + decimal maximumMultiplier = decimal.MaxValue / result; + + while (multiplier >= maximumMultiplier) + { + // Drop one digit of precision and try again. + multiplier *= 0.1m; + divisor *= 10m; + + useScaledMultiplier = true; + } + } + + // Step 3: Scale the value and adjust it to the desired range. This may decrease // the accuracy by adjusting the scale as necessary, but we get the best possible // outcome by starting with the most precise scale. - return result * (max - min) / 7.9228162514264337593543950335m + min; + if (useScaledMultiplier) + return result * multiplier / divisor + min; + else + return result * (max - min) / 7.9228162514264337593543950335m + min; } ///