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

new(*inar) click to toggle source

@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.
Calls superclass method
# 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

==(r) click to toggle source

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
===(obj) click to toggle source

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
Also aliased as: include?, member?
bsearch(*rest, &bloc) click to toggle source

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}.

Calls superclass method
# 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
cover?(i) click to toggle source

See {#include?} or {#===}, and {Range#cover?}

Calls superclass method
# 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
each(*rest, &bloc) click to toggle source

@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.

Calls superclass method
# 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
eql?(r) click to toggle source

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
exclude_begin?() click to toggle source

Returns true if the “begin” boundary is excluded, or false otherwise.

# File lib/rangesmaller/rangesmaller.rb, line 141
def exclude_begin?
  @exclude_begin
end
first(*rest) click to toggle source

@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.

Calls superclass method
# 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
hash(*rest) click to toggle source

@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.

Calls superclass method
# 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
include?(obj)
Alias for: ===
inspect() click to toggle source

Return eg., ‘(“a”<…“c”)’, ‘(“a”<..“c”)’, if {#exclude_begin?} is true,

or else, identical to those for {Range}.

@return [String]

Calls superclass method
# 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
member?(obj)
Alias for: ===
min(*rest, &bloc) click to toggle source

See {#first} for the definition when {#exclude_begin?} is true.

Calls superclass method
# 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
min_by(*rest, &bloc) click to toggle source

See {#first} for the definition when {#exclude_begin?} is true.

Calls superclass method
# 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
minmax(*rest, &bloc) click to toggle source

See {#first} for the definition when {#exclude_begin?} is true.

Calls superclass method
# 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
minmax_by(*rest, &bloc) click to toggle source

See {#first} for the definition when {#exclude_begin?} is true.

Calls superclass method
# 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
rangepart() click to toggle source

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
size(*rest) click to toggle source

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).

Calls superclass method
# 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
step(*rest, &bloc) click to toggle source

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.

Calls superclass method
# 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
to_s() click to toggle source

Return eg., “(a<…c)”, “(a<..c)”, if {#exclude_begin?} is true,

or else, identical to those for {Range}.

@return [String]

Calls superclass method
# 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

midstr_when_exclude_begin() click to toggle source

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
rs_equal_core(r, method) click to toggle source

@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