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

Fuzzy decimal using decimals #849

Open
wants to merge 4 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
65 changes: 56 additions & 9 deletions docs/fuzzy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,32 +132,79 @@ FuzzyDecimal
The :class:`FuzzyDecimal` fuzzer generates random :class:`decimals <decimal.Decimal>` within a given
inclusive range.

.. note::
Providing decimals as argument will result in a :exc:`TypeError`. Use :class:`~factory.fuzzy.FuzzyDecimalDec`
instead.

The :attr:`low` bound may be omitted, in which case it defaults to 0:

.. code-block:: pycon

>>> FuzzyDecimal(0.5, 42.7)
>>> fi.low, fi.high
>>> fd = FuzzyDecimal(0.5, 42.7)
>>> fd.low, fd.high
0.5, 42.7

>>> fi = FuzzyDecimal(42.7)
>>> fi.low, fi.high
>>> fd = FuzzyDecimal(42.7)
>>> fd.low, fd.high
0.0, 42.7

>>> fi = FuzzyDecimal(0.5, 42.7, 3)
>>> fi.low, fi.high, fi.precision
>>> fd = FuzzyDecimal(0.5, 42.7, 3)
>>> fd.low, fd.high, fd.precision
0.5, 42.7, 3

.. attribute:: low

decimal, the inclusive lower bound of generated decimals
float or int, the inclusive lower bound of generated decimals

.. attribute:: high

decimal, the inclusive higher bound of generated decimals
float or int, the inclusive higher bound of generated decimals

.. attribute:: precision
int, the number of digits to generate after the dot. The default is 2 digits.

int, the number of digits after the decimal separator. The default is 2.


FuzzyDecimalDec
---------------

.. class:: FuzzyDecimalDec(low[, high[, precision=2]])

The :class:`FuzzyDecimalDec` fuzzer generates random :class:`decimals <decimal.Decimal>` within a given
inclusive range, accepting any argument that the constructor of the :class:`decimal <decimal.Decimal>`
accepts.

.. note::
One purpose of this class is accuracy. Providing inaccurate arguments (:class:`floats <float>`), will result in
inaccurate bounds. In that case it's better to use :class:`~factory.fuzzy.FuzzyDecimal` instead.

The :attr:`low` bound may be omitted, in which case it defaults to 0:

.. code-block:: pycon

>>> fd = FuzzyDecimalDec('0.5', Decimal('42.7'))
>>> fd.low, fd.high
Decimal('0.5'), Decimal('42.7')

>>> fd = FuzzyDecimalDec(42.7)
>>> fd.low, fd.high
Decimal('0'), Decimal('42.7000000000000028421709430404007434844970703125')

>>> fd = FuzzyDecimalDec('0.5', '42.7', 3)
>>> fd.low, fd.high, fd.precision
Decimal('0.5'), Decimal('42.7'), 3

.. attribute:: low

string, Decimal or int, the inclusive lower bound of generated decimals

.. attribute:: high

string, Decimal or int, the inclusive higher bound of generated decimals

.. attribute:: precision

int, the number of digits after the decimal separator. The default is 2.


FuzzyFloat
Expand Down
18 changes: 18 additions & 0 deletions factory/fuzzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,24 @@ def fuzz(self):
return base.quantize(decimal.Decimal(10) ** -self.precision)


class FuzzyDecimalDec(BaseFuzzyAttribute):
"""Random decimal within a given range, accepting decimal-like arguments."""
def __init__(self, low, high=None, precision=2):
if high is None:
high = low
low = decimal.Decimal('0')

self.low = decimal.Decimal(low)
self.high = decimal.Decimal(high)
self.precision = precision

super().__init__()

def fuzz(self):
base = random.uniform_decimal(self.low, self.high)
return base.quantize(decimal.Decimal(10) ** -self.precision)


class FuzzyFloat(BaseFuzzyAttribute):
"""Random float within a given range."""

Expand Down
6 changes: 6 additions & 0 deletions factory/random.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import decimal
import random

