I have been using range with the syntax of r2 and r4 for a while now and encountered this issue just today. I noticed that for this specific value of y and this number of points, range behaves strangely. The (start:step:stop) syntax does not create the last point in the range, stop. The issue is easier to see if you compare:
collect(r1)
collect(r2)
Where collect(r2) creates a 20, not a 21-element Vector{Float64}.
I will probably simply switch how I am declaring StepRangeLen objects from now on, but does anybody have an explanation as to what is happening here?
You’ve created a range where due to floating point precision in the step, it will not nicely end on 3y, thus skipping it.
Adding the floating point precision back to stop (even though eps(y)*npts is only 1.1657341758564144e-15 will demonstrate this: r2 = -3y:6y/(npts-1):3y+eps(y)*npts.
If you want to have a fixed length, it’s best to use the range constructor.
edit: 7(eps(y)) is already enough here to make it to 21
Ranges are tricky beasts, especially when it comes to floating point numbers. There are four key properties of a range (start, stop, number of points, and step size), but no matter how you cut it, one of those is fully dependent upon the other three.
So that’s fundamentally why there are multiple ways of specifying the range — and why you might get different results depending upon which way you go.
The start:step:stop formulation tries to fit as many steps as possible between start and stop. And the stop that you get back might be slightly less than the value you specified. But it’ll never be more. Depending upon the exact floating point values, it can sometimes be surprising that the stop you specified is a little short of where you expected the computed step to hit.
Just as a little tidbit, Julia does try pretty hard to do make start:step:stop hit the stop you specified with the step you meant, but only if “what you meant” could possibly round to the exact value you specified. For example, by the literally exact floating point values specified in the range 0.0:0.1:0.3 there should only be three values (0.0, 0.1, and 0.2). The exact value specified by 0.1 isn’t \frac{1}{10} but slightly more than that, and the exact value specified by 0.3 isn’t \frac{3}{10} but it’s slightly less… so the naive thing should miss 0.3. But there is a simple rational that rounds to 0.1 that can make it work, so Julia uses that instead! That’s what’s happening with the higher precision stuff @Benny is peeking at.
This whole “do what you might have meant” thing only works, however, if what you meant rounds exactly to the value you passed (and isn’t closer to any other floating point values). Doing more than one arithmetic computation in computing your step and/or endpoints yourself is a very easy way to get away from an exact round. Simplify your computations — specifically the “computation distance” between the values you use in the range — and things more often work as you expect:
Even better, of course, is to just specify the length directly with range if that’s really what you mean, but hopefully this helps you understand why a bit more.