Module: FatCore::Range

Includes:
Comparable
Included in:
Range
Defined in:
lib/fat_core/range.rb

Defined Under Namespace

Modules: ClassMethods

Operations collapse

Queries collapse

Sorting collapse

Class Method Details

.included(base) ⇒ Object



461
462
463
# File 'lib/fat_core/range.rb', line 461

def self.included(base)
  base.extend(ClassMethods)
end

Instance Method Details

#<=>(other) ⇒ Integer, ...

Compare this range with other first by min values, then by max values.

This causes a sort of Ranges with Comparable elements to sort from left to right on the number line, then for Ranges that start on the same number, from smallest to largest.

Examples:

(4..8) <=> (5..7) #=> -1
(4..8) <=> (4..7) #=> 1
(4..8) <=> (4..8) #=> 0

Parameters:

  • other (Range)

    range to compare self with

Returns:

  • (Integer, -1, 0, 1)

    if self is less, equal, or greater than other



427
428
429
# File 'lib/fat_core/range.rb', line 427

def <=>(other)
  [min, max] <=> other.minmax
end

#compatible?(ranges) ⇒ Boolean

Returns:

  • (Boolean)


431
432
433
434
435
436
437
438
439
# File 'lib/fat_core/range.rb', line 431

def compatible?(ranges)
  numeric_ok = min.is_a?(Numeric) && max.is_a?(Numeric)
  if numeric_ok
    ranges.map.all? { |r| r.min.is_a?(Numeric) && r.max.is_a?(Numeric) }
  else
    self_class = min.class
    ranges.map.all? { |r| r.min.is_a?(self_class) && r.max.is_a?(self_class) }
  end
end

#contiguous?(other) ⇒ Boolean

Is self contiguous to other either on the left or on the right? First, the two ranges are sorted by their min values, and the range with the lowest min value is considered to be on the "left" and the other range on the "right". Whether one range is "contiguous" to another then has two cases:

  1. If the max element of the Range on the left respond to the #succ method (that is, its value is a discrete value such as Integer or Date) test whether the succ to the max value of the Range on the left is equal to the min value of the Range on the right.
  2. If the max element of the Range on the left does not respond to the #succ method (that is, its values are continuous values such as Floats) test whether the max value of the Range on the left is equal to the min value of the Range on the right

Examples:

(0..10).contiguous?((11..20))           #=> true
(11..20).contiguous?((0..10))           #=> true, right_contiguous
(0..10).contiguous?((15..20))           #=> false
(3.145..12.3).contiguous?((0.5..3.145)) #=> true
(3.146..12.3).contiguous?((0.5..3.145)) #=> false

Parameters:

  • other (Range)

    other range to test for contiguity

Returns:

  • (Boolean)

    is self contiguous with other



318
319
320
# File 'lib/fat_core/range.rb', line 318

def contiguous?(other)
  left_contiguous?(other) || right_contiguous?(other)
end

#difference(other) ⇒ Array<Range> Also known as: -

The difference method, -, removes the overlapping part of the other argument from self. Because in the case where self is a superset of the other range, this will result in the difference being two non-contiguous ranges, this returns an array of ranges. If there is no overlap or if self is a subset of the other range, return an array of self

Parameters:

  • other (Range)

    the Range whose overlap is removed from self

Returns:

  • (Array<Range>)

    the Ranges representing self with other removed



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/fat_core/range.rb', line 189

def difference(other)
  unless max.respond_to?(:succ) && min.respond_to?(:pred) &&
         other.max.respond_to?(:succ) && other.min.respond_to?(:pred)
    raise 'Range difference requires objects have pred and succ methods'
  end

  # Remove the intersection of self and other from self.  The intersection
  # is either (a) empty, so return self, (b) coincides with self, so
  # return nothing,
  isec = self & other
  return [self] if isec.nil?
  return [] if isec == self

  # (c) touches self on the right, (d) touches self on the left, or (e)
  # touches on neither the left or right, in which case the difference is
  # two ranges.
  if isec.max == max && isec.min > min
    # Return the part to the left of isec
    [(min..isec.min.pred)]
  elsif isec.min == min && isec.max < max
    # Return the part to the right of isec
    [(isec.max.succ..max)]
  else
    # Return the parts to the left and right of isec
    [(min..isec.min.pred), (isec.max.succ..max)]
  end
