Class: RangeExtd
- 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.
Defined Under Namespace
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
-
.middle_strings ⇒ Array<String>
See RangExtd.middle_strings=() for detail.
- .middle_strings=(ary) ⇒ Array, Symbol
-
.valid?(*inar) ⇒ Boolean
Returns true if the range to be constructed (or given) is valid, as a range, accepted in RangeExtd.
Instance Method Summary collapse
-
#==(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.
- #===(obj) ⇒ Boolean (also: #include?, #member?)
- #begin ⇒ Object
-
#bsearch(*rest, &bloc) ⇒ Object
bsearch is internally implemented by converting a float into 64-bit integer.
-
#count(*rest, &block) ⇒ Integer
This works without modification mostly.
- #cover?(i) ⇒ Boolean
-
#each(*rest, &bloc) ⇒ RangeExtd, Enumerator
slightly modified for #exclude_begin? being true.
- #end ⇒ Object
-
#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).
-
#exclude_begin? ⇒ Boolean
Returns true if the “begin” boundary is excluded, or false otherwise.
-
#exclude_end? ⇒ Boolean
Returns true if the “end” boundary is excluded, or false otherwise.
-
#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?.
-
#hash(*rest) ⇒ Object
Redefines the hash definition.
-
#initialize(*inar, **hsopt) ⇒ RangeExtd
constructor
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.
-
#inspect ⇒ String
Return eg., ‘(“a”<…“c”)’, ‘(“a”<..“c”)’, if #exclude_begin? is true, or else, identical to those for Range.
-
#is_none? ⇒ Boolean
true if self is identical to NONE.
-
#last(*rest) ⇒ Object, Array
Updated version of Range#last, considering #exclude_begin?.
-
#max(*rest, &bloc) ⇒ Object
See #first for the definition when #exclude_begin? is true.
-
#max_by(*rest, &bloc) ⇒ Object
See #first for the definition when #exclude_begin? is true.
-
#min(*rest, &bloc) ⇒ Object
See #first for the definition when #exclude_begin? is true.
-
#min_by(*rest, &bloc) ⇒ Object
See #first for the definition when #exclude_begin? is true.
-
#minmax(*rest, &bloc) ⇒ Object
See #first for the definition when #exclude_begin? is true.
-
#minmax_by(*rest, &bloc) ⇒ Object
See #first for the definition when #exclude_begin? is true.
-
#size(*rest) ⇒ Integer, ...
Implementation of Range#size to this class.
-
#step(*rest, &bloc) ⇒ RangeExtd, Enumerator
See #each.
-
#to_s ⇒ String
Return eg., “(a<…c)”, “(a<..c)”, if #exclude_begin? is true, or else, identical to those for Range.
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
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.
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_strings ⇒ Array<String>
See RangExtd.middle_strings=() for detail.
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:
-
prefix
-
begin-inclusive
-
begin-exclusive
-
middle-string to bridge both ends
-
end-exclusive
-
end-inclusive
-
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', '<', '<=', ''] )
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
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:
-
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).
-
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. -
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.
-
#begin must be smaller than or equal to #end, that is, (#begin <=> #end) must be either -1 or 0.
-
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 offalse(namely, inclusive) for #begin, whereas RangeExtd(1…1, true) is valid and equal (+==+) to NONE. -
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.
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?
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'
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 |
#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
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
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
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 |
#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)
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.
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.
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)
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 |
#inspect ⇒ String
Return eg., ‘(“a”<…“c”)’, ‘(“a”<..“c”)’, if #exclude_begin? is true, or else, identical to those for Range.
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!
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.
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, ...
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).
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.
861 862 863 |
# File 'lib/range_extd.rb', line 861 def step(*rest, &bloc) _step_each_core(__method__, *rest, &bloc) end |
#to_s ⇒ String
Return eg., “(a<…c)”, “(a<..c)”, if #exclude_begin? is true, or else, identical to those for Range.
617 618 619 |
# File 'lib/range_extd.rb', line 617 def to_s re_inspect_core(__method__) end |