class RangeExtd

Class RangeExtd

Authors

Masa Sakano

License

MIT

Summary

Extended Range class that features:

1. includes exclude_begin? (to exclude the "begin" boundary),
2. allows open-ended range to the infinity (very similar to beginless/endless Range),
3. defines NONE and ALL constants,
4. the first self-consistent logical structure,
5. complete compatibility with the built-in Range.

The instance of this class is immutable, that is, you can not alter the element once an instance is generated.

This class has some constants

What is valid is checked with the class method {RangeExtd.valid?}. See the document of that method for the definition.

This class has two constants: {RangeExtd::NONE} representing an empty range and {RangeExtd::ALL} representing the entire range, both in the abstract sense.

@example An instance of a range of 5 to 8 with both ends being exclusive is created as

r = RangeExtd(5...8, true) 
r.exclude_begin?  # => true

NOTE: Write nothing for the class description of RangeExtd, because it would have a higher priority for yard!

NOTE: Write nothing for the class description of RangeExtd, because it would have a higher priority for yard!

Constants

ALL

Range covers everything.

ERR_MSGS

Error messages

NONE
No range.
In Ruby1.8, this causes  ArgumentError: bad value for range (because (nil..nil) is unaccepted).

NONE = RangeExtd.new(nil, nil, true, true, :Constant)

Public Class Methods

middle_strings() click to toggle source

See {RangExtd.middle_strings=}() for detail.

@return [Array<String>]

# File lib/range_extd.rb, line 1227
def self.middle_strings()
  @@middle_strings
end
middle_strings=(ary) click to toggle source

