Re-enable spec/library for full CI runs.
[rbx.git] / kernel / core / range.rb
blobc93956cb171cd5f591fe9d9fbfd35a8abc30dda6
1 # depends on: class.rb enumerable.rb
3 ##
4 # A Range represents an interval, a set of values with a start and an end.
6 # Ranges may be constructed using the <tt>s..e</tt> and <tt>s...e</tt>
7 # literals, or with Range::new.
9 # Ranges constructed using <tt>..</tt> run from the start to the end
10 # inclusively. Those created using <tt>...</tt> exclude the end value. When
11 # used as an iterator, ranges return each value in the sequence.
13 #   (-1..-5).to_a      #=> []
14 #   (-5..-1).to_a      #=> [-5, -4, -3, -2, -1]
15 #   ('a'..'e').to_a    #=> ["a", "b", "c", "d", "e"]
16 #   ('a'...'e').to_a   #=> ["a", "b", "c", "d"]
18 # Ranges can be constructed using objects of any type, as long as the objects
19 # can be compared using their <tt><=></tt> operator and they support the
20 # <tt>succ</tt> method to return the next object in sequence.
22 #   class Xs # represent a string of 'x's
23 #     include Comparable
24 #     attr :length
25 #     def initialize(n)
26 #       @length = n
27 #     end
28 #     def succ
29 #       Xs.new(@length + 1)
30 #     end
31 #     def <=>(other)
32 #       @length <=> other.length
33 #     end
34 #     def to_s
35 #       sprintf "%2d #{inspect}", @length
36 #     end
37 #     def inspect
38 #       'x'# @length
39 #     end
40 #   end
41 #   
42 #   r = Xs.new(3)..Xs.new(6)   #=> xxx..xxxxxx
43 #   r.to_a                     #=> [xxx, xxxx, xxxxx, xxxxxx]
44 #   r.member?(Xs.new(5))       #=> true
46 # In the previous code example, class Xs includes the Comparable module. This
47 # is because Enumerable#member? checks for equality using ==. Including
48 # Comparable ensures that the == method is defined in terms of the <=> method
49 # implemented in Xs.
51 class Range
52   include Enumerable
54   ##
55   # Constructs a range using the given +start+ and +end+.
56   #
57   # If the third parameter is omitted or is false, the range will include the
58   # end object; otherwise, it will be excluded.
60   def initialize(first, last, exclude_end = false)
61     raise NameError, "`initialize' called twice" if @begin
62     
63     unless first.is_a?(Fixnum) && last.is_a?(Fixnum)
64       begin
65         raise ArgumentError, "bad value for range" unless first <=> last
66       rescue
67         raise ArgumentError, "bad value for range"
68       end
69     end
70     
71     @begin = first
72     @end = last
73     @excl = exclude_end
74   end
76   # Returns <tt>true</tt> only if <em>obj</em> is a Range, has
77   # equivalent beginning and end items (by comparing them with
78   # <tt>==</tt>), and has the same #exclude_end? setting as <i>rng</t>.
79   #
80   #   (0..2) == (0..2)            #=> true
81   #   (0..2) == Range.new(0,2)    #=> true
82   #   (0..2) == (0...2)           #=> false
83   def ==(other)
84     self.equal?(other) ||
85       (other.is_a?(Range) && self.first == other.first &&
86        self.last == other.last && self.exclude_end? == other.exclude_end?)
87    
88   end
89   alias_method :eql?, :==
91   # Returns <tt>true</tt> if <em>obj</em> is an element of <em>rng</em>,
92   # <tt>false</tt> otherwise. Conveniently, <tt>===</tt> is the
93   # comparison operator used by <tt>case</tt> statements.
94   #
95   #   case 79
96   #     when 1..50   then   print "low\n"
97   #     when 51..75  then   print "medium\n"
98   #     when 76..100 then   print "high\n"
99   #   end
100   #
101   # <em>produces:</em>
102   #
103   #   high
104   def ===(value)
105     if @begin <= value
106       if self.exclude_end?
107         return true if value < @end
108       else
109         return true if value <= @end
110       end
111     end
112     return false
113   rescue
114     return false
115   end
116   alias_method :member?, :===
117   alias_method :include?, :===
119   ##
120   # :call-seq:
121   #   rng.each { |i| block }  => rng
122   #
123   # Iterates over the elements +rng+, passing each in turn to the block. You
124   # can only iterate if the start object of the range supports the
125   # succ method (which means that you can't iterate over ranges of
126   # Float objects).
127   #
128   #   (10..15).each do |n|
129   #      print n, ' '
130   #   end
131   #
132   # produces:
133   #
134   #   10 11 12 13 14 15
136   def each(&block)
137     first, last = @begin, @end # dup?
138     
139     raise TypeError, "can't iterate from #{first.class}" unless first.respond_to? :succ
141     if first.is_a?(Fixnum) && last.is_a?(Fixnum)
142       last -= 1 if self.exclude_end?
143       first.upto(last, &block)
144     elsif first.is_a?(String)
145       first.upto(last) do |s|
146         block.call(s) unless @excl && s == last
147       end
148     else
149       current = first
150       if @excl then
151         while (current <=> last) < 0
152           block.call(current)
153           current = current.succ
154         end
155       else
156         while (c = current <=> last) && c <= 0
157           block.call(current)
158           break if c == 0
159           current = current.succ
160         end
161       end
162     end
163     return self
164   end
166   ##
167   # :call-seq:
168   #   rng.exclude_end?  => true or false
169   #
170   # Returns true if +rng+ excludes its end value.
172   def exclude_end?
173     @excl
174   end
176   ##
177   # :call-seq:
178   #   rng.first  => obj
179   #   rng.begin  => obj
180   #
181   # Returns the first object in +rng+.
183   def first
184     @begin
185   end
186   alias_method :begin, :first
188   # Generate a hash value such that two ranges with the same start and
189   # end points, and the same value for the "exclude end" flag, generate
190   # the same hash value.
191   def hash
192     excl = @excl ? 1 : 0
193     hash = excl
194     hash ^= @begin.hash << 1
195     hash ^= @end.hash << 9
196     hash ^= excl << 24;
197     return hash
198   end
200   # Convert this range object to a printable form (using
201   # <tt>inspect</tt> to convert the start and end objects).
202   def inspect
203     "#{@begin.inspect}#{@excl ? "..." : ".."}#{@end.inspect}"
204   end
206   # Returns the object that defines the end of <em>rng</em>.
207   #
208   #    (1..10).end    #=> 10
209   #    (1...10).end   #=> 10
210   def last
211     @end
212   end
213   alias_method :end, :last
215   ##
216   # :call-seq:
217   #   rng.step(n = 1) { |obj| block }  => rng
218   #
219   # Iterates over +rng+, passing each +n+th element to the block. If the range
220   # contains numbers or strings, natural ordering is used. Otherwise
221   # +step+ invokes +succ+ to iterate through range elements. The following
222   # code uses class Xs, which is defined in the class-level documentation.
223   #
224   #   range = Xs.new(1)..Xs.new(10)
225   #   range.step(2) { |x| puts x }
226   #   range.step(3) { |x| puts x }
227   #
228   # produces:
229   #
230   #    1 x
231   #    3 xxx
232   #    5 xxxxx
233   #    7 xxxxxxx
234   #    9 xxxxxxxxx
235   #    1 x
236   #    4 xxxx
237   #    7 xxxxxxx
238   #   10 xxxxxxxxxx
240   def step(step_size = 1, &block) # :yields: object
241     first, last = @begin, @end
242     step_size = (Float === first) ? Float(step_size) : Integer(step_size)
244     raise ArgumentError, "step can't be negative" if step_size < 0
245     raise ArgumentError, "step can't be 0" if step_size == 0
246     
247     if step_size == 1
248       each(&block)
249     elsif first.kind_of?(Numeric)
250       cmp_method = self.exclude_end? ? :< : :<=
251       
252       while first.__send__(cmp_method, last)
253         block.call(first)
254         first += step_size
255       end
256     else
257       counter = 0
258       each do |o|
259         block.call(o) if counter % step_size == 0
260         counter += 1
261       end
262     end
263     
264     return self
265   end
267   ##
268   # Convert this range object to a printable form.
270   def to_s
271     "#{@begin}#{@excl ? "..." : ".."}#{@end}"
272   end