class Rangesmaller
Class Rangesmaller
¶ ↑
- Authors
-
Masa Sakano
- License
-
MIT
Summary¶ ↑
Range
with inclusion of exclude_begin?().
Namely, the first element can be tagged as excluded, if specified so. It behaves the same as Range
. Note the method first() (hence each(), size() etc) behaves accordingly, depending whether the smallest boundary is excluded or not (default).
Examples¶ ↑
An instance of a range of 5 to 8 with both ends being exclusive is created as
r = Rangesmaller(5...8, :exclude_begin => true) r.exclude_begin? # => true
Public Class Methods
@overload new(rangesmaller)
@param [Rangesmaller] key
@overload new(range, opts)
@param range [Range] standard Range object @option opts [Object] :exclude_begin the begin boundary is excluded, if false (Default)
@overload new(obj_begin, obj_end, exclude_end=false)
This form is not recomended, but for the sake of compatibility with Range. @param obj_begin [Object] Any object (preferably {Comparable}) @param obj_end [Object] Any object, compatible with begin @param exclude_end [Object] true or false(Default) or any.
@overload new(obj_begin, obj_end, opts)
@param obj_begin [Object] Any object (preferably {#Comparable}) @param obj_end [Object] Any object, compatible with begin @param opts [Hash] see below @option opts [Object] :exclude_end the end boundary is excluded, or false (Default) @option opts [Object] :exclude_begin the begin boundary is excluded, or false (Default)
@note The here-mentioned “any” object means any object that can consist of {Range}.
For example, (nil..nil) is accepted, but (Complex(2,3)..Complex(2,3)) is not.
# File lib/rangesmaller/rangesmaller.rb, line 46 def initialize(*inar) @rangepart = nil # Defined here only if Range is given as # (a part of) the arguments, and in that case it will be # the same object. If not, left undefined, # but will be defined once rangepart() is called. # Note: it is entirely possible to set @rangepart here if wanted, # but there is no point to do that, providing no method # uses @rangepart explicitly, but only via rangepart(). @exclude_begin = false # Default case inar.size when 1 # Arg: (Rangesmaller or Range) r = inar[0] if defined?(r.rangepart) && defined?(r.exclude_begin?) # Rangesmaller @exclude_begin = (r.exclude_begin? && true) @rangepart = r.rangepart r = @rangepart super(r.begin, r.end, r.exclude_end?) else # Range if defined?(r.begin) && defined?(r.end) && defined?(r.exclude_end?) super(r.begin, r.end, r.exclude_end?) @rangepart = r else raise ArgumentError, "bad value for Rangesmaller" end end when 2 r = inar[0] if defined? r.exclude_end? # Arg: (Range, :exclude_begin => false) begin @exclude_begin = (inar[1][:exclude_begin] && true) rescue # cf., Rangesmaller.new(nil,5) # => ArgumentError: bad value for range raise ArgumentError, "bad value for Rangesmaller" end if defined?(r.begin) && defined?(r.end) # && defined?(r.exclude_end?) super(r.begin, r.end, r.exclude_end?) @rangepart = r begin if inar[1].has_key?(:exclude_end) if r.exclude_end? ^ inar[1][:exclude_end] warn "Warning(Rangesmaller.new): Option :exclude_end is given, but is meaningless, as Range is also given." end end rescue # Very unlikely. # The above is just a warning, hence no exception should be raised here, whatever the reason. end else # Very unlikely, but possible. raise ArgumentError, "bad value for Rangesmaller" end else # Arg: (Num(Begin), Num(End)) super end when 3 flag_end = false begin @exclude_begin = (inar[2][:exclude_begin] && true) if inar[2].has_key?(:exclude_end) flag_end = true end rescue NoMethodError, TypeError # TypeError can be raised if the third argument (inar[2]) is an array or Integer or alike. # Likely conventional argument list as in Range.new(). super # Arg: (begin, end, exclude_end=false) else if flag_end super(inar[0], inar[1], inar[2][:exclude_end]) # Arg: (begin, end, :exclude_end => SOMETHING, :exclude_begin => false) else super(inar[0], inar[1]) # Arg: (begin, end, opt_without_exclude_end) end end else raise ArgumentError, "wrong number of arguments (#{inar.size} for 1..3)" end # case inar.size # Adjust @exclude_begin (as it can be anything). if @exclude_begin @exclude_begin = true else @exclude_begin = false end end
Public Instance Methods
Like {Range}, returns true only if it is either {Range} (or its subclasses), and in addition if both {#exclude_begin?} and {#exclude_end?} match between the two objects. See {#eql?} @return [Boolean]
# File lib/rangesmaller/rangesmaller.rb, line 156 def ==(r) rs_equal_core(r, :==) end
See also {#cover?} and {Range#===} @return [Boolean]
# File lib/rangesmaller/rangesmaller.rb, line 168 def ===(obj) # ("a".."z")===("cc") # => false begin 1.0+(obj) # OK if Numeric. rescue TypeError # obj is not Numeric, hence runs brute-force check. each do |ei| if ei == obj return true end end false else cover?(obj) end end
bsearch is internally implemented by converting a float into 64-bit integer. The following examples demonstrate what is going on.
ary = [0, 4, 7, 10, 12] (3...4).bsearch{ |i| ary[i] >= 11} # => nil (3...5).bsearch{ |i| ary[i] >= 11} # => 4 (Integer) (3..5.1).bsearch{ |i| ary[i] >= 11} # => 4.0 (Float) (3.6..4).bsearch{ |i| ary[i] >= 11} # => 4.0 (Float) (3.6...4).bsearch{ |i| ary[i] >= 11} # => nil (3.6...4.1).bsearch{|i| ary[i] >= 11} # => 4.0 (Float) class Special def [](f) (f>3.5 && f<4) ? true : false end end sp = special.new (3..4).bsearch{ |i| sp[i]} # => nil (3...4).bsearch{ |i| sp[i]} # => nil (3.0...4).bsearch{|i| sp[i]} # => 3.5000000000000004 (3...4.0).bsearch{|i| sp[i]} # => 3.5000000000000004 (3.3..4).bsearch{ |i| sp[i]} # => 3.5000000000000004 (Rational(36,10)..5).bsearch{|i| ary[i] >= 11} => # TypeError: can't do binary search for Rational (Ruby 2.1) (3..Rational(61,10)).bsearch{|i| ary[i] >= 11} => # TypeError: can't do binary search for Fixnum (Ruby 2.1)
In short, bsearch works only with Integer and/or Float (as in Ruby 2.1). If either of begin and end is an Float, the search is conducted in Float and the returned value will be Float, unless nil. If Float, it searches on the binary plane. If Integer, the search is conducted on the descrete Integer points only, and no search will be made in between the adjascent integers.
Given that, Rangesmaller#bsearch
follows basically the same, even when exclude_begin? is true. If either end is Float, it searches between begin*(1+Float::EPSILON) and end. If both are Integer, it searches from begin+1. When {#exclude_begin?} is false, {Rangesmaller#bsearch} is identical to {Range#bsearch}.
# File lib/rangesmaller/rangesmaller.rb, line 228 def bsearch(*rest, &bloc) if @exclude_begin if ((Float === self.begin()) || (Integer === self.begin()) && (Float === self.end())) Range.new(self.begin()*(Float::EPSILON+1.0), self.end, exclude_end?).send(__method__, *rest, &bloc) # @note Technically, if begin is Rational, there is no strong reason it should not work. # However Range#bsearch does not accept Rational, hence this code. # Users should give a Rangesmaller with begin being Rational.to_f in that case. elsif (defined? self.begin().succ) # Both non-Float Range.new(self.begin().succ, self.end, exclude_end?).send(__method__, *rest, &bloc) # In practice it will not raise an Exception, only when both are Integer. else @rangepart.send(__method__, *rest, &bloc) # It will raise an exception anyway! Such as, (Rational..Rational) end else super end end
See {#include?} or {#===}, and {Range#cover?}
# File lib/rangesmaller/rangesmaller.rb, line 248 def cover?(i) # ("a".."z").cover?("cc") # => true if @exclude_begin if super if self.begin == i false else true end else false end else super end end
@raise [TypeError] If {#exclude_begin?} is true, and {#begin}() or {#rangepart} does not have a method of {#succ}, then even if no block is given, this method raises TypeError straightaway. @return [Rangesmaller] self @return [Enumerator] if block is not given.
# File lib/rangesmaller/rangesmaller.rb, line 270 def each(*rest, &bloc) # (1...3.5).each{|i|print i} # => '123' to STDOUT # (1.3...3.5).each # => #<Enumerator: 1.3...3.5:each> # (1.3...3.5).each{|i|print i} # => TypeError: can't iterate from Float # Note: If the block is not given and if @exclude_begin is true, the self in the returned Enumerator is not the same as self here. if @exclude_begin if defined? self.begin.succ ret = Range.new(self.begin.succ,self.end,exclude_end?).send(__method__, *rest, &bloc) if block_given? self else ret end else raise TypeError, "can't iterate from "+self.begin.class.name end else super end end
The same as {:==} but uses eql?() as each comparison.
# File lib/rangesmaller/rangesmaller.rb, line 161 def eql?(r) rs_equal_core(r, :eql?) end
Returns true if the “begin” boundary is excluded, or false otherwise.
# File lib/rangesmaller/rangesmaller.rb, line 141 def exclude_begin? @exclude_begin end
@param [Numeric] Optional. Must be non-negative. Consult {Range#first} for detail. @note Like {Range#last}, if no argument is given, it behaves like {#begin()}, that is, it returns the initial value, regardless of {#exclude_begin?}.
However, if an argument is given (nb., acceptable since Ruby 1.9) when {#exclude_begin?} is true, it returns the array that starts from {#begin}().succ().
@raise [TypeError] if the argument (Numeric) is given, yet if {#begin}().succ is not defined.
# File lib/rangesmaller/rangesmaller.rb, line 297 def first(*rest) # (1...3.1).last # => 3.1 # (1...3.1).last(1) # => [3] if ! @exclude_begin super else case rest.size when 0 self.begin when 1 ## Check the argument. Array.new[ rest[0] ] # Check Type of rest[0] (if invalid, it should cause TypeError) begin if rest[0] < 0 raise ArgumentError, "negative array size (or size too big)" end rescue NoMethodError # Should not happen, but just to play safe. end if (RUBY_VERSION < "1.9.1") && (1 == rest[0]) flag_18 = true else flag_18 = false end ## Main begin b = self.begin.succ rescue NoMethodError raise TypeError, "can't iterate from "+self.begin.class.name end begin Range.new(b, self.end, exclude_end?).send(__method__, *rest) rescue ArgumentError => err if flag_18 # first() does not accept an argument in Ruby 1.8. Range.new(b, self.end, exclude_end?).send(__method__) else raise err end end else raise ArgumentError, "wrong number of arguments (#{rest.size} for 0..1)" end end # if ! @exclude_begin end
@note When {#exclude_begin?} is true, the returned value is not strictly guaranteed to be unique, though in pracrtice it is most likely to be so.
# File lib/rangesmaller/rangesmaller.rb, line 350 def hash(*rest) if @exclude_begin Range.new(self.begin, self.end, exclude_end?).send(__method__, *rest) - 1 else super end end
Return eg., ‘(“a”<…“c”)’, ‘(“a”<..“c”)’, if {#exclude_begin?} is true,
or else, identical to those for {Range}.
@return [String]
# File lib/rangesmaller/rangesmaller.rb, line 362 def inspect if @exclude_begin self.begin.inspect + midstr_when_exclude_begin() + self.end.inspect else super end end
See {#first} for the definition when {#exclude_begin?} is true.
# File lib/rangesmaller/rangesmaller.rb, line 385 def min(*rest, &bloc) # (1...3.5).max # => TypeError: cannot exclude non Integer end value if @exclude_begin begin first_val = self.begin.succ rescue NoMethodError raise TypeError, 'cannot exclude non Integer begin value' end Range.new(first_val, self.end, exclude_end?).send(__method__, *rest, &bloc) else super end end
See {#first} for the definition when {#exclude_begin?} is true.
# File lib/rangesmaller/rangesmaller.rb, line 402 def min_by(*rest, &bloc) # (1...3.5).max # => TypeError: cannot exclude non Integer end value if @exclude_begin begin first_val = self.begin.succ rescue NoMethodError raise TypeError, 'cannot exclude non Integer begin value' end Range.new(first_val, self.end, exclude_end?).send(__method__, *rest, &bloc) else super end end
See {#first} for the definition when {#exclude_begin?} is true.
# File lib/rangesmaller/rangesmaller.rb, line 419 def minmax(*rest, &bloc) # (0...3.5).minmax # => [0, 3] # (1.3...5).minmax # => TypeError: can't iterate from Float # Note that max() for the same Range raises an exception. # In that sense, it is inconsistent! if @exclude_begin begin first_val = self.begin.succ rescue NoMethodError raise TypeError, "can't iterate from "+self.begin.class.name # Trap it, just in order to issue the error message that is consisntent with max(). # raise TypeError, 'cannot exclude non Integer begin value' end Range.new(first_val, self.end, exclude_end?).send(__method__, *rest, &bloc) else super end end
See {#first} for the definition when {#exclude_begin?} is true.
# File lib/rangesmaller/rangesmaller.rb, line 441 def minmax_by(*rest, &bloc) # (0...3.5).minmax # => [0, 3] # Note that max() for the same Range raises an exception. # In that sense, it is inconsistent! if @exclude_begin begin first_val = self.begin.succ rescue NoMethodError raise TypeError, "can't iterate from "+self.begin.class.name # Trap it, just in order to issue the error message that is consisntent with max(). # raise TypeError, 'cannot exclude non Integer begin value' end Range.new(first_val, self.end, exclude_end?).send(__method__, *rest, &bloc) else super end end
Return the object equivalent to the {Range} part (namely, without the definition of {#exclude_begin?}) @return [Range]
# File lib/rangesmaller/rangesmaller.rb, line 148 def rangepart @rangepart ||= Range.new(self.begin, self.end, self.exclude_end?) end
See {#first} for the definition when {#exclude_begin?} is true. @see blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/49797 [ruby-list:49797] from matz for how {#size} behaves (in Japanese).
# File lib/rangesmaller/rangesmaller.rb, line 463 def size(*rest) # (1..5).size # => 5 # (1...5).size # => 4 # (0.8...5).size # => 5 # Why??? # (1.2...5).size # => 4 # Why??? # (1.2..5).size # => 4 # Why??? # (Rational(3,2)...5).size => 3 # (1.5...5).size # => 4 # Why not 3?? # (1.5...4.9).size # => 4 # Why not 3?? # (1.5...4.5).size # => 3 # (0...Float::INFINITY).size # => Infinity if @exclude_begin # Dealing with Infinity. begin inf = Float::INFINITY rescue # Ruby 1.8 or earlier. inf = 1/0.0 end if -inf == self.begin if -inf == self.end return 0 else return inf end elsif inf == self.begin if inf == self.end return 0 else return inf end end begin 1.0 + self.begin() # Numeric Range.new(self.begin()+1, self.end, exclude_end?).send(__method__, *rest) # Swap the order of '+' from the above, so that Integer/Rational is calculated as it is. rescue TypeError # Non-Numeric Range.new(self.begin().succ, self.end, exclude_end?).send(__method__, *rest) # => nil in Ruby 2.1 end else super end end
See {#each}. @raise [TypeError] If {#exclude_begin?} is true, and {#begin}() or {#rangepart} does not have a method of {#succ}, then even if no block is given, this method raises TypeError straightaway. @return [Rangesmaller] self @return [Enumerator] if block is not given.
# File lib/rangesmaller/rangesmaller.rb, line 516 def step(*rest, &bloc) # (1...3.5).each{|i|print i} # => '123' to STDOUT # (1.3...3.5).each # => #<Enumerator: 1.3...3.5:each> # (1.3...3.5).each{|i|print i} # => TypeError: can't iterate from Float # Note: If the block is not given and if @exclude_begin is true, the self in the returned Enumerator is not the same as self here. if @exclude_begin if defined? self.begin.succ ret = Range.new(self.begin.succ,self.end,exclude_end?).send(__method__, *rest, &bloc) if block_given? self else ret end else raise TypeError, "can't iterate from "+self.begin.class.name end else super end end
Return eg., “(a<…c)”, “(a<..c)”, if {#exclude_begin?} is true,
or else, identical to those for {Range}.
@return [String]
# File lib/rangesmaller/rangesmaller.rb, line 374 def to_s if @exclude_begin self.begin.to_s + midstr_when_exclude_begin() + self.end.to_s else super end end
Private Instance Methods
Middle string for {#inspect} and {#to_s}
# File lib/rangesmaller/rangesmaller.rb, line 540 def midstr_when_exclude_begin if exclude_end? "<..." else "<.." end end
@param r [Object] to compare. @param method [Symbol] of the method name.
# File lib/rangesmaller/rangesmaller.rb, line 550 def rs_equal_core(r, method) if ! defined? r.exclude_end? false # Not Range family. elsif empty? && defined?(r.is_none?) && r.is_none? # r is RangeOpen::NONE true elsif defined? r.exclude_begin? (self.exclude_begin? ^! r.exclude_begin?) && (self.exclude_end? ^! r.exclude_end?) && (self.begin.send(method, r.begin)) && (self.end.send( method, r.end)) # (self.begin == r.begin) && # (self.end == r.end) else # r is Range if self.exclude_begin? false else @rangepart.send(method, r) # Comparison as two Range-s. end end end