end

#gaps(ranges) ⇒ Array<Range>

If this range is not spanned by the ranges collectively, return an Array of ranges representing the gaps in coverage. The ranges can over-cover this range on the left or right without affecting the result, that is, each range in the returned array of gap ranges will always be subsets of this range.

If the ranges span this range, return an empty array.

Examples:

(0..10).gaps([(0..3), (5..6), (9..10)])  #=> [(4..4), (7..8)]
(0..10).gaps([(-4..3), (5..6), (9..15)]) #=> [(4..4), (7..8)]
(0..10).gaps([(-4..3), (4..6), (7..15)]) #=> [] ranges span this one
(0..10).gaps([(-4..-3), (11..16), (17..25)]) #=> [(0..10)] no overlap
(0..10).gaps([])                             #=> [(0..10)] no overlap

Parameters:

Returns:

Raises:

  • (ArgumentError)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/fat_core/range.rb', line 60

def gaps(ranges)
  return [clone] if ranges.empty?

  msg = "#{ranges.first.min.class} range incompatible with #{min.class} Range"
  raise ArgumentError, msg unless compatible?(ranges)

  return [] if spanned_by?(ranges)

  ranges = ranges.select { |r| r.overlaps?(self) }.sort
  self_is_continuous = ranges.map(&:minmax).flatten.any? { |p| !p.respond_to?(:succ) }
  gaps = []
  cur_point = min
  ranges.each do |rr|
    # Loop Invariant: cur_point is the last element in the self Range
    # NOT covered by the given ranges or the gaps so far.
    break if cur_point == max

    if (self_is_continuous && rr.min > cur_point) ||
       (!self_is_continuous && rr.min > cur_point)
      # There is a gap between the cur_point within self and the start
      # of this range, rr, so we need to record it.
      start_point = cur_point
      end_point = self_is_continuous ? rr.min : rr.min.pred
      gaps << (start_point..end_point)
    end
    cur_point =
      if rr.max.is_a?(String) && rr.max[-1].match?(/[Zz9]/)
        # This is a real kludge that stems from the fact that 'z'.succ <
        # 'z', so the test for gaps at the end of the self range
        # believes there is a gap when there is none.  This ensures that
        # cur_point is set to something > rr.max when it is one of the
        # problematic strings ending in 'Z', 'z', or '9', all of whose
        # successors sort less than them.
        rr.max + rr.max[-1]
      else
        self_is_continuous ? rr.max : rr.max.succ
      end
  end
  # Add any gap between the last of the ranges and the end of self.
  if cur_point <= max
    gaps << (cur_point..max)
  end
  gaps
end

#intersection(other) ⇒ Range? Also known as: &

Return a Range that represents the intersection between this range and the other range. If there is no intersection, return nil.

Examples:

(0..10) & (5..20)             #=> (5..10)
(0..10).intersection((5..20)) #=> (5..10)
(0..10) & (15..20)            #=> nil

Parameters:

  • other (Range)

    the Range self is intersected with

Returns:

  • (Range, nil)

    a Range representing the intersection



156
157
158
159
160
# File 'lib/fat_core/range.rb', line 156

def intersection(other)
  return unless overlaps?(other)

  ([min, other.min].max..[max, other.max].min)
end

#join(other) ⇒ Range?

Return a range that concatenates this range with other if it is contiguous with this range on the left or right; return nil if the ranges are not contiguous.

Examples:

(0..3).join(4..8) #=> (0..8)

Parameters:

  • other (Range)

    the Range to join to this range

Returns:

  • (Range, nil)

    this range joined to other

See Also:



35
36
37
38
39
40
41
# File 'lib/fat_core/range.rb', line 35