Set the class variable to be used in {RangeExtd#to_s} and {RangeExtd#inspect} to configure the format of their returned values.

The parameters should be given as an Array with 7 elements of string in principle, which gives the characters for each index:

  1. prefix

  2. begin-inclusive

  3. begin-exclusive

  4. middle-string to bridge both ends

  5. end-exclusive

  6. end-inclusive

  7. postfix

If the elements [1] and [2], or [4] and [5] are equal, a warning is issued as some of {RangeExtd} in display will be indistinguishable. Note even if no warning is issued, that does not mean all the forms will be not ambiguous. For example, if you specify

['(', '', '.', '..', '.', '', ')']

a string (3…7) can mean either exclusive {#begin} or {#end}. It is user’s responsibility to make it right.

The two most popular forms can be given as a Symbol instead of Array, that is,

:default  ( ['', '', '<', '..', '.', '', ''] )
:math     ( ['', '<=', '<', 'x', '<', '<=', ''] )

@param ary [Array, Symbol] @return [Array, Symbol]

@example

RangeExtd.middle_strings=:default  # Default
RangeExtd(2...6).to_s    # => "2...6"
RangeExtd(2,6,1).to_s    # => "2<..6"
RangeExtd.middle_strings=:math
RangeExtd(2...6).to_s    # => "2<=x<6"
RangeExtd(2,6,1).to_s    # => "2<x<=6"
RangeExtd.middle_strings=['[','(in)','(ex)',', ','(ex)','(in)',']']
RangeExtd(2...6).to_s    # => "[2(in), (ex)6]"
RangeExtd(2,6,1).to_s    # => "[2(ex), (in)6]"
# File lib/range_extd.rb, line 1201
def self.middle_strings=(ary)
  case ary
  when :default
    @@middle_strings = ['', '', '<', '..', '.', '', '']
  when :math
    @@middle_strings = ['', '<=', '<', 'x', '<', '<=', '']
  else
    begin
      if ary.size == 7
        _dummy = 'a' + ary[6]
        @@middle_strings = ary
        if (ary[1] == ary[2]) || (ary[4] == ary[5])
          warn "warning: some middle_strings are indistinguishable."
        end
      else
        raise
      end
    rescue
      raise ArgumentError, "invalid argument"
    end
  end
end
new(*inar, **hsopt) click to toggle source

@note The flag of exclude_begin|end can be given in the arguments in a couple of ways.

If there is any duplication, those specified in the optional hash have the highest
priority.  Then the two descrete Boolean parameters have the second.
If not, the values embeded in the {Range} or {RangeExtd} object or the String form
in the parameter are used.  In default, both of them are false.

@example

RangeExtd(1...2)
RangeExtd(1..3, true)
RangeExtd(2..3, :exclude_begin => true)
RangeExtd(1, 4, false, true)
RangeExtd(1,'<...',5)
RangeExtd.middle_strings = :math
RangeExtd(2,'<x<=',5)
RangeExtd(RangeExtd::Infinity::NEGATIVE..RangeExtd::Infinity::POSITIVE)

@overload new(range, [exclude_begin=false, [exclude_end=false]], opts)

@param [Object] range Instance of {Range} or its subclasses, including {RangeExtd}
@param exclude_begin [Boolean] If specified, this has the higher priority, or false in default.
@param exclude_end [Boolean] If specified, this has the higher priority, or false in default.
@option opts [Boolean] :exclude_begin If specified, this has the highest priority, or false in default.
@option opts [Boolean] :exclude_end If specified, this has the highest priority, or false in default.

@overload new(obj_begin, obj_end, [exclude_begin=false, [exclude_end=false]], opts)

@param obj_begin [Object] Any object that is +Comparable+ with end
@param obj_end [Object] Any object that is Comparable with begin
@param exclude_begin [Boolean] If specified, this has the lower priority, or false in default.
@param exclude_end [Boolean] If specified, this has the lower priority, or false in default.
@option opts [Boolean] :exclude_begin If specified, this has the higher priority, or false in default.
@option opts [Boolean] :exclude_end If specified, this has the higher priority, or false in default.

@overload new(obj_begin, string_form, obj_end, [exclude_begin=false, [exclude_end=false]], opts)

@param obj_begin [Object] Any object that is +Comparable+ with end
@param string_form [Object] String form (without pre/postfix) of range expression set by {RangeExtd.middle_strings=}()
@param obj_end [Object] Any object that is +Comparable+ with begin
@param exclude_begin [Boolean] If specified, this has the lower priority, or false in default.
@param exclude_end [Boolean] If specified, this has the lower priority, or false in default.
@option opts [Boolean] :exclude_begin If specified, this has the higher priority, or false in default.
@option opts [Boolean] :exclude_end If specified, this has the higher priority, or false in default.

Note if you use the third form with “string_form” with the user-defined string (via {RangeExtd.middle_strings=}()), make 100 per cent sure you know what you are doing. If the string is ambiguous, the result may differ from what you thought you would get! See {RangeExtd.middle_strings=}() for detail. Below are a couple of examples:

RangeExtd.new(5, '....', 6)      # => RangeError because (5..("....")) is an invalid Range.
RangeExtd.new("%", '....', "y")  # => ("%" <.. "....")
                                 #   n.b., "y" is interpreted as TRUE for
                                 #   the flag for "exclude_begin?"
RangeExtd.new("x", '....', "y")  # => RangeError because ("x" <..("....")) is an invalid RangeExte,
                                 #   in the sense String "...." is *smaller* than "x"
                                 #   in terms of the "<=>" operator comparison.

@raise [ArgumentError] particularly if the range to be created is not {#valid?}.

Calls superclass method
# File lib/range_extd.rb, line 119
def initialize(*inar, **hsopt)        # **k expression from Ruby 1.9?

  # This is true only for RangeExtd::NONE,
  # which is identical to +RangeExtd(nil, nil, true, true)+ without this.
  @is_none = false

  if inar[4] == :Constant
    # Special case to create two Constants (NONE and ALL)
    @rangepart = (inar[2] ? (inar[0]...inar[1]) : (inar[0]..inar[1]))
    @exclude_end, @exclude_begin = inar[2..3]

    # In Ruby-2.7+ and hence RangeExtd Ver.2+, RangeExtd::NONE looks very similar to (nil...nil)
    # except RangeExtd::NONE.@exclude_begin == true
    #@is_none = (@rangepart.begin.nil? && @rangepart.end.nil? && @exclude_begin && @exclude_end)
    @is_none = (@rangepart.begin.respond_to?(:nowhere?) &&
                @rangepart.begin.nowhere? &&
                @rangepart.end.respond_to?(:nowhere?) &&
                @rangepart.end.nowhere? &&
                @exclude_begin &&
                @exclude_end)
    raise(ArgumentError, "NONE has been already defined.") if @is_none && self.class.const_defined?(:NONE)  
    super(*inar[0..2])
    return
  end

  arout = RangeExtd.send(:_get_init_args, *inar, **hsopt)
  # == [RangeBeginValue, RangeEndValue, exclude_begin?, exclude_end?]

  ### The following routine is obsolete.
  ### Users, if they wish, should call RangeExtd::Infinity.overwrite_compare() beforehand.
  ### Or better, design their class properly in the first place!
  ### See the document in Object#<=> in this code for detail.
  #
  # # Modify (<=>) method for the given object, so that
  # # it becomes comparable with RangeExtd::Infinity,
  # # if the object is already Comparable.
  # #
  # # This must come first.
  # # Otherwise it may raise ArgumentError "bad value for range",
  # # because the native Range does not accept
  # #   (Obj.new..RangeExtd::Infinity::POSITIVE)
  # #
  # boundary = nil
  # aroutid0 = arout[0].object_id
  # aroutid1 = arout[1].object_id
  # if    aroutid0 == RangeExtd::Infinity::NEGATIVE.object_id ||
  #       aroutid0 == RangeExtd::Infinity::POSITIVE.object_id
  #   boundary = arout[1]
  # elsif aroutid1 == RangeExtd::Infinity::NEGATIVE.object_id ||
  #       aroutid1 == RangeExtd::Infinity::POSITIVE.object_id
  #   boundary = arout[0]
  # end
  # if (! boundary.nil?) && !defined?(boundary.infinity?)
  #   RangeExtd::Infinity.overwrite_compare(boundary) # To modify (<=>) method for the given object.
  #   # Infinity::CLASSES_ACCEPTABLE ...
  # end

  if ! RangeExtd.valid?(*arout)
    raise RangeError, "the combination of the arguments does not constitute a valid RangeExtd instance."
  end

  @exclude_end   = arout.pop
  @exclude_begin = arout.pop
  artmp = [arout[0], arout[1], @exclude_end]
  @rangepart = Range.new(*artmp)
  super(*artmp)
end
valid?(*inar) click to toggle source

Returns true if the range to be constructed (or given) is valid, as a range, accepted in {RangeExtd}.

This routine is also implemented as a method in {Range}, and accordingly its sub-classes.

This routine is called from {RangeExtd.new}, hence for any instance of {RangeExtd} class, its {#valid?} returns true.

What is valid is defined as follows:

  1. The {#begin} and {#end} elements must be Comparable to each other, and the comparison results must be consistent betwen the two. The three exceptions are {RangeExtd::NONE} and Beginless and Endless Ranges introduced in Ruby 2.7 and 2.6, respectively (see below for the exceptions), which are all valid. Accordingly, +(nil..nil)+ is valid in {RangeExtd} Ver.1.0+ (nb., it used to raise Exception in Ruby 1.8).

  2. Except for {RangeExtd::NONE} and Beginless Range, {#begin} must have the method +<=+. Therefore, some Endless Ranges (Ruby 2.6 and later) like +(true..)+ are not valid. Note even “true” has the method +<=>+ and hence checking +<=+ is essential.

  3. Similarly, except for {RangeExtd::NONE} and Endless Range, {#end} must have the method +<=+. Therefore, some Beginless Ranges (Ruby 2.7 and later) like +(..true)+ are not valid.

  4. {#begin} must be smaller than or equal to {#end}, that is, ({#begin} <=> {#end}) must be either -1 or 0.

  5. If {#begin} is equal to {#end}, namely, ({#begin} <=> {#end}) == 0, the exclude status of the both ends must agree, except for the cases where both {#begin} and {#end} ani nil (beginless and endless Range). In other words, if the {#begin} is excluded, {#end} must be also excluded, and vice versa. For example, +(1…1)+ is NOT valid for this reason, because any built-in Range object has the exclude status of false (namely, inclusive) for {#begin}, whereas +RangeExtd(1…1, true)+ is valid and equal (+==+) to {RangeExtd::NONE}.

  6. If either or both of {#begin} and {#end} is {RangeExtd::Nowhere::NOWHERE}, the range has to be {RangeExtd::NONE}.

Note the second last point may change in the future release.

Note ([2]..) is NOT valid, because Array does not include Comparable for some reason, as of Ruby 2.1.1, even though it has the redefined and working [#<=>]. You can make those valid, by including Comparable in Array class, should you wish.

@example

RangeExtd.valid?(nil..nil)     # => true
RangeExtd.valid?(nil...nil)    # => true
RangeExtd.valid?(nil<..nil)    # => true
RangeExtd.valid?(nil<...nil)   # => true
RangeExtd.valid?(0..0)         # => true
RangeExtd.valid?(0...0)        # => false
RangeExtd.valid?(0...)         # => true
RangeExtd.valid?(true..)       # => false
RangeExtd.valid?(0..0,  true)  # => false
RangeExtd.valid?(0...0, true)  # => true
RangeExtd.valid?(2..-1)        # => false
RangeExtd.valid?(RangeExtd::NONE)     # => true
RangeExtd.valid?(RangeExtd::ALL)      # => true
RangeExtd.valid?(3..Float::INFINITY)  # => true
RangeExtd.valid?(3..Float::INFINITY, true)  # => true
RangeExtd.valid?(RangeExtd::Infinity::NEGATIVE..?d)        # => true
RangeExtd.valid?(RangeExtd::Infinity::NEGATIVE..?d, true)  # => true
RangeExtd.valid?(RangeExtd::Nowhere::NOWHERE..nil)  # => false

@note The flag of exclude_begin|end can be given in the arguments in a couple of ways.

If there is any duplication, those specified in the optional hash have the highest
priority.  Then the two descrete Boolean parameters have the second.
If not, the values embeded in the {Range} or {RangeExtd} object
in the parameter are used.  In default, both of them are false.

@overload valid?(range, [exclude_begin=false, [exclude_end=false]])

@param range [Object] Instance of Range or its subclasses, including RangeExtd
@param exclude_begin [Boolean] If specified, this has the higher priority, or false in default.
@param exclude_end [Boolean] If specified, this has the higher priority, or false in default.

@overload valid?(obj_begin, obj_end, [exclude_begin=false, [exclude_end=false]])

@param obj_begin [Object] Any object that is +Comparable+ with end
@param obj_end [Object] Any object that is +Comparable+ with begin (or nil, for Ruby 2.6 onwards)
@param exclude_begin [Boolean] If specified, this has the lower priority, or false in default.
@param exclude_end [Boolean] If specified, this has the lower priority, or false in default.
# File lib/range_extd.rb, line 1098
def self.valid?(*inar)
  (vbeg, vend, exc_beg, exc_end) = _get_init_args(*inar)

  if defined?(inar[0].is_none?) && inar[0].is_none? && exc_beg && exc_end
    # inar[0] is RangeExtd::NONE
    return true
  elsif vbeg.nil? && vbeg.nowhere? || vend.nil? && vend.nowhere?
    # RangeExtd::Nowhere::NOWHERE should not reside anywhere but in RangeExtd::NONE
    return false
  elsif vbeg.nil? && vend.nil?
    return true
  end

  return false if !vbeg.respond_to?(:<=>)
  begin
    t = (vbeg <=> vend)
    begin
      if vbeg.nil?  # Beginless Range introduced in Ruby 2.7
        return vend.respond_to?(:<=)
      elsif vend.nil?
        begin
          _ = (vbeg..nil)  # Endless Range introduced in Ruby 2.6
          return vbeg.respond_to?(:<=)
        rescue ArgumentError
          # Before Ruby 2.6
          return false
        end
      end
      return false if !vend.respond_to?(:<=>)
      return false if t != -1*(vend <=> vbeg) # false if not commutative (n.b., an exception should not happen).
    rescue NoMethodError, TypeError
      if (Float === vend && defined?(vbeg.infinity?) && vbeg.infinity?) ||
         (Float === vbeg && defined?(vend.infinity?) && vend.infinity?)
        warn self.const_get(:ERR_MSGS)[:infinity_compare] if !$VERBOSE.nil?  # one of the tests comes here.
      end
      return false    # return
    end
  rescue
    warn "This should not happen. Contact the code developer (warn01)."
    false     # return
  else
    case t
    when -1
      true
    when 0
      if defined?(vbeg.<=) && defined?(vend.<=)       # Comparable?
        ((true && exc_beg) ^! exc_end)        # True if single value or empty, false if eg, (1...1)
      else
        false         # Not Comparable
      end
    when 1
      false
    else
      warn "This should not happen. Contact the code developer (warn02)."
      if (Float === vend && defined?(vbeg.infinity?) && vbeg.infinity?) ||
         (Float === vbeg && defined?(vend.infinity?) && vend.infinity?)
        warn self.const_get(:ERR_MSGS)[:infinity_compare] if !$VERBOSE.nil?  # not tested so far?
      end
      false   # Not Comparable.
    end       # case t
    # All statements of return above.
  end
end

Private Class Methods

_get_init_args(*inar, **hsopt) click to toggle source

Private class method to evaluate the arguments.

@note The specification changed from RangeExtd Ver.1 to Ver.2.

In Ver.1 or earlier, this returns [begin, end, exclude_end, exclude_begin]
In Ver.2+, this returns [begin, end, exclude_begin, exclude_end]
Notice the third and fourth elements are swapped. Now it is in line
with {RangeExtd.new}.

@param (see RangeExtd#initialize) (or valid) @raise [ArgumentError] if the input format is invalid (otherwise the caller may raise RangeError (it depends)) @return [Array<Object, Object, Boolean, Boolean>] 4-compoents: [begin, end, exclude_begin, exclude_end]

# File lib/range_extd.rb, line 880
def self._get_init_args(*inar, **hsopt)
  nMin = 1; nMax = 5
  if inar.size < nMin || nMax < inar.size
    raise ArgumentError, "wrong number of arguments (#{inar.size} for #{nMin}..#{nMax})"
  end

  hsFlag = { :prm1st => nil, :excl_offset => nil }
  if    defined? inar[0].exclude_begin?
    hsFlag[:prm1st] = :rangeextd
  elsif defined? inar[0].exclude_end?
    hsFlag[:prm1st] = :range
  else
    hsFlag[:prm1st] = :object
  end

  case hsFlag[:prm1st]
  when :rangeextd, :range
    if inar.size > 1
      exclude_begin = (true ^! inar[1])
    elsif :rangeextd == hsFlag[:prm1st]
      exclude_begin = inar[0].exclude_begin?
    else
      exclude_begin = false
    end

    if inar.size > 2
      exclude_end   = (true ^! inar[2])
    else
      exclude_end   = inar[0].exclude_end?
    end

    nMin = 1; nMax = 3
    if inar.size > nMax
      raise ArgumentError, "wrong number of arguments (#{inar.size} for #{nMin}..#{nMax})"
    end

    beginend = [inar[0].begin, inar[0].end]

  when :object
    nMin = 2; nMax = 5
    if inar.size < 2
      raise ArgumentError, "wrong number of arguments (#{inar.size} for #{nMin}..#{nMax})"
    end

    # Default: assuming the form (obj_begin, obj_end, [excl_begin, [excl_end]])
    beginend = [inar[0], inar[1]]
    hsFlag[:excl_offset] = 0
    exclude_begin = false
    exclude_end   = false

    # Now, checking if the form is the String one, and if so, process it.
    arMid = @@middle_strings.map{|i| Regexp.quote(i)} # See self.middle_strings=(ary) for description.

    # Originally, defined?(inar[1].=~) seemed enough.  But as of Ruby 2.6 (maybe even before),
    # Numeric has :=~ method as well!
    if (inar.size > 2 &&
        inar[1].respond_to?(:=~) &&
        inar[1].respond_to?(:to_str))
      begin
        cmp = (inar[0] <=> inar[2]).abs
      rescue
        cmp = nil
      end
      if ((cmp == 0)||(cmp == 1)) && (inar[1] =~ /^(#{arMid[1]}|#{arMid[2]})#{arMid[3]}(#{arMid[4]}|#{arMid[5]})$/)
        # The form is (obj_begin, midStr, obj_end, [excl_begin, [excl_end]])
        # Hence all the default values are overwritten.
        beginend = [inar[0], inar[2]]
        hsFlag[:excl_offset] = 1
        exclude_begin = ($1 != @@middle_strings[1])
        exclude_end   = ($2 == @@middle_strings[4])
      else
        nMin = 2; nMax = 4
        if inar.size > nMax
          raise ArgumentError, "wrong number of arguments (#{inar.size} for #{nMin}..#{nMax})"
        end
      end
    else
      nMin = 2; nMax = 4
      if inar.size > nMax
        raise ArgumentError, "wrong number of arguments (#{inar.size} for #{nMin}..#{nMax})"
      end
    end

    if inar.size > 2+hsFlag[:excl_offset]
      exclude_begin = (true ^! inar[2+hsFlag[:excl_offset]])  # 3rd or 4th argument
    end

    if inar.size > 3+hsFlag[:excl_offset]
      exclude_end   = (true ^! inar[3+hsFlag[:excl_offset]])  # 4th or 5th argument
    end

  else
    raise     # (for coding safety)
  end         # case hsFlag[:prm1st]

  if hsopt.has_key?(:exclude_begin)
    exclude_begin = (hsopt[:exclude_begin] && true)
  end
  if hsopt.has_key?(:exclude_end)
    exclude_end   = (hsopt[:exclude_end] && true)
  end

  # [RangeBeginValue, RangeEndValue, exclude_begin?, exclude_end?]
  _normalize_infinity_float(beginend) + [exclude_begin, exclude_end]
end
_normalize_infinity_float(beginend) click to toggle source

Replaces {RangeExtd::Infinity} with {Float::INFINITY} when appropriate

@param beginend [Array] 2-compoents(begin, end) @return [Array] 2-compoents(begin, end)

# File lib/range_extd.rb, line 991
def self._normalize_infinity_float(beginend)
  is_begin_inf = Infinity.infinity?(beginend[0])
  return beginend if is_begin_inf ^! Infinity.infinity?(beginend[1])

  # Now, only one of them is a {RangeExtd::Infinity} type object.
  if is_begin_inf && beginend[1].respond_to?(:divmod)
    [_normalize_infinity_float_core(beginend[0]), beginend[1]] # "begin" is Infinity
  elsif beginend[0].respond_to?(:divmod)
    [beginend[0], _normalize_infinity_float_core(beginend[1])] # "end" is Infinity
  else
    beginend
  end
end
_normalize_infinity_float_core(inf) click to toggle source

@param inf [RangeExtd::Infinity] @return [RangeExtd::Infinity, Float] +/-Float::INFINITY if Float

# File lib/range_extd.rb, line 1008
def self._normalize_infinity_float_core(inf)
  msg = 'RangeExtd component of the RangeExtd::Infinity object replaced with Float::INFINITY.'
  warn msg if $DEBUG || $VERBOSE
  (inf.positive? ? 1 : -1) * Float::INFINITY
end

Public Instance Methods

==(r) click to toggle source

Like {Range}, returns true only if both of them are {Range} (or its subclasses), and in addition if both {#exclude_begin?} and {#exclude_end?} match (==) between the two objects. For the empty ranges they are somewhat different. In short, when both of them are empty and they belong to the same Class or have common ancestors (apart from Object and BasicObject, excluding all the included modules), this returns true, regardless of their boundary values. And any empty range is equal to RangeExtd::Infinity::NONE.

Note the last example will return false for #eql?

@example

(1...1)    == RangeExtd::NONE # => false (b/c the Range is invalid)
(1<...1)   == RangeExtd::NONE # => true
(?a<...?b) == RangeExtd::NONE # => true
(1<...1) == (2<...2)     # => true
(1<...1) == (3<...4)     # => true
(?a<...?b) == (?c<...?c) # => true
(1<...1) != (?c<...?c)   # - because of Fixnum and String
(1.0<...1.0) == (3<...4) # => true

@return [Boolean]

# File lib/range_extd.rb, line 236
def ==(r)
  _re_equal_core(r, :==)
end
===(obj) click to toggle source

If the object is open-ended to the negative (Infinity), this returns nil in default, unless the given object is Numeric (and comparable of Real), in which case this calls {#cover?}, or if self is {RangeExtd::ALL} and the object is Comparable.

In the standard Range, this checks whether the given object is a member, hence,

(?D..?z) === ?c    # => true
(?a..?z) === "cc"  # => false

In the case of the former, after finite trials of [#succ] from ?c, it reaches the end (?z). In the latter, after finit trials of [#succ] from the begin ?a, it reaches the end (?z). Therefore it is theoretically possible to prove it (n.b., the actual algorithm of built-in +Range#include?+ is different and cheating! See below.).

However, in the case of

(?D..Infinity) === ?c

it can never prove ?c is a member after infinite trials of [#succ], whether it starts the trials from the begin (?D) or the object (?c).

For anything but Numeric, use {#cover?} instead.

Note

(?B..?z) === 'dd'  # => false

as Ruby’s {Range} knows the algorithm of +String#succ+ and +String#<=>+ and specifically checks with it, before using +Enumerable#include?+. {github.com/ruby/ruby/blob/trunk/range.c}

Therefore, even if you change the definition of +String#succ+ so that ‘B’.succ => ‘dd’, ‘dd’.succ => ‘z’, as follows,

class String
  alias :succ_orig :succ
  def succ
    if self == 'B'
      'dd'
    elsif self == 'dd'
      'z'
    else
      :succ_orig
    end
  end
end

the resutl of +Range#===+ will unchange;

(?B..?z) === 'dd'  # => false
(?B..?z).to_a      # => ["B", "dd", "z"]

Similarly {Range} treats String differently;

(?X..?z).each do |i| print i;end  # => "XYZ[\]^_`abcdefghijklmnopqrstuvwxyz"
?Z.succ  # => 'AA'

@param [Object] obj If this Object is a member? @return [Boolean]

# File lib/range_extd.rb, line 311
def ===(obj)
  # ("a".."z")===("cc")       # => false

  return false if empty?   # n.b, NONE includes nothing, even NOWHERE (because of exclude_begin/end)

  rapart = _converted_rangepart
  beg = rapart.begin
  if beg.nil? && !beg.nowhere?
    return rapart.send(__method__, obj)
  end

  begin
    _ = 1.0+obj       # OK if Numeric.
    return cover?(obj)  # This excludes begin() if need be.
  rescue TypeError
  end

  # obj is not Numeric, hence runs brute-force check.
  beg = self.begin()
  if beg.respond_to?(:infinity?) && beg.infinity?
    return nil
    # raise TypeError "can't iterate from -Infinity"
  end

  each do |ei|  # This excludes begin() if need be.
    return true if ei == obj
  end
  false
end
Also aliased as: include?, member?
begin() click to toggle source

@return [Object]

# File lib/range_extd.rb, line 417
def begin()
  @rangepart.begin()
end
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)

+Range#bsearch+ works only with Integer and/or Float (as in Ruby 2.1), not even Rational (as in Ruby 3.1). If either of begin and end is a Float, the search is conducted in Float and the returned value will be a 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, {RangeExtd#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, {RangeExtd#bsearch} is identical to +Range#bsearch+.

# File lib/range_extd.rb, line 463
def bsearch(*rest, &bloc)
  if is_none? # No need of null?(), supposedly!
    raise TypeError, "can't do binary search for NONE range"
  end

  if @exclude_begin
    if ((Float === self.begin()) ||
        (Integer === self.begin()) && (Float === self.end()))
      #NOTE: Range#bsearch accepts Infinity, whether it makes sense or not.
      # if Infinity::FLOAT_INFINITY == self.begin()
      #   raise TypeError, "can't do binary search from -Infinity"
      # else
        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 (at Ruby 2.1), hence this code.
        #   Users should give a RangeExtd with begin being Rational.to_f in that case.
      # end
    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
    @rangepart.send(__method__, *rest, &bloc)
  end
end
count(*rest, &block) click to toggle source

This works without modification mostly.

Presumably because Enumerable#count is called and each is internally used. Exceptions are infinities and borderless (nil).

(5..).count             # => Float::INFINITY  # exceptional case
(..5).count             # => Float::INFINITY  # exceptional case
(..nil).count           # => Float::INFINITY  # exceptional case
(-Float::INFINITY..nil) # => Float::INFINITY  # exceptional case
(-Float::INFINITY..Float::INFINITY).count  # raises (TypeError) "can't iterate from Float"
(..5).count(4)          # raises (TypeError)
(..5).count{|i| i<3}    # raises (TypeError)
(1..).count{|i| i<3}    # infinite loop!

Here I define as another exceptional case:

RangeExtd::ALL.count    # => Float::INFINITY

@return [Integer]

Calls superclass method
# File lib/range_extd.rb, line 509
def count(*rest, &block)
  return Float::INFINITY if self == RangeExtd::ALL
  super
end
cover?(i) click to toggle source

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

# File lib/range_extd.rb, line 516
def cover?(i)
  # ("a".."z").cover?("cc")   # => true
  # (?B..?z).cover?('dd')     # => true  (though 'dd'.succ would never reach ?z)

  return false if empty?  # equivalent to null? in this case because self is alwasy true==valid?

  if @exclude_begin && self.begin == i
    false
  else
    @rangepart.send(__method__, i)
  end
end
each(*rest, &bloc) click to toggle source

slightly modified for {#exclude_begin?} being true

@raise [TypeError] If {#exclude_begin?} is true, and {#begin}() (+@rangepart+) does not have a method of [#succ], then even if no block is given, this method raises TypeError straightaway. @return [RangeExtd] self @return [Enumerator] if block is not given.

# File lib/range_extd.rb, line 535
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.

  _step_each_core(__method__, *rest, &bloc)
end
end() click to toggle source

@return [Object]

# File lib/range_extd.rb, line 422
def end()
  @rangepart.end()
end
equiv?(other) click to toggle source

Return true if self and the other are equivalent; if [#to_a] is defined, it is similar to

(self.to_a == other.to_a)

(though the ends are checked more rigorously), and if not, equivalent to

(self == other)

@example

RangeExtd(2...7,true).equiv?(3..6)     # => true
RangeExtd(2...7,true).equiv?(3..6.0)   # => false
RangeExtd(2...7,true).equiv?(3.0..6.0) # => false
RangeExtd(2...7,true).equiv?(3..6.5)   # => false
RangeExtd(2...7,true).equiv?(RangeExtd(2.0...7.0,true))   # => true
RangeExtd(2...7,true).equiv?(3...7.0)  # => true

@param other [Range, RangeExtd]

# File lib/range_extd.rb, line 359
def equiv?(other)
  # This routine is very similar to Range#equiv? except
  # exclude_begin? in this object is always defined, hence
  # a more thorough check is needed.

  t_or_f = (defined?(self.begin.succ) && defined?(other.begin.succ) && defined?(other.end) && defined?(other.exclude_end?))
  if ! t_or_f
    return(self == other)     # succ() for begin is not defined.
  else
    # Checking the begins.
    if defined?(other.exclude_begin?)
      other_excl_beg = other.exclude_begin?
    else
      other_excl_beg = false
    end

    if (self.begin == other.begin)
      if (exclude_begin? ^! other_excl_beg)
        # Pass
      else
        return false
      end
    else
      if (exclude_begin? ^! other_excl_beg)
        return false
      elsif (exclude_begin? && (self.begin.succ == other.begin)) ||
            (other_excl_beg && (self.begin == other.begin.succ))
        # Pass
      else
        return false
      end
    end       # if (self.begin == other.begin)      # else
        
    # Now, the begins agreed.  Checking the ends.
    if (self.end == other.end)
      if (exclude_end? ^! other.exclude_end?)
        return true
      else
        return false
      end
    else      # if (self.end == other.end)
      if (exclude_end? ^! other.exclude_end?)
        return false
        # elsif defined?(other.last) && (self.last(1) == other.last(1))       # Invalid for Ruby 1.8 or earlier     # This is not good - eg., in this case, (1..5.5).equiv?(1..5.4) would return true.
        
      #   return true
      elsif (      exclude_end? && defined?(other.end.succ) && (self.end == other.end.succ)) ||
            (other.exclude_end? && defined?( self.end.succ) && (self.end.succ == other.end))
        return true
      else
        return false
      end
    end       # if (self.end == other.end)
  end # if ! t_or_f
end
exclude_begin?() click to toggle source

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

# File lib/range_extd.rb, line 205
def exclude_begin?
  @exclude_begin
end
exclude_end?() click to toggle source

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

# File lib/range_extd.rb, line 210
def exclude_end?
  @exclude_end
end
first(*rest) click to toggle source

Like +Range#last+, if no argument is given, it behaves like {#begin}(), that is, it returns the initial value, regardless of {#exclude_begin?}.

If an argument is given (nb., acceptable since Ruby 1.9.2) when {#exclude_begin?} is true, it returns the array that starts from {#begin}().succ(), in the same way as +Range#last+ with {#exclude_end?} of true.

The default behaviours are:

(1...3.1).last    # => 3.1
(1...3.1).last(1) # => [3]
(1...3.0).last(1) # => [2]
(3.0..8).first(1) # raise: can't iterate from Float (TypeError)

@raise [TypeError] if the argument (Numeric) is given and if {#exclude_begin?} is true, yet if {#begin}().succ is not defined, or yet if {#is_none?} @raise [RangeError] “cannot get the first element of beginless range” as per Range. @raise [ArgumentError] if more than 1 arguments are specified (delegated to {Range}) @param rest [Integer] Optional. Must be non-negative. Consult +Range#first+ for detail. @return [Object] if no argument is given, equivalent to {#end}. @return [Array] if an argument is given.

# File lib/range_extd.rb, line 579
def first(*rest)
  if is_none?
    raise RangeError, "cannot get the first element of RangeExtd::NONE"
  end

  ran = _converted_rangepart(transform_to_nil: false, consider_exclude_begin: (1 == rest.size && exclude_begin?))
  ran.send(__method__, *rest)
end
hash(*rest) click to toggle source

Redefines the hash definition

Without re-definition, the hash value does NOT depend on {#exclude_begin?} presumably because the parent class method +Range#hash+ does not take it into account (of course).

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/range_extd.rb, line 597
def hash(*rest)
  if @exclude_begin
    @rangepart.send(__method__, *rest) - 1
  else
    @rangepart.send(__method__, *rest)
  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]

# File lib/range_extd.rb, line 609
def inspect
  re_inspect_core(__method__)
end
is_none?() click to toggle source

true if self is identical to {RangeExtd::NONE}.

Overwriting {Range#is_none?} This is different from {#==} method!

@example

RangeExtd(0,0,true,true).valid?    # => true
RangeExtd(0,0,true,true) == RangeExtd::NONE  # => true
RangeExtd(0,0,true,true).empty?    # => true
RangeExtd(0,0,true,true).is_none?  # => false
RangeExtd::NONE.is_none?     # => true
# File lib/range_extd.rb, line 199
def is_none?
  @is_none
end
last(*rest) click to toggle source

Updated version of +Range#last+, considering {#exclude_begin?}.

If either (let alone both) side of the edge is Infinity, you can not give an argument in practice, the number of the members of the returned array.

@raise [TypeError] If self.begin.succ is not defined, or if either side is Infinity. @return [Object] if no argument is given, equivalent to {#end}. @return [Array] if an argument is given.

# File lib/range_extd.rb, line 629
def last(*rest)
  if is_none?
    raise RangeError, "cannot get the last element of RangeExtd::NONE"
  end

  _converted_rangepart(transform_to_nil: false).send(__method__, *rest)
end
max(*rest, &bloc) click to toggle source

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

# File lib/range_extd.rb, line 704
def max(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end
max_by(*rest, &bloc) click to toggle source

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

# File lib/range_extd.rb, line 710
def max_by(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end
member?(obj)
Alias for: ===
min(*rest, &bloc) click to toggle source

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

# File lib/range_extd.rb, line 671
def min(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end
min_by(*rest, &bloc) click to toggle source

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

# File lib/range_extd.rb, line 677
def min_by(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end
minmax(*rest, &bloc) click to toggle source

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

# File lib/range_extd.rb, line 684
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!
  _re_min_max_core(__method__, *rest, &bloc)
end
minmax_by(*rest, &bloc) click to toggle source

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

# File lib/range_extd.rb, line 694
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!
  _re_min_max_core(__method__, *rest, &bloc)
end
size(*rest) click to toggle source

Implementation of +Range#size+ to this class.

It is essentially the same, but the behaviour when {#exclude_begin?} is true may not always be natural. See {#first} for the definition when {#exclude_begin?} is true.

+Range#size+ only works for Numeric ranges. And in +Range#size+, the value is calculated when the initial value is non-Integer, by stepping by 1.0 from the {#begin} value, and the returned value is an integer. For example,

(1.4..2.6).size == 2

because both 1.4 and 2.4 (== 1.4+1.0) are included in the Range.

That means you had better be careful with the uncertainty (error) of floating-point. For example, at least in an environment,

4.8 - 4.5   # => 0.2999999999999998
(2.5...4.5000000000000021).size  => 2
(2.8...4.8000000000000021).size  => 3
(2.8..4.8).size  => 3

In {RangeExtd#size}, the principle is the same. If the {#begin} value has the method [#succ] defined, the object is regarded to consist of discrete values. If not, it is a range with continuous elements. This dinstinguishment affects the behavious seriously in some cases when {#exclude_begin?} is true. For example, the following two cases may seem unnatural.

RangeExtd(1..5, true, true)      == RangeExtd(Rational(1,1), 5, true, true)
RangeExtd(1..5, true, true).size != RangeExtd(Rational(1,1), 5, true, true).size

Although those two objects are equal by [#==], they are different in nature, as far as {Range} and {RangeExtd} are concerned, and that is why they work differently;

RangeExtd(1..5, true, true).eql?(RangeExtd(Rational(1,1), 5, true, true))  # => false
RangeExtd(1..5, true, true).to_a      # => [2, 3, 4]
RangeExtd(1..5, true, true).to_a.size # => 3
RangeExtd(Rational(1,1)..5).to_a   # => TypeError

Also, the floating-point uncertainties in Float can more often be problematic; for example, in an environment,

4.4 - 2.4   # => 2.0000000000000004
4.8 - 2.8   # => 2.0
RangeExtd(2.4..4.4, true, true).size  # => 3
RangeExtd(2.8..4.8, true, true).size  # => 2

The last example is what you would naively expect, because both

2.8+a(lim a->0)  and  3.8+a(lim a->0) are

in the range whereas 4.8 is not in the range by definition, but not the example right above.

Ruby 2.6 Endless Range and Infinity.

Before RangeExtd Ver.1.1, if a {RangeExtd} object contains {RangeExtd::Infinity} objects for either begin or end, {#size} used to be always Float::INFINITY no matter what the other object is (except when the other object is also a {RangeExtd::Infinity} object). However, since the introduction of the endless Range in Ruby 2.6, Ruby returns as follows:

(5..).size  # => Float::INFINITY
(?a..).size # => nil

Accordingly, this class {RangeExtd} now behaves the same as Ruby (2.6 or later).

Similarly,

(Float::INFINITY..Float::INFINITY).size

has changed (I do not know in which Ruby version)! It used to be 0 (in Ruby-2.1). However, As of Ruby 2.6, it raises +FloatDomainError: NaN+ Again this class now follows Ruby’s default ({RangeExtd} Ver.1.0 or later).

@note When both ends n are the same INFINITY (of the same parity),

+(n..n).size+ used to be 0.  As of Ruby 2.6, it is FloatDomainError: NaN.
This routine follows what Ruby produces, depending on Ruby's version it is run on.

@see blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/49797 [ruby-list:49797] from matz for how +Range#size+ behaves (in Japanese).

@return [Integer] 0 if {RangeExtd::NONE} @return [Float] Float::INFINITY if either (or both) the end is infinity, regardless of the class of the elements. @return [nil] if the range is non-Numeric. @raise [FloatDomainError] (Infinity..Infinity).size (as in Ruby-3.1, though it used to be 0 in Ruby-2.1)

# File lib/range_extd.rb, line 796
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

  return 0 if is_none?        # No need of null?(), supposedly!
    
  if self.begin.nil? || self.end.nil?   # RangeExtd#begin/end can be nil only in Ruby-2.7+/2.6+
    # Behaves as Ruby does -
    #  Infinity::FLOAT_INFINITY for Numeric and nil, but nil for any other
    #  {#exclude_end?} does not matter.
    return (self.begin..self.end).size
  end

  rbeg = self.begin
  rend = self.end

  # Either or both sides are (general or Float) Infinity
  if RangeExtd::Infinity.infinite?(rbeg) || RangeExtd::Infinity.infinite?(rend)
    return @rangepart.send(__method__, *rest)  # delegates to {Range#size}
  end

  return @rangepart.send(__method__, *rest) if !exclude_begin?

  # Now, {#exclude_begin?} is true:
  begin
    _dummy = 1.0 + rbeg       # _dummy to suppress warning: possibly useless use of + in void context
  rescue TypeError
    # Non-Numeric
    if defined? rbeg.succ
      return Range.new(rbeg.succ, rend, exclude_end?).send(__method__, *rest) # => nil in Ruby 2.1+
    else
      return nil      # See the line above.
      # raise TypeError, "can't iterate from "+self.begin.class.name
    end
  end

  # Numeric
  if rbeg.respond_to? :succ
    Range.new(rbeg.succ, rend, exclude_end?).send(__method__, *rest)
  else
    size_no_exclude = Range.new(rbeg, rend).send(__method__, *rest)   # exclude_end? == true, ie., Range with both ends inclusinve.
    diff = self.end - self.begin
    if diff.to_i == diff              # Integer difference
      return size_no_exclude - 1      # At least exclude_begin?==true (so exclude_end? does not matter)
    else
      return size_no_exclude
    end
  end
end
step(*rest, &bloc) click to toggle source

See {#each}.

@raise [TypeError] If {#exclude_begin?} is true, and {#begin}() does not have the method [#succ], then even if no block is given, this method raises TypeError straightaway. @return [RangeExtd] self @return [Enumerator] if block is not given.

# File lib/range_extd.rb, line 860
def step(*rest, &bloc)
  _step_each_core(__method__, *rest, &bloc)
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]

# File lib/range_extd.rb, line 616
def to_s
  re_inspect_core(__method__)
end

Private Instance Methods

_converted_rangepart(consider_exclude_begin: true, transform_to_nil: true, raises: false) click to toggle source

Converts RangeExtd::Infinity to nil in @rangepart

@param consider_exclude_begin [Boolean] If true (Default), and if {#exclude_begin?} is true, the first element is ignored. Note the resultant Range may be invalid. @param transform_to_nil [Boolean] If true (Default), {RangeExtd::Infinity} objects are transformed into nil when appropriate (i.e., {RangeExtd::Infinity::NEGATIVE} should be {RangeExtd#begin} and not at the end, and vice versa). @param raises [Boolean] If true (Def: false), and if {#exclude_begin?} is true but [#succ] is not defined for {#begin}, this routine raises an Exception as per (almost) Ruby default. @return [Range]

# File lib/range_extd.rb, line 644
def _converted_rangepart(consider_exclude_begin: true, transform_to_nil: true, raises: false)
  rbeg = @rangepart.begin
  if consider_exclude_begin && exclude_begin?
    if rbeg.respond_to? :succ
      rbeg = rbeg.succ
    elsif raises
      if rbeg.nil?
        raise RangeError, "cannot get the first element of beginless range"
      elsif is_none?  # maybe empty?() in some cases?
        raise RangeError, "cannot get the first element of NONE range"
      else
        # This includes {RangeExtd::Infinity} class objects (RangeExtd::Infinity.infinity?(rbeg) == true) and Float::INFINITY.
        raise TypeError, "can't iterate from "+self.begin.class.name
      end
    end
  end

  rbeg = nil if RangeExtd::Infinity::NEGATIVE == rbeg && transform_to_nil
  rend = @rangepart.end
  rend = nil if RangeExtd::Infinity::POSITIVE == rend && transform_to_nil

  Range.new(rbeg, rend, exclude_end?)
end
_re_equal_core(r, method=:==) click to toggle source

Core routine for {#===}

@param [Object] r to compare. @param [Symbol] method of the method name.

# File lib/range_extd.rb, line 1294
def _re_equal_core(r, method=:==)
  return false if !r.respond_to? :exclude_end?  # Not Range family.
  return false if !r.respond_to? :empty?  # Not Range family.
  return false if !r.respond_to? :valid?  # Not Range family.

  return false if !r.valid?  # always returns false with an invalid Range; n.b., RangeExtd#valid? is always true, hence they cannot be identical (eql?).

  is_r_empty = r.empty?
  return !!is_r_empty if is_none?  # RangeExtd::NONE==(1<...1); n.b. "!!" is redundant because r must be valid.
  return true if empty? && r.respond_to?(:is_none?) && r.is_none?     # r is RangeExtd::NONE
  return false if !_both_same_nowhere_parity?(r)  # inconsistent nil, non-nil, NOWHERE combination

  if empty?   && is_r_empty
    (self.begin.class.ancestors - self.begin.class.included_modules - [Object, BasicObject]).each do |ec|
      if ec === r.begin
        return true   # (1.0<...1.0) == (2<...2)        # (Float<Numeric <-> Fixnum<Numeric) Yes!
      end
    end
    return false      #    (?a...?a) != (2<...2)   # (String <-> Numeric) No!
  end

  return false if !(self.exclude_end? ^! r.exclude_end?)

  # Neither self nor r is guaranteed to be RangeExtd::NONE
  is_nil_equal = _both_eqleql_nil?(r, method)

  if defined? r.exclude_begin? # r is RangeExtd
    (self.exclude_begin? ^! r.exclude_begin?) &&
      (self.exclude_end? ^! r.exclude_end?) &&
      (self.begin.send(method, r.begin) && self.end.send(method, r.end) || is_nil_equal)
  else 
    # r is Range
    if self.exclude_begin?
      false
    else
      is_nil_equal || @rangepart.send(method, r)      # Comparison as two Range-s.
    end
  end
end
_re_min_max_core(method, *rest, &bloc) click to toggle source

Core routine for {#min}, {#max}, {#minmax} etc. @param method [Symbol] of the method name. @param rest [Object]

# File lib/range_extd.rb, line 1338
def _re_min_max_core(method, *rest, &bloc)
  # (1...3.5).max     # => TypeError: cannot exclude non Integer end value
  if is_none? || self.begin.nil? && self.begin.nowhere? || self.end.nil? && self.end.nowhere?
    # In fact, Range#minmax etc should be modified to deal with RangeExtd::Nowhere::NOWHERE (it is treated as nil at the moment)...
    raise TypeError, "no meaningful range."
  end

  if !@exclude_begin ||
     [:max, :max_by].include?(method) ||
     self.begin.respond_to?(:succ) ||
     [:min_by, :minmax_by].include?(method) && !block_given?
    # For the last one, if Range starts from something uniterable, it returns
    # Enumerator (without errors); but once it is executed, it will raise TypeError
    # "can't iterate from Float" or RangeError "cannot get the minimum of beginless range"
    return _converted_rangepart(transform_to_nil: false).send(method, *rest, &bloc)
  end

  # Now @exclude_begin is true, "begin" is not discrete, and block is given (if _by).
  raise TypeError, "can't exclude non-iterable begin value" #+self.begin.to_s
end
_step_each_core(method, *rest, &bloc) click to toggle source

Core routine for {#each} and {#step}

@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 [RangeExtd] self @return [Enumerator] if block is not given.

# File lib/range_extd.rb, line 549
def _step_each_core(method, *rest, &bloc)
  raise TypeError, "can't iterate for NONE range" if is_none?

  if block_given?
    # when a block is given to {#each}, self should be returned.
    _converted_rangepart(consider_exclude_begin: true, raises: true ).send(method, *rest, &bloc)
    self
  else
    _converted_rangepart(consider_exclude_begin: true, raises: false).send(method, *rest)
  end
end
re_inspect_core(method) click to toggle source

Core routine for {#inspect} and {#to_s} @param [Symbol] method the method name.

# File lib/range_extd.rb, line 1239
def re_inspect_core(method)
  # 0. prefix
  # 1. begin-inclusive
  # 2. begin-exclusive
  # 3. middle-string to bridge both ends
  # 4. end-exclusive
  # 5. end-inclusive
  # 6. postfix

  midStr = ''
  if @exclude_begin
    midStr += @@middle_strings[2]
  else
    midStr += @@middle_strings[1]
  end
  midStr += @@middle_strings[3]
  if @exclude_end
    midStr += @@middle_strings[4]
  else
    midStr += @@middle_strings[5]
  end

  if is_none?
    strBegin = 'Null'         # Null<...Null
    strEnd   = 'Null'
  else
    strBegin = self.begin.send(method)
    strEnd   = self.end.send(method)
  end

  @@middle_strings[0] + strBegin + midStr + strEnd + @@middle_strings[6]
end
re_inspect_core_orig(method) click to toggle source

Core routine for {#inspect} and {#to_s} @param [Symbol] method the method name.

# File lib/range_extd.rb, line 1274
def re_inspect_core_orig(method)
  if @exclude_end
    midStr = "..."
  else
    midStr = ".."
  end
  if is_none?
    'Null' + '<' + midStr + 'Null'    # Null<...Null
  elsif @exclude_begin
    self.begin.send(method) + '<' + midStr + self.end.send(method)
  else
    self.begin.send(method) + midStr + self.end.send(method)
  end
end