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

Instance Method Details

#<=>(other) ⇒ -1, ...

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


367
368
369
# File 'lib/fat_core/range.rb', line 367

def <=>(other)
  [min, max] <=> [other.min, other.max]
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


265
266
267
# File 'lib/fat_core/range.rb', line 265

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

#difference(other) ⇒ Object 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



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/fat_core/range.rb', line 154

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
  if subset_of?(other)
    # (4..7) - (0..10)
    []
  elsif proper_superset_of?(other)
    # (4..7) - (5..5) -> [(4..4), (6..7)]
    [(min..other.min.pred), (other.max.succ..max)]
  elsif overlaps?(other) && other.min <= min
    # (4..7) - (2..5) -> (6..7)
    [(other.max.succ..max)]
  elsif overlaps?(other) && other.max >= max
    # (4..7) - (6..10) -> (4..5)
    [(min..other.min.pred)]
  else
    [self]
  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


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/fat_core/range.rb', line 53

def gaps(ranges)
  if ranges.empty?
    [clone]
  elsif spanned_by?(ranges)
    []
  else
    # TODO: does not work unless min and max respond to :succ
    ranges = ranges.sort_by(&:min)
    gaps = []
    cur_point = min
    ranges.each do |rr|
      break if rr.min > max
      if rr.min > cur_point
        start_point = cur_point
        end_point = rr.min.pred
        gaps << (start_point..end_point)
        cur_point = rr.max.succ
      elsif rr.max >= cur_point
        cur_point = rr.max.succ
      end
    end
    gaps << (cur_point..max) if cur_point <= max
    gaps
  end
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


126
127
128
129
# File 'lib/fat_core/range.rb', line 126

def intersection(other)
  return nil 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)

See Also:



28
29
30
31
32
33
34
# File 'lib/fat_core/range.rb', line 28

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

#left_contiguous?(other) ⇒ Boolean

Is self on the left of and contiguous to other? 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


206
207
208
209
210
211
212
# File 'lib/fat_core/range.rb', line 206

def left_contiguous?(other)
  if max.respond_to?(:succ)
    max.succ == other.min
  else
    max == other.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)]


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/fat_core/range.rb', line 89

def overlaps(ranges)
  if ranges.empty? || spanned_by?(ranges)
    []
  else
    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
end

#overlaps?(other) ⇒ Boolean

Return whether self overlaps with other Range.



309
310
311
312
# File 'lib/fat_core/range.rb', line 309

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.



323
324
325
326
# File 'lib/fat_core/range.rb', line 323

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, without their boundaries touching.



283
284
285
# File 'lib/fat_core/range.rb', line 283

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

#proper_superset_of?(other) ⇒ Boolean

Return whether self contains other range, without their boundaries touching.



301
302
303
# File 'lib/fat_core/range.rb', line 301

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

#right_contiguous?(other) ⇒ Boolean

Is self on the right of and contiguous to other? 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


234
235
236
237
238
239
240
# File 'lib/fat_core/range.rb', line 234

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

#spanned_by?(ranges) ⇒ Boolean

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



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/fat_core/range.rb', line 333

def spanned_by?(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?
    joined_range.min <= min && joined_range.max >= max
  else
    false
  end
end

#subset_of?(other) ⇒ Boolean

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



274
275
276
# File 'lib/fat_core/range.rb', line 274

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.



292
293
294
# File 'lib/fat_core/range.rb', line 292

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

#tex_quoteString

Allow erb or erubis documents to directly interpolate a Range.



180
181
182
# File 'lib/fat_core/range.rb', line 180

def tex_quote
  to_s
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


143
144
145
146
# File 'lib/fat_core/range.rb', line 143

def union(other)
  return nil unless overlaps?(other) || contiguous?(other)
  ([min, other.min].min..[max, other.max].max)
end