def join(other)
  if left_contiguous?(other)
    ::Range.new(other.min, max)
  elsif right_contiguous?(other)
    ::Range.new(min, other.max)
  end
end

#left_contiguous?(other) ⇒ Boolean

Is other on the left of and contiguous to self? Whether one range is "contiguous" to another has two cases:

  1. If the elements of the Range on the left respond to the #succ method (that is, its values are discrete values such as Integers or Dates) test whether the succ to the max value of the Range on the left is equal to the min value of the Range on the right.
  2. If the elements of the Range on the left do not respond to the #succ method (that is, its values are continuous values such as Floats) test whether the max value of the Range on the left is equal to the min value of the Range on the right

Examples:

(0..10).left_contiguous((11..20))           #=> true
(11..20).left_contiguous((0..10))           #=> false, but right_contiguous
(0.5..3.145).left_contiguous((3.145..18.4)) #=> true
(0.5..3.145).left_contiguous((3.146..18.4)) #=> false

Parameters:

  • other (Range)

    other range to test for contiguity

Returns:

  • (Boolean)

    is self left_contiguous with other



259
260
261
262
263
264
265
# File 'lib/fat_core/range.rb', line 259

def left_contiguous?(other)
  if other.max.respond_to?(:succ)
    other.max.succ == min
  else
    other.max == min
  end
end

#overlaps(ranges) ⇒ Array<Range>

Within this range return an Array of Ranges representing the overlaps among the given Array of Ranges ranges. If there are no overlaps, return an empty array. Don't consider overlaps in the ranges that occur outside of self.

Examples:

(0..10).overlaps([(-4..4), (2..7), (5..12)]) => [(2..4), (5..7)]

Parameters:

  • ranges (Array<Range>)

    ranges to search for overlaps

Returns:

  • (Array<Range>)

    overlaps with ranges but inside this Range

Raises:

  • (ArgumentError)


115
116
117
118
119
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
# File 'lib/fat_core/range.rb', line 115

def overlaps(ranges)
  return [] if ranges.empty?

  msg = "#{ranges.first.min.class} range incompatible with #{min.class} Range"
  raise ArgumentError, msg unless compatible?(ranges)

  return [] if spanned_by?(ranges)

  ranges = ranges.sort_by(&:min)
  overlaps = []
  cur_point = nil
  ranges.each do |rr|
    # Skip ranges outside of self
    next if rr.max < min || rr.min > max

    # Initialize cur_point to max of first range
    if cur_point.nil?
      cur_point = rr.max
      next
    end
    # We are on the second or later range
    if rr.min < cur_point
      start_point = rr.min
      end_point = cur_point
      overlaps << (start_point..end_point)
    end
    cur_point = rr.max
  end
  overlaps
end

#overlaps?(other) ⇒ Boolean

Return whether self overlaps with other Range.

Parameters:

  • other (Range)

    range to test for overlap with self

Returns:

  • (Boolean)

    is there an overlap?



364
365
366
367
# File 'lib/fat_core/range.rb', line 364

def overlaps?(other)
  cover?(other.min) || cover?(other.max) ||
    other.cover?(min) || other.cover?(max)
end

#overlaps_among?(ranges) ⇒ Boolean

Return whether any of the ranges that overlap self have overlaps among one another.

This does the same thing as Range.overlaps_among?, except that it filters the ranges to only those overlapping self before testing for overlaps among them.

Parameters:

  • ranges (Array<Range>)

    ranges to test for overlaps

Returns:

  • (Boolean)

    were there overlaps among ranges?



378
379
380
381
# File 'lib/fat_core/range.rb', line 378

def overlaps_among?(ranges)
  iranges = ranges.select { |r| overlaps?(r) }
  ::Range.overlaps_among?(iranges)
end

#proper_subset_of?(other) ⇒ Boolean

Return whether self is contained within other range, with at most one boundary touching.

Parameters:

  • other (Range)

    the containing range

Returns:

  • (Boolean)

    is self wholly within other



336
337
338
339
# File 'lib/fat_core/range.rb', line 336

def proper_subset_of?(other)
  subset_of?(other) &&
    (min > other.min || max < other.max)
