Reputation: 750
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:
Start, Stop, Step all have __add__
, __sub__
and __truediv__
defined, are interoperable between the objects.
When combined like: (start - stop + step)//step
we get a bare number (int, float, complex, etc).
Start, Stop or Step + 1 may raise a TypeError, i.e. timedelta(days=1) + 1
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
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