Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added unit test for #319 and fixed issue #320

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Source/Bogus.Tests/GitHubIssues/Issue319.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Bogus.Extensions;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Bogus.Tests.GitHubIssues
{
public class Issue319 : SeededTest
{
ITestOutputHelper _output;

public Issue319(ITestOutputHelper output)
=> _output = output;


[Fact]
public void can_generate_decimal_edge_case()
{
Expand All @@ -33,5 +43,48 @@ public void decimal2_should_throw_on_edge_case()

a.Should().Throw<OverflowException>();
}


class TestDataProvider : DataAttribute
{
public override IEnumerable<object[]> 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 };
}
}

[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.Decimal3(min, max).Should().BeInRange(min, max);
}
catch
{
_output.WriteLine("Test failed on iteration {0}", iteration);
throw;
}
}
}


[Fact]
public void decimal3_fails_after_2_calls()
{
var r = new Randomizer();
r.Decimal3(0m, decimal.MaxValue);
r.Decimal3(0m, decimal.MaxValue);
}


}
}
49 changes: 49 additions & 0 deletions Source/Bogus.Tests/RandomizerTest.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Bogus.Extensions;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Bogus.Tests
{
Expand Down Expand Up @@ -197,6 +200,52 @@ public void generate_decimal_with_min_and_max()
r.Decimal(2.2m, 5.2m).Should().Be(4.0105668499183690m);
}

[Fact]
public void generate_maximum_range_decimal()
{
const decimal SmallestMaxPrecision = -7.9228162514264337593543950335m;
const decimal LargestMaxPrecision = 7.9228162514264337593543950335m;

for (int iteration = 0; iteration < 1000; iteration++)
{
try
{
ExtensionsForRandomizer.GenerateRandomMaximumPrecisionDecimal(r).Should().BeInRange(SmallestMaxPrecision, LargestMaxPrecision);
}
catch
{
console.WriteLine("Test failed on iteration {0}", iteration);
throw;
}
}
}

class DecimalScaleDataProvider : DataAttribute
{
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
yield return new object[] { 0m, 1m, 0.5m };
yield return new object[] { -100m, 100m, 0m };
yield return new object[] { decimal.MinValue, decimal.MaxValue, 0m };
yield return new object[] { 0m, decimal.MaxValue, decimal.MaxValue * 0.5m - 1m };
yield return new object[] { decimal.MinValue, 0m, decimal.MinValue * 0.5m + 1m };
yield return new object[] { 0m, 0m, 0m };
yield return new object[] { decimal.MinValue, decimal.MinValue, decimal.MinValue };
yield return new object[] { decimal.MaxValue, decimal.MaxValue, decimal.MaxValue };
}
}

[Theory, DecimalScaleDataProvider]
public void scale_decimal_with_min_and_max(decimal min, decimal max, decimal middle)
{
const decimal SmallestMaxPrecision = -7.9228162514264337593543950335m;
const decimal LargestMaxPrecision = 7.9228162514264337593543950335m;

ExtensionsForRandomizer.ScaleMaximumPrecisionDecimalToRange(SmallestMaxPrecision, min, max).Should().Be(min);
ExtensionsForRandomizer.ScaleMaximumPrecisionDecimalToRange(LargestMaxPrecision, min, max).Should().Be(max);
ExtensionsForRandomizer.ScaleMaximumPrecisionDecimalToRange(0m, min, max).Should().Be(middle);
}

