Class: RangeExtd

Inherits:
Range show all
Defined in:
lib/range_extd.rb,
lib/range_extd/nowhere.rb,
lib/range_extd/infinity.rb

Overview

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: NONE representing an empty range and ALL representing the entire range, both in the abstract sense.

Examples:

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 

Defined Under Namespace

Classes: Infinity, Nowhere

Constant Summary collapse

NONE =

Constant to represent no RangeExtd. Note that (3…3).valid?, for example, is false under this scheme, and it should be represented with this constant.

:Abstract
ALL =

Constant to represent a general RangeExtd that include everything (negative to positive infinities). This is basically a generalized version of range of (-Float::INFINITY..Float::INFINITY) to any (comparable) Class objects.

:Abstract
@@middle_strings =

To conrol how the RangeExtd should be displayed or set (in one form). It can be read and reset by middle_strings and middle_strings= Default is [”, ”, ‘<’, ‘..’, ‘.’, ”, ”]

[]

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Range

#empty?, #equal_prerangeextd?, #equiv_all?, #is_all?, #null?, #size_prerangeextd?, #valid?

Constructor Details

#new(range, [exclude_begin = false, [exclude_end=false]], opts) ⇒ RangeExtd #new(obj_begin, obj_end, [exclude_begin = false, [exclude_end=false]], opts) ⇒ RangeExtd #new(obj_begin, string_form, obj_end, [exclude_begin = false, [exclude_end=false]], opts) ⇒ RangeExtd

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.

Note if you use the third form with “string_form” with the user-defined string (via 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 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.

Examples:

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)

Overloads:

  • #new(range, [exclude_begin = false, [exclude_end=false]], opts) ⇒ RangeExtd

    Parameters:

    • range (Object)

      Instance of Range or its subclasses, including RangeExtd

    • exclude_begin (Boolean)

      If specified, this has the higher priority, or false in default.

    • exclude_end (Boolean)

      If specified, this has the higher priority, or false in default.

    Options Hash (opts):

    • :exclude_begin (Boolean)

      If specified, this has the highest priority, or false in default.

    • :exclude_end (Boolean)

      If specified, this has the highest priority, or false in default.

  • #new(obj_begin, obj_end, [exclude_begin = false, [exclude_end=false]], opts) ⇒ RangeExtd

    Parameters:

    • obj_begin (Object)

      Any object that is Comparable with end

    • obj_end (Object)

      Any object that is Comparable with begin

    • exclude_begin (Boolean)

      If specified, this has the lower priority, or false in default.

    • exclude_end (Boolean)

      If specified, this has the lower priority, or false in default.

    Options Hash (opts):

    • :exclude_begin (Boolean)

      If specified, this has the higher priority, or false in default.

    • :exclude_end (Boolean)

      If specified, this has the higher priority, or false in default.

  • #new(obj_begin, string_form, obj_end, [exclude_begin = false, [exclude_end=false]], opts) ⇒ RangeExtd

    Parameters:

    • obj_begin (Object)

      Any object that is Comparable with end

    • string_form (Object)

      String form (without pre/postfix) of range expression set by middle_strings=()

    • obj_end (Object)

      Any object that is Comparable with begin

    • exclude_begin (Boolean)

      If specified, this has the lower priority, or false in default.

    • exclude_end (Boolean)

      If specified, this has the lower priority, or false in default.

    Options Hash (opts):

    • :exclude_begin (Boolean)

      If specified, this has the higher priority, or false in default.

    • :exclude_end (Boolean)

      If specified, this has the higher priority, or false in default.

Raises:

  • (ArgumentError)

    particularly if the range to be created is not Range#valid?.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/range_extd.rb', line 120

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

Class Method Details

.middle_stringsArray<String>

See RangExtd.middle_strings=() for detail.

Returns:

  • (Array<String>)


1228
1229
1230
# File 'lib/range_extd.rb', line 1228

def self.middle_strings()
  @@middle_strings
end

.middle_strings=(ary) ⇒ Array, Symbol

Set the class variable to be used in #to_s and #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', '<', '<=', ''] )

Examples:

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]"

Parameters:

  • ary (Array, Symbol)

Returns:

  • (Array, Symbol)


1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
# File 'lib/range_extd.rb', line 1202

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

.valid?(range, [exclude_begin = false, [exclude_end=false]]) ⇒ Boolean .valid?(obj_begin, obj_end, [exclude_begin = false, [exclude_end=false]]) ⇒ Boolean

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.

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 new, hence for any instance of RangeExtd class, its Range#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 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 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 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 NONE.

  6. If either or both of #begin and #end is RangeExtd::Nowhere::NOWHERE, the range has to be 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.

Examples:


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

Overloads:

  • .valid?(range, [exclude_begin = false, [exclude_end=false]]) ⇒ Boolean

    Parameters:

    • range (Object)

      Instance of Range or its subclasses, including RangeExtd

    • exclude_begin (Boolean)

      If specified, this has the higher priority, or false in default.

    • exclude_end (Boolean)

      If specified, this has the higher priority, or false in default.

  • .valid?(obj_begin, obj_end, [exclude_begin = false, [exclude_end=false]]) ⇒ Boolean

    Parameters:

    • obj_begin (Object)

      Any object that is Comparable with end

    • obj_end (Object)

      Any object that is Comparable with begin (or nil, for Ruby 2.6 onwards)

    • exclude_begin (Boolean)

      If specified, this has the lower priority, or false in default.

    • exclude_end (Boolean)

      If specified, this has the lower priority, or false in default.

Returns:

  • (Boolean)


1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
# File 'lib/range_extd.rb', line 1099

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

Instance Method Details

