[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>


More information about the Python-ideas mailing list