[Fact]
public void generate_float_with_min_and_max()
{
Expand Down
141 changes: 140 additions & 1 deletion Source/Bogus/Extensions/ExtensionsForRandomizer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Bogus.Extensions
using System;

namespace Bogus.Extensions
{
public static class ExtensionsForRandomizer
{
Expand Down Expand Up @@ -41,5 +43,142 @@ public static decimal Decimal2(this Randomizer r, decimal min = 0.0m, decimal ma
// outcome by starting with the most precise scale.
return result * (max - min) / 7.9228162514264337593543950335m + min;
}


public static decimal Decimal3(this Randomizer r, decimal min = 0.0m, decimal max = 1.0m)
{
if (min > max)
{
decimal tmp = min;

min = max;
max = tmp;
}

var unscaledSample = GenerateRandomMaximumPrecisionDecimal(r);

return ScaleMaximumPrecisionDecimalToRange(unscaledSample, min, max);
}


/// <summary>
/// Generates a decimal with maximum precision with uniform distribution across the range of
/// decimal values with maximum scaling: (decimal.MinValue .. decimal.MaxValue) divided by 10^28.
/// </summary>
/// <returns></returns>
internal static decimal GenerateRandomMaximumPrecisionDecimal(Randomizer r)
{
// Decimal: 128 bits wide
// bit 0: sign bit
// bit 1-10: not used
// bit 11-15: scale (values 29, 30, 31 not used)
// bit 16-31: not used
// bit 32-127: mantissa (96 bits)

// Max value: 00000000 FFFFFFFF FFFFFFFF FFFFFFFF
// = 79228162514264337593543950335

// Max value with max scaling: 001C0000 FFFFFFFF FFFFFFFF FFFFFFFF
// = 7.9228162514264337593543950335

int lowBits = r.Number(int.MinValue, int.MaxValue);
int middleBits = r.Number(int.MinValue, int.MaxValue);
int highBits = r.Number(int.MinValue, int.MaxValue);

bool isNegative = r.Bool();

const int Scale = 28;

return new decimal(lowBits, middleBits, highBits, isNegative, Scale);
}

/// <summary>
/// Takes a decimal in the range in which decimal has maximum precision ((decimal.MinValue .. decimal.MaxValue) scaled by the maximum
/// scaling factor of 10^28) and transforms it into the caller's desired range, reducing precision only as much as needed.
/// </summary>
/// <param name="input">A decimal in the range (decimal.MinValue / SCALE .. decimal.MaxValue / SCALE), where SCALE is the maximum scaling factor (10^28).</param>
/// <param name="min">The lower bound of the target range.</param>
/// <param name="max">The upper bound of the target range.</param>
/// <returns></returns>
internal static decimal ScaleMaximumPrecisionDecimalToRange(decimal input, decimal min, decimal max)
{
const decimal SmallestMaxPrecision = -7.9228162514264337593543950335m;
const decimal LargestMaxPrecision = 7.9228162514264337593543950335m;

if (input <= SmallestMaxPrecision)
return min;
if (input >= LargestMaxPrecision)
return max;

// Step 1: 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 = 7.922816251426433759354395032m; // This value is the maximum value at maximum precision times (OneHalfScaleFactor / 0.5).

// Check condition 2:
decimal inputMagnitude = Math.Abs(input);

if (inputMagnitude >= 1.0m)
{
decimal maximumMultiplier = decimal.MaxValue / inputMagnitude;

while (multiplier >= maximumMultiplier)
{
// Drop one digit of precision and try again.
multiplier *= 0.1m;
divisor *= 0.1m;

useScaledMultiplier = true;
}
}

// Step 2: 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.
if (useScaledMultiplier)
{
decimal rangeMiddle = max * OneHalfScaleFactor + min * OneHalfScaleFactor;

return rangeMiddle + input * multiplier / divisor;
}
else
{
// If we're dealing with values at the upper extreme ends (decimal.MinValue, decimal.MaxValue),
// then half their value can't be represented in a decimal and will be subject to rounding. The
// result of that rounding is that adding the values together will produce an out-of-range value.
// Therefore, we can't use a straightforward (min + max) * 0.5, or even (min * 0.5 + max * 0.5).
decimal rangeSize = max - min;
decimal rangeMiddle;

if (min == max)
rangeMiddle = min;
else if (-min == max)
rangeMiddle = 0m;
else if (Math.Abs(min) < Math.Abs(max))
rangeMiddle = max - rangeSize * 0.5m;
else
rangeMiddle = min + rangeSize * 0.5m;

return rangeMiddle + input * rangeSize / 15.845632502852867518708790067m;
}
}

}
}