# [Python-ideas] Float range class

Ethan Furman ethan at stoneleaf.us
Fri Jan 9 04:19:23 CET 2015

```On 01/08/2015 08:01 AM, Todd wrote:
>
> Currently, the range class is limited to integers.  However, in many applications, especially science and mathematics,
> ranges of float values are the norm.

Here is a proposed range replacement -- it works with int and float directly, and will attempt to work with any other
type for which sufficient information can be gleaned from the provided parameters:

- range(Date(2015, 1, 1), step=ONE_DAY, count=31) # step must be specified; gives every day in January 2015

- range(Fraction(5, 10), count=3) # step is deduced as 10/10 (aka 1)

- range(100.0) # 0.0, 1.0, 2.0, ..., 99.0

For more flexibility, an epsilon keyword argument is available, and step can be a function:

range([start,] stop [,step] [,count] [,epsilon])

If step is a function, on each iteration it is called with start_value, current_iteration, last_value.

epsilon is used to check for the stop point, and for containment checks.

Containment is only supported when the iteration type and step value support addition, subtraction, and division, and
the step cannot be a function (in these cases, use list() to get a concrete sequence).

This particular version works from 2.6+ and 3.2+ (3.1 should work if you add a 'callable' function), which is why it's
called xrange in the code.

--- 8< xrange.py --------------------------------------------------------------
from decimal import Decimal

class xrange(object):
'''
accepts arbitrary objects to use to produce sequences
'''

def __init__(self, start, stop=None, step=None, count=None, epsilon=None):
if stop is not None and count is not None:
raise ValueError("cannot specify both stop and count")
if stop is None and count is None:
# check for default start based on type
start, stop = None, start
if isinstance(stop, (int, float, Decimal, complex)):
start = type(stop)(0)
else:
raise ValueError("start must be specified for type %r" %
type(stop))
if start is None:
ref = type(stop)
else:
ref = type(start)
if step is None:
try:
step = ref(1)
except TypeError:
raise ValueError("step must be specified for type %r" %
type(stop))
if not callable(step):
ref = type(step)
if epsilon is None:
if isinstance(start, (float, Decimal, complex)):
try:
epsilon = .0000001 * step
except TypeError:
epsilon = ref(0)
else:
epsilon = ref(0)
if count is None:
try:
stop = stop - epsilon
except TypeError:
pass
self.start = self.init_start = start
self.stop = stop
self.step = step
self.count = self.init_count = count
self.epsilon = epsilon

def __contains__(self, value):
start, stop, step = self.start, self.stop, self.step
count, epsilon = self.count, self.epsilon
if callable(step):
raise TypeError(
"range with step %r does not support containment checks" %
step)
try:
distance = round((value - start) / step)
except TypeError:
raise TypeError(
"range of %s with step %s does not support "
"containment checks" % (type(start), type(step)))
if start - epsilon <= value <= start + epsilon:
return True
if step > type(step)(0):
if start + epsilon > value:
return False
elif stop is not None and stop - epsilon <= value:
return False
elif count is not None and distance >= count:
return False
else:
if start + epsilon < value:
return False
elif stop is not None and stop - epsilon >= value:
return False
elif count is not None and distance >= count:
return False
target = start + distance * step
if target - epsilon <= value <= target + epsilon:
return True
return False

def __iter__(self):
start = self.start
stop = self.stop
step = self.step
count = self.count
epsilon = self.epsilon
i = -1
while 'more values to yield':
i += 1
if callable(step):
if i:
value = step(start, i, value)
else:
value = start
else:
value = start + i * step
if count is not None:
if count < 1:
break
count -= 1
else:
if stop > start and value >= stop:
break
if stop < start and value <= stop:
break
yield value

def __repr__(self):
values = [
'%s=%s' % (k,v)
for k,v in (
('start',self.start),
('stop',self.stop),
('step', self.step),
('count', self.count),
('epsilon', self.epsilon),
)
if v is not None
]
return '<%s(%s)>' % (self.__class__.__name__, ', '.join(values))
--- 8< xrange.py --------------------------------------------------------------

--- 8< test_xrange.py ---------------------------------------------------------
from unittest import TestCase, main
from xrange import xrange
import datetime

class Test_xrange(TestCase):

def test_int_iter_forwards(self):
self.assertEqual(
list(range(10)),
list(xrange(10)))
self.assertEqual(
list(range(0, 10)),
list(xrange(0, 10)))
self.assertEqual(
list(range(0, 10, 1)),
list(xrange(0, 10, 1)))
self.assertEqual(
list(range(0, 10, 1)),
list(xrange(0, count=10)))
self.assertEqual(
list(range(0, 10, 1)),
list(xrange(10, step=lambda s, i, v: v+1)))
self.assertEqual(
list(range(0, 10, 1)),
list(xrange(0, 10, step=lambda s, i, v: v+1)))
self.assertEqual(
list(range(5, 15)),
list(xrange(5, count=10)))
self.assertEqual(
list(range(-10, 0)),
list(xrange(-10, 0)))
self.assertEqual(
list(range(-9, 1)),
list(xrange(-9, 1)))
self.assertEqual(
list(range(-20, 20, 1)),
list(xrange(-20, 20, 1)))
self.assertEqual(
list(range(-20, 20, 2)),
list(xrange(-20, 20, 2)))
self.assertEqual(
list(range(-20, 20, 3)),
list(xrange(-20, 20, 3)))
self.assertEqual(
list(range(-20, 20, 4)),
list(xrange(-20, 20, 4)))
self.assertEqual(
list(range(-20, 20, 5)),
list(xrange(-20, 20, 5)))

def test_int_iter_backwards(self):
self.assertEqual(
list(range(9, -1, -1)),
list(xrange(9, -1, -1)))
self.assertEqual(
list(range(9, -9, -1)),
list(xrange(9, -9, -1)))
self.assertEqual(
list(range(9, -9, -2)),
list(xrange(9, -9, -2)))
self.assertEqual(
list(range(9, -9, -3)),
list(xrange(9, -9, -3)))
self.assertEqual(
list(range(9, 0, -1)),
list(xrange(9, 0, -1)))
self.assertEqual(
list(range(9, -1, -1)),
list(xrange(9, step=-1, count=10)))

def test_int_containment(self):
robj = xrange(10)
for i in range(10):
self.assertTrue(i in robj)
self.assertFalse(-1 in robj)
self.assertFalse(10 in robj)
self.assertFalse(5.23 in robj)

def test_float_iter(self):
floats = [float(i) for i in range(100)]
self.assertEqual(
floats,
list(xrange(100.0)))
self.assertEqual(
floats,
list(xrange(0, 100.0)))
self.assertEqual(
floats,
list(xrange(0, 100.0, 1.0)))
self.assertEqual(
floats,
list(xrange(100.0, step=lambda s, i, v: v + 1.0)))
self.assertEqual(
floats,
list(xrange(100.0, step=lambda s, i, v: s + i * 1.0)))
self.assertEqual(
floats,
list(xrange(0.0, count=100)))
self.assertEqual(
[0.3, 0.6],
list(xrange(0.3, 0.9, 0.3)))
self.assertEqual(
[0.4, 0.8],
list(xrange(0.4, 1.2, 0.4)))

def test_float_iter_backwards(self):
floats = [float(i) for i in range(99, -1, -1)]
self.assertEqual(
floats,
list(xrange(99, -1, -1)))
self.assertEqual(
floats,
list(xrange(99, step=lambda s, i, v: v - 1.0, count=100)))
self.assertEqual(
[0.6, 0.3],
list(xrange(0.6, 0.0, -0.3)))
self.assertEqual(
[0.8, 0.4]
, list(xrange(0.8, 0.0, -0.4)))

def test_float_containment(self):
robj = xrange(100000000.0)
for i in [float(i) for i in range(10000)]:
self.assertTrue(i in robj)
self.assertFalse(0.000001 in robj)
self.assertFalse(100000000.0 in robj)
self.assertFalse(50.23 in robj)

def test_date_iter(self):
ONE_DAY = datetime.timedelta(1)
ONE_WEEK = datetime.timedelta(7)
robj = xrange(datetime.date(2014, 1, 1), step=ONE_DAY, count=31)
day1 = datetime.date(2014, 1, 1)
riter = iter(robj)
try:
datetime.timedelta(7) / datetime.timedelta(1)
containment = True
except TypeError:
containment = False
for i in range(31):
day = day1 + i * ONE_DAY
rday = next(riter)
self.assertEqual(day, rday)
if containment:
self.assertTrue(day in robj)
else:
self.assertRaises(TypeError, robj.__contains__, day)
self.assertRaises(StopIteration, next, riter)
if containment:
self.assertFalse(day + ONE_DAY in robj)
else:
self.assertRaises(TypeError, robj.__contains__, day + ONE_DAY)

def test_fraction_iter(self):
from fractions import Fraction as F
f = xrange(F(5, 10), count=3)
self.assertEqual([F(5, 10), F(15, 10), F(25, 10)], list(f))

if __name__ == '__main__':
main()
--- 8< test_xrange.py ---------------------------------------------------------

--
~Ethan~

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 836 bytes
Desc: OpenPGP digital signature
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20150108/82f92384/attachment-0001.sig>
```