#==(r) ⇒ Boolean

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?

Examples:

(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

Returns:

  • (Boolean)


237
238
239
# File 'lib/range_extd.rb', line 237

def ==(r)
  _re_equal_core(r, :==)
end

#===(obj) ⇒ Boolean Also known as: include?, member?

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 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?. https://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'

Parameters:

  • obj (Object)

    If this Object is a member?

Returns:

  • (Boolean)


312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/range_extd.rb', line 312

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

#beginObject

Returns:



418
419
420
# File 'lib/range_extd.rb', line 418

def begin()
  @rangepart.begin()
end

#bsearch(*rest, &bloc) ⇒ Object

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, #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, #bsearch is identical to Range#bsearch.



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/range_extd.rb', line 464

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) ⇒ Integer

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

Returns:



510
511
512
513
# File 'lib/range_extd.rb', line 510

def count(*rest, &block)
  return Float::INFINITY if self == RangeExtd::ALL
  super
end

#cover?(i) ⇒ Boolean

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

Returns:

  • (Boolean)


517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/range_extd.rb', line 517

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) ⇒ RangeExtd, Enumerator

slightly modified for #exclude_begin? being true

Returns:

  • (RangeExtd)

    self

  • (Enumerator)

    if block is not given.

Raises:

  • (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.



536
537
538
539
540
541
542
543
# File 'lib/range_extd.rb', line 536

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

#endObject

Returns:



423
424
425
# File 'lib/range_extd.rb', line 423

def end()
  @rangepart.end()
end

#equiv?(other) ⇒ Boolean

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)

Examples:

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

Parameters:

Returns:

  • (Boolean)


360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/range_extd.rb', line 360

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?Boolean

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

Returns:

  • (Boolean)


206
207
208
# File 'lib/range_extd.rb', line 206

def exclude_begin?
  @exclude_begin
end

#exclude_end?Boolean

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

Returns:

  • (Boolean)


211
212
213
# File 'lib/range_extd.rb', line 211

def exclude_end?
  @exclude_end
end

#first(*rest) ⇒ Object, Array

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)

Parameters:

  • rest (Integer)

    Optional. Must be non-negative. Consult Range#first for detail.

Returns:

  • (Object)

    if no argument is given, equivalent to #end.

  • (Array)

    if an argument is given.

Raises:

  • (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?

  • (RangeError)

    “cannot get the first element of beginless range” as per Range.

  • (ArgumentError)

    if more than 1 arguments are specified (delegated to Range)



580
581
582
583
584
585
586
587
# File 'lib/range_extd.rb', line 580

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) ⇒ Object

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.



598
599
600
601
602
603
604
# File 'lib/range_extd.rb', line 598

def hash(*rest)
  if @exclude_begin
    @rangepart.send(__method__, *rest) - 1
  else
    @rangepart.send(__method__, *rest)
  end
end

#inspectString

Return eg., ‘(“a”<…“c”)’, ‘(“a”<..“c”)’, if #exclude_begin? is true, or else, identical to those for Range.

Returns:

  • (String)


610
611
612
# File 'lib/range_extd.rb', line 610

def inspect
  re_inspect_core(__method__)
end

#is_none?Boolean

true if self is identical to NONE.

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

Examples:

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

Returns:

  • (Boolean)


200
201
202
# File 'lib/range_extd.rb', line 200

def is_none?
  @is_none
end

#last(*rest) ⇒ Object, Array

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.

Returns:

  • (Object)

    if no argument is given, equivalent to #end.

  • (Array)

    if an argument is given.

Raises:

  • (TypeError)

    If self.begin.succ is not defined, or if either side is Infinity.



630
631
632
633
634
635
636
# File 'lib/range_extd.rb', line 630

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) ⇒ Object

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



705
706
707
# File 'lib/range_extd.rb', line 705

def max(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end

#max_by(*rest, &bloc) ⇒ Object

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



711
712
713
# File 'lib/range_extd.rb', line 711

def max_by(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end

#min(*rest, &bloc) ⇒ Object

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



672
673
674
# File 'lib/range_extd.rb', line 672

def min(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end

#min_by(*rest, &bloc) ⇒ Object

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



678
679
680
# File 'lib/range_extd.rb', line 678

def min_by(*rest, &bloc)
  _re_min_max_core(__method__, *rest, &bloc)
end

#minmax(*rest, &bloc) ⇒ Object

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



685
686
687
688
689
690
691
# File 'lib/range_extd.rb', line 685

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) ⇒ Object

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



695
696
697
698
699
700
# File 'lib/range_extd.rb', line 695

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) ⇒ Integer, ...

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.

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

Returns:

  • (Integer)

    0 if NONE

  • (Float)

    Float::INFINITY if either (or both) the end is infinity, regardless of the class of the elements.

  • (nil)

    if the range is non-Numeric.

Raises:

  • (FloatDomainError)

    (Infinity..Infinity).size (as in Ruby-3.1, though it used to be 0 in Ruby-2.1)

See Also:



797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
# File 'lib/range_extd.rb', line 797

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) ⇒ RangeExtd, Enumerator

See #each.

Returns:

  • (RangeExtd)

    self

  • (Enumerator)

    if block is not given.

Raises:

  • (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.



861
862
863
# File 'lib/range_extd.rb', line 861

def step(*rest, &bloc)
  _step_each_core(__method__, *rest, &bloc)
end

#to_sString

Return eg., “(a<…c)”, “(a<..c)”, if #exclude_begin? is true, or else, identical to those for Range.

Returns:

  • (String)


617
618
619
# File 'lib/range_extd.rb', line 617

def to_s
  re_inspect_core(__method__)
end