import faker.generator
Expand Down Expand Up @@ -28,3 +29,8 @@ def reseed_random(seed):
r = random.Random(seed)
random_internal_state = r.getstate()
set_random_state(random_internal_state)


def uniform_decimal(low, high):
modifier = decimal.Decimal(randgen.random())
return low + (high - low) * modifier
88 changes: 88 additions & 0 deletions tests/test_fuzzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,94 @@ def test_no_approximation(self):
finally:
decimal_context.traps[decimal.FloatOperation] = old_traps

def test_decimal_args(self):
low = decimal.Decimal('5.5')
high = decimal.Decimal('10')
fuzz = fuzzy.FuzzyDecimal(low, high)

with self.assertRaises(TypeError):
utils.evaluate_declaration(fuzz)


class FuzzyDecimalDecTestCase(unittest.TestCase):
def test_definition(self):
"""Tests all ways of defining a FuzzyDecimal."""
fuzz = fuzzy.FuzzyDecimalDec('2.0', decimal.Decimal('3.0'))
for _i in range(20):
res = utils.evaluate_declaration(fuzz)
self.assertTrue(
decimal.Decimal('2.0') <= res <= decimal.Decimal('3.0'),
"value %d is not between 2.0 and 3.0" % res,
)

fuzz = fuzzy.FuzzyDecimalDec(4.0)
for _i in range(20):
res = utils.evaluate_declaration(fuzz)
self.assertTrue(
decimal.Decimal('0.0') <= res <= decimal.Decimal('4.0'),
"value %d is not between 0.0 and 4.0" % res,
)

fuzz = fuzzy.FuzzyDecimalDec(1.0, '4.0', precision=5)
for _i in range(20):
res = utils.evaluate_declaration(fuzz)
self.assertTrue(
decimal.Decimal('1.0') <= res <= decimal.Decimal('4.0'),
"value %d is not between 1.0 and 4.0" % res,
)
self.assertTrue(res.as_tuple().exponent, -5)

def test_biased(self):
fake_uniform = lambda low, high: low + high

fuzz = fuzzy.FuzzyDecimalDec(decimal.Decimal('2.0'), decimal.Decimal('8.0'))

with mock.patch('factory.random.uniform_decimal', fake_uniform):
res = utils.evaluate_declaration(fuzz)

self.assertEqual(decimal.Decimal('10.0'), res)
fuzz = fuzzy.FuzzyDecimalDec('2.0', '8.0')

with mock.patch('factory.random.uniform_decimal', fake_uniform):
res = utils.evaluate_declaration(fuzz)

self.assertEqual(decimal.Decimal('10.0'), res)

fuzz = fuzzy.FuzzyDecimalDec(3.33, 20 / 3, precision=2)

with mock.patch('factory.random.uniform_decimal', fake_uniform):
res = utils.evaluate_declaration(fuzz)

self.assertEqual(decimal.Decimal('10.00'), res)

def test_biased_high_only(self):
fake_uniform = lambda low, high: low + high

fuzz = fuzzy.FuzzyDecimalDec('8.0')

with mock.patch('factory.random.uniform_decimal', fake_uniform):
res = utils.evaluate_declaration(fuzz)

self.assertEqual(decimal.Decimal('8.0'), res)

def test_precision(self):
fake_uniform = lambda low, high: low + high + decimal.Decimal('0.001')

fuzz = fuzzy.FuzzyDecimalDec('8.0', precision=3)

with mock.patch('factory.random.uniform_decimal', fake_uniform):
res = utils.evaluate_declaration(fuzz)

self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res)

def test_decimal_args(self):
low = decimal.Decimal('5.5')
high = decimal.Decimal('10')
fuzz = fuzzy.FuzzyDecimalDec(low, high)

res = utils.evaluate_declaration(fuzz)
self.assertTrue(low <= res <= high, "value %d is not between 5.5 and 10" % res)


class FuzzyFloatTestCase(unittest.TestCase):
def test_definition(self):
Expand Down