justanr
justanr

Reputation: 750

Calculating length of a generalized range

I started writing a class called grange that serves as a generalized range -- create a range of dates, etc.

Calculating the length of a range of integers is easy: (stop - start)//step:

len(range(0, 8, 2)) -> (8 - 0)//2 -> (8)//2 -> 4

Of course, dealing with negative steps and start > stop is a bit trickier, but the general idea holds.

But when I moved from the concrete realm of integers to the abstract realm of "things" my mind started to melt.

Let's assume a few things:

Here's what I currently have:

def __len__(self):
    if self.stop is None:
        # 10/10 wish float('inf') was returnable
        raise TypeError("Infinite Range!")
    try:
        # factor in self.step to compensate to include start position
        if self._has_neg_step:
            calc = self.start - self.stop - self.step
        else:
            calc = self.stop - self.start + self.step
        # counteract float and negative steps
        return int(abs(calc//self.step))
    except (TypeError, ValueError):
        # throw a hail mary!
        # seems dangerous but should be *alright* since
        # bail out on infinite sequence case
        return len(list(iter(self)))

This works great for dates!

start, stop = datetime(2015, 3, 1), datetime(2015, 3, 8)
step = timedelta(days=2)
assert len(grange(start, stop, step)) == 4

But with integers...

assert len(grange(0,8,1)) == 8, len(grange(0, 8, 1))
AssertationError: 9

Originally, I had considered coming up with an abstraction over one:

self._one = step//step

So I could just do:

(self.stop - self.start - self.step + self.one)//self.step

A thing divided by itself is one! Thank you elementary school! But there's an issue: timedelta(days=2)//timedelta(days=2) == 1 not timedelta(days=1)

Turns out I forgot the part where x unit/x unit also cancels out the unit part, too. At least for timedelta. Who knows what other objects will do?

I'm not convinced that with the implementation that a generalized algorithm is possible (or maybe even at all!) but bailing straight to: len(list(iter(self))) seems wasteful but that also seems like the only foolproof solution.

It seems like a compromise would be to calculate the length once using len(list(iter(self))) and stash it in a property:

# works because Python will bypass __getattribute__ when len is called.
@property
def __len__(self):
    if self.stop is None:
        raise TypeError('Infinite range')
    if not hasattr(self, '__len__'):
        self.__len__ = len(list(iter(self))
    return self.__len__

This removes many of the assumptions about the passed objects.

Any suggestions?

Upvotes: 2

Views: 174

Answers (1)

Soronbe
Soronbe

Reputation: 926

The issue has nothing to do with the types: in len(grange(0,8,1)) the 8 is counted for the length as well (so 0,1,2,3,4,5,6,7,8 is 9 numbers). This isn't occurring for your dates example because you can't get to 8 starting from 1 with a step of 2.

So if calc is a multiple (multiplicative? English is not my native tongue) of step, (normal div, not truediv) subtract length by 1.

TL;DR your len acts as if the grange includes the stop, but you expect it to exclude the stop giving unexpected results.

Upvotes: 2

Related Questions