end

#proper_superset_of?(other) ⇒ Boolean

Return whether self contains other range, with at most one boundary touching.

Parameters:

  • other (Range)

    the contained range

Returns:

  • (Boolean)

    does self wholly contain other



355
356
357
358
# File 'lib/fat_core/range.rb', line 355

def proper_superset_of?(other)
  superset_of?(other) &&
    (min < other.min || max > other.max)
end

#right_contiguous?(other) ⇒ Boolean

Is other on the right of and contiguous to self? Whether one range is "contiguous" to another has two cases:

  1. If the elements of the Range on the left respond to the #succ method (that is, its values are discrete values such as Integers or Dates) test whether the succ to the max value of the Range on the left is equal to the min value of the Range on the right.
  2. If the elements of the Range on the left do not respond to the #succ method (that is, its values are continuous values such as Floats) test whether the max value of the Range on the left is equal to the min value of the Range on the right

Examples:

(11..20).right_contiguous((0..10))           #=> true
(0..10).right_contiguous((11..20))           #=> false, but left_contiguous
(3.145..12.3).right_contiguous((0.5..3.145)) #=> true
(3.146..12.3).right_contiguous((0.5..3.145)) #=> false

Parameters:

  • other (Range)

    other range to test for contiguity

Returns:

  • (Boolean)

    is self right_contiguous with other



287
288
289
290
291
292
293
# File 'lib/fat_core/range.rb', line 287

def right_contiguous?(other)
  if max.respond_to?(:succ)
    max.succ == other.min
  else
    max == other.min
  end
end

#spanned_by?(ranges) ⇒ Boolean

Return true if the given ranges collectively cover this range without overlaps and without gaps.

Parameters:

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/fat_core/range.rb', line 388

def spanned_by?(ranges)
  return empty? if ranges.empty?

  msg = "#{ranges.first.min.class} range incompatible with #{min.class} Range"
  raise ArgumentError, msg unless compatible?(ranges)

  joined_range = nil
  ranges.sort.each do |r|
    unless joined_range
      joined_range = r
      next
    end
    joined_range = joined_range.join(r)
    break if joined_range.nil?
  end
  if joined_range.nil?
    false
  else
    joined_range.min <= min && joined_range.max >= max
  end
end

#subset_of?(other) ⇒ Boolean

Return whether self is contained within other range, even if their boundaries touch.

Parameters:

  • other (Range)

    the containing range

Returns:

  • (Boolean)

    is self within other



327
328
329
# File 'lib/fat_core/range.rb', line 327

def subset_of?(other)
  min >= other.min && max <= other.max
end

#superset_of?(other) ⇒ Boolean

Return whether self contains other range, even if their boundaries touch.

Parameters:

  • other (Range)

    the contained range

Returns:

  • (Boolean)

    does self contain other



346
347
348
# File 'lib/fat_core/range.rb', line 346

def superset_of?(other)
  min <= other.min && max >= other.max
end

#tex_quoteString

Allow erb or erubis documents to directly interpolate a Range.

Returns:



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/fat_core/range.rb', line 221

def tex_quote
  minq =
    if min.respond_to?(:tex_quote)
      min.tex_quote
    else
      min.to_s
    end
  maxq =
    if max.respond_to?(:tex_quote)
      max.tex_quote
    else
      max.to_s
    end
  "(#{minq}..#{maxq})"
end

#union(other) ⇒ Range? Also known as: +

Return a Range that represents the union between this range and the other range. If there is no overlap and self is not contiguous with other, return nil.

Examples:

(0..10) + (5..20)       #=> (0..20)
(0..10).union((5..20))  #=> (0..20)
(0..10) + (15..20)      #=> nil

Parameters:

  • other (Range)

    the Range self is union-ed with

Returns:

  • (Range, nil)

    a Range representing the union



174
175
176
177
178
# File 'lib/fat_core/range.rb', line 174

def union(other)
  return unless overlaps?(other) || contiguous?(other)

  ([min, other.min].min..[max, other.max].max)
end