Skip to content

Instantly share code, notes, and snippets.

@AndroxxTraxxon
Last active January 21, 2019 16:21
Show Gist options
  • Save AndroxxTraxxon/eeb788811a8b481326cd5f6a8cd2e090 to your computer and use it in GitHub Desktop.
Save AndroxxTraxxon/eeb788811a8b481326cd5f6a8cd2e090 to your computer and use it in GitHub Desktop.
A fully-fleshed solution for Floating point ranges in Python
# -*- coding: utf-8 -*-
from numbers import Real
class FloatRange:
"""
step/precision used as the tolerance in __contains__ method. Defaults to 1/(10**10).
This is used to attempt to mitigate some of the floating point rounding error, but scales with the step size
so that smaller step sizes are not susceptible to catching more than they ought.
Basically, there's a 2 in <precision> chance that a number will randomly
"""
_precision = 10**10
_start, _step = 0, 1 # stop must be defined by user. No use putting it in here.
"""
Here we set up our constraints as the user passes their values:
- You can pass the values as follows:
- stop (start defaults to 0, step defaults to 1)
- start, stop (step again defaults to 1)
- start, stop, step
- start, step, and stop must be Real numbers
- The step must be non-zero
- The step must be in the same direction as the difference between stop and start
"""
def __init__(self, *args, **kwargs):
self.start = self._start
self.step = self._step
self.precision = self._precision
self.counter = 0
if len(args) == 1:
(self.stop, ) = args # FloatRange
elif len(args) == 2:
(self.start, self.stop) = args
elif len(args) == 3:
(self.start, self.stop, self.step) = args
else:
raise TypeError("FloatRange accepts 1, 2, or 3 arguments. ({0} given)".format(len(args)))
for num in self.start, self.step, self.stop:
if not isinstance(num, Real):
raise TypeError("FloatRange only accepts Real number arguments. ({0} : {1} given)".format(type(num), str(num)))
if self.step == 0:
raise ValueError("FloatRange step cannot be 0")
if (self.stop-self.start)/self.step < 0:
raise ValueError("FloatRange value must be in the same direction as the start->stop")
self.set_precision(self._precision) # x in FloatRange will return True for values within 0.1% of step size
if len(kwargs) > 0:
for key, value in kwargs.items():
if key == "precision":
if value < 0:
raise ValueError("FloatRange precision must be positive")
self.tolerance = self.step/self.precision
else:
raise ValueError("Unknown kwargs key: {0}".format(key))
"""
Returns the next value in the iterator, or it stops the iteration and resets.
"""
def __next__(self):
output = self.start + (self.counter * self.step)
if ((self.step > 0 and output >= self.stop) or
(self.step < 0 and output <= self.stop)) :
self.counter = 0
raise StopIteration
self.counter += 1
return output
"""
The class already implements __next__(), so it is its own iterable
"""
def __iter__(self):
return self
def set_precision(self, precision=None):
if precision is None:
self.precision = self._precision
elif isinstance(precision, Real):
if precision < 0:
raise ValueError("FloatRange precision cannot be a negative number.")
self.precision = precision
else:
raise ValueError("FloatRange precision must be a Real number.")
self.tolerance = abs(self.step/self.precision)
"""
len(my_FloatRange)
"""
def __len__(self):
# we have to do this minute addition here so that floating point rounding does not fail.
return int((self.stop - self.start + self.step/10**13) / self.step)
"""
x in my_FloatRange
Evaluates whether a given number is contained in the range, in constant time.
Non-exact values will return True if they are within the provided tolerance.
Use set_precision(precision) to define the precision:step ratio (the tolerance)
"""
def __contains__(self, item):
diff = (item - self.start) % self.step
# if we're dealing with exact cases (not recommended, but okay.)
if (self.step > 0 and
item >= self.start-self.tolerance and
item < self.stop):
return (min(diff, self.step-diff) < self.tolerance)
elif (self.step < 0 and
item <= self.start+self.tolerance and
item > self.stop
):
return (min(abs(diff), abs(self.step-diff)) < self.tolerance)
return False
def __str__(self):
return self.__repr__()
def __repr__(self):
ext = ""
if not self.step == 1:
ext += ", {0}".format(self.step)
if self.precision != self._precision:
ext += ", precision={0}, tolerance={1}".format(
self.precision, self.tolerance
)
return "FloatRange({0}, {1}{2})".format(
self.start,
self.stop,
ext
)
# -*- coding: utf-8 -*-
import unittest
from FloatRange import FloatRange
class TestCase_FloatRange(unittest.TestCase):
def test_compare_basic(self, start=None, stop=1, step=None, verbose=False):
my_range = None
my_FloatRange = None
if step is None:
if start is None:
my_range = range(stop)
my_FloatRange = FloatRange(stop)
else:
my_range = range(start, stop)
my_FloatRange = FloatRange(start, stop)
else:
my_range = range(start, stop, step)
my_FloatRange = FloatRange(start, stop, step)
if verbose:
print("Validating:[{0}] == [{1}]".format(
my_range, my_FloatRange))
for x,y in zip(my_range, my_FloatRange):
try:
self.assertEqual(x,y)
except:
print("{0} and {1} failed to produce the same values.".format(
my_range, my_FloatRange
))
raise
def test_compare_range_functionality(self):
_length = 10 # arbitrary number for adequate length
_step = 2
_start = 5
self.test_compare_basic(stop = _length)
self.test_compare_basic(start =_start,
stop = _length)
self.test_compare_basic(start=_start,
stop= _start+_length)
self.test_compare_basic(start=_start,
stop= _start+_length*_step,
step= _step)
def test_correct_length(self):
for _divisor in range(1, 100):
for _step_base in range(1, 100):
for _length in range(1, 100):
_step = _step_base / _divisor
_start = 1 / _divisor + 1
_stop = _start + _length*_step
my_FloatRange = FloatRange(_start,
_stop,
_step)
try:
self.assertEqual(len(my_FloatRange), _length)
except Exception:
print("Length test failed with parameters:\n\tstart:{0}\n\tstop :{1}\n\tstep: {2}\n\tvalue: {2}".format(
_start, _stop, _step, len(my_FloatRange)
))
raise
def test_value_set(self, subject=FloatRange(1), values=[0], verbose=False):
if verbose:
print("Validating {0} produces {1}".format(subject, values))
try:
self.assertEqual(len(subject), len(values))
except:
print("{0} and {1} do not have the same length!".format(subject, values))
raise
for f, v in zip(subject, values):
try:
self.assertAlmostEqual(f, v) # floating point rounding doesn't allow for exact equality.
except:
print("{0} does not produce {1}".format(subject, values))
raise
def test_values(self):
self.test_value_set(FloatRange(0, 10, 1/3), [(x/3) for x in range(30)])
self.test_value_set(FloatRange(5, 15, 1/3), [(5+(x/3)) for x in range(30)])
self.test_value_set(FloatRange(1, 11, 1/7), [(1+(x/7)) for x in range(70)])
self.test_value_set(FloatRange(8, 18, 1/7), [(8+(x/7)) for x in range(70)])
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment