tag:www.rhnh.net,2008:/range
Range - Xavier Shay's Blog
2009-08-03T08:11:41Z
Enki
Xavier Shay
notreal@rhnh.net
tag:www.rhnh.net,2008:Post/798
2009-08-03T08:18:00Z
2009-08-03T08:11:41Z
Range#include? in ruby 1.9
<p><code>Range#include?</code> behaviour has changed in ruby 1.9 for non-numeric ranges. Rather than a greater-than/less-than check against the min and max values, the range is iterated over from min until the test value is found (or max). This is necessary to cover some edge cases of ranges which are incorrect in 1.8.7, as demonstrated by the following example:</p><table class="CodeRay"><tr>
<td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>4<tt>
</tt>5<tt>
</tt>6<tt>
</tt>7<tt>
</tt>8<tt>
</tt>9<tt>
</tt><strong>10</strong><tt>
</tt>11<tt>
</tt>12<tt>
</tt>13<tt>
</tt>14<tt>
</tt>15<tt>
</tt>16<tt>
</tt>17<tt>
</tt>18<tt>
</tt>19<tt>
</tt><strong>20</strong><tt>
</tt>21<tt>
</tt>22<tt>
</tt>23<tt>
</tt>24<tt>
</tt>25<tt>
</tt>26<tt>
</tt>27<tt>
</tt>28<tt>
</tt></pre></td>
<td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">class</span> <span class="cl">EvenNumber</span> < <span class="co">Struct</span>.new(<span class="sy">:value</span>)<tt>
</tt> <span class="r">def</span> <span class="fu"><=></span>(other)<tt>
</tt> puts <span class="s"><span class="dl">"</span><span class="il"><span class="idl">#{</span>value<span class="idl">}</span></span><span class="k"> <=> </span><span class="il"><span class="idl">#{</span>other.value<span class="idl">}</span></span><span class="dl">"</span></span><tt>
</tt> value <=> other.value<tt>
</tt> <span class="r">end</span><tt>
</tt><tt>
</tt> <span class="r">def</span> <span class="fu">succ</span><tt>
</tt> puts <span class="s"><span class="dl">"</span><span class="k">succ: </span><span class="il"><span class="idl">#{</span>value<span class="idl">}</span></span><span class="dl">"</span></span><tt>
</tt> <span class="co">EvenNumber</span>.new(value + <span class="i">2</span>)<tt>
</tt> <span class="r">end</span><tt>
</tt><span class="r">end</span><tt>
</tt><tt>
</tt>puts (<span class="co">EvenNumber</span>.new(<span class="i">2</span>)..<span class="co">EvenNumber</span>.new(<span class="i">6</span>)).include?(<span class="co">EvenNumber</span>.new(<span class="i">5</span>))<tt>
</tt><tt>
</tt><tt>
</tt><span class="c"># 1.8.7</span><tt>
</tt><span class="c"># 2 <=> 6</span><tt>
</tt><span class="c"># 2 <=> 5</span><tt>
</tt><span class="c"># 5 <=> 6</span><tt>
</tt><span class="c"># true # buggy!</span><tt>
</tt><span class="c"># 1.9.1 </span><tt>
</tt><span class="c"># 2 <=> 6</span><tt>
</tt><span class="c"># 2 <=> 6</span><tt>
</tt><span class="c"># succ: 2</span><tt>
</tt><span class="c"># 4 <=> 6</span><tt>
</tt><span class="c"># succ: 4</span><tt>
</tt><span class="c"># 6 <=> 6</span><tt>
</tt><span class="c"># false # correct!</span><tt>
</tt></pre></td>
</tr></table>
<p>This makes sense for the conceptual range, but has a performance impact especially on large ranges. <code>#include?</code> has gone from <code>O(1)</code> to <code>O(N)</code>. This is most likely to crop up when checking time ranges – Time#succ returns a time one second in the future.</p><table class="CodeRay"><tr>
<td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>4<tt>
</tt>5<tt>
</tt>6<tt>
</tt></pre></td>
<td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }">(<span class="co">Time</span>.utc(<span class="i">1999</span>)..<span class="co">Time</span>.utc(<span class="i">2001</span>)).include?(<span class="i">2000</span>) <tt>
</tt><tt>
</tt><span class="c"># 1.8.7</span><tt>
</tt><span class="c"># true</span><tt>
</tt><span class="c"># 1.9.1</span><tt>
</tt><span class="c"># Don't wait for this to finish...</span><tt>
</tt></pre></td>
</tr></table>
<h2>Workarounds</h2>
<p>Ruby 1.9 introduces a new method <code>Range#cover?</code> that implements the old <code>include?</code> behaviour, however this method isn’t available in 1.8.7.</p><table class="CodeRay"><tr>
<td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>4<tt>
</tt>5<tt>
</tt>6<tt>
</tt>7<tt>
</tt>8<tt>
</tt>9<tt>
</tt></pre></td>
<td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }">puts (<span class="co">EvenNumber</span>.new(<span class="i">2</span>)..<span class="co">EvenNumber</span>.new(<span class="i">6</span>)).cover?(<span class="co">EvenNumber</span>.new(<span class="i">5</span>))<tt>
</tt><tt>
</tt><span class="c"># 1.8.7</span><tt>
</tt><span class="c"># undefined method `cover?' for #<struct EvenNumber value=2>..#<struct EvenNumber value=6> (NoMethodError)</span><tt>
</tt><span class="c"># 1.9.1</span><tt>
</tt><span class="c"># 2 <=> 6</span><tt>
</tt><span class="c"># 2 <=> 5</span><tt>
</tt><span class="c"># 5 <=> 6</span><tt>
</tt><span class="c"># true</span><tt>
</tt></pre></td>
</tr></table>
<p>Another alternative, if it makes sense for your range, is to define the <code>to_int</code> method, which ruby will use to do a straight comparison against your min/max values.</p><table class="CodeRay"><tr>
<td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>4<tt>
</tt>5<tt>
</tt>6<tt>
</tt>7<tt>
</tt>8<tt>
</tt>9<tt>
</tt><strong>10</strong><tt>
</tt>11<tt>
</tt>12<tt>
</tt>13<tt>
</tt>14<tt>
</tt>15<tt>
</tt></pre></td>
<td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">class</span> <span class="cl">EvenNumber</span> < <span class="co">Struct</span>.new(<span class="sy">:value</span>)<tt>
</tt> <span class="c"># ... as before</span><tt>
</tt><tt>
</tt> <span class="r">def</span> <span class="fu">to_int</span><tt>
</tt> value<tt>
</tt> <span class="r">end</span><tt>
</tt><span class="r">end</span><tt>
</tt><tt>
</tt>puts (<span class="co">EvenNumber</span>.new(<span class="i">2</span>)..<span class="co">EvenNumber</span>.new(<span class="i">6</span>)).include?(<span class="co">EvenNumber</span>.new(<span class="i">5</span>))<tt>
</tt><tt>
</tt><span class="c"># 1.8.6 and 1.9.1</span><tt>
</tt><span class="c"># 2 <=> 6</span><tt>
</tt><span class="c"># 2 <=> 5</span><tt>
</tt><span class="c"># 5 <=> 6</span><tt>
</tt><span class="c"># true</span><tt>
</tt></pre></td>
</tr></table>
<p>Personally, I’ve monkey-patched range in 1.8.* to alias <code>cover?</code> to <code>include?</code>. That’s it. May your test suites not appear to hang.</p>