Wednesday, September 2, 2009

Potential Cool Use (Extension?) For Range

While working on the tests for my next piece of code to talk about on this blog, I ran into one of those perennial nuisances of numeric programming. It is very common to need a list of numbers from A to B (inclusive) with either N numbers or split into S segments. So perhaps I want ten numbers from 1.0 to 2.0; then the numbers are 1.0, 1.11111, 1.22222, 1.33333, 1.44444, 1.55555, 1.66666, 1.77777, 1.88888, 2.0. There are two issues here. The first is the potential for fencepost error: if I think ten numbers from 1.0 to 2.0, my first thought is always that they go up by 0.1, when they actually go up by 1/9th. The second issue is that if you do the obvious loop ($x = 1.0; $x <= 2.0; $x += (1/9)), it's a crapshoot whether you get the "2.0" or not, and even if you do, it might actually be 1.99998 or something like that. Usually in these cases getting the exact ending value is highly desirable (as it is an edge case).

(Before getting into potential solutions for this, let me note that this particular example is already solvable in the latest Rakudo builds using Rats. loop ($x = 1; $x <= 2; $x += (1/9)) should give the exact list 1, 10/9, 11/9, 12/9, ... 17/9, 2. But that doesn't help with the fencepost, and the general problem is very ugly to solve this way!)

My first thought was that Range with :by might do the trick. Unfortunately, I don't see Perl 6 specs for how :by actually works in this sort of case, and as far as I know, it's not actually implemented yet to test. At any rate, I'd like to suggest that :by does actually handle this case, if possible (ie if an element of the range is within epsilon of the to value, then the to value is returned instead). And I'd like to suggest two more modifiers, something like :size(N) to specify that the Range should return N values, with the :by value calculated as appropriate, and :sections(N) to specify that the Range should return N+1 values.

In the meantime, here's a quick implementation of what I think the Range :size function should do. I believe that it is properly set up to be lazy, while laziness is available. Note that if you use integer parameters, it will return Rats!


  1. I would have done it differently personally. I'd do something like, (1..9).map { $_* 1/9 + 1 }. That just seems to be a better soln.

  2. Hmmm... as you've written it, I don't know, but if you swap it around a little, you get (0..9).map({($_ / 9) * ($b - $a) + $a }). That's almost perfect, because the Rat version of 9 / 9 will be exactly equal to 1. But even then, the last number will be ($b - $a) + $a, and if those are floating point numbers, it is not guaranteed to be exactly equal to $a.