fat_core 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/fat_core/hash.rb CHANGED
@@ -1,8 +1,47 @@
1
+ # The FatCore extensions to Hash provide a handful of generally useful methods
2
+ # on Ruby Hash objects.
3
+ #
4
+ # You can get these with:
5
+ #
6
+ # ```
7
+ # require 'fat_core/hash'
8
+ # ```
9
+ #
10
+ # It provides a couple of methods for manipulating the keys of a Hash:
11
+ # `#remap_keys` for translating the current set of keys to a new set provided by
12
+ # a Hash of old to new keys, and `#replace_keys` for doing a similar operation
13
+ # with an Array of new keys. Along the same line, the method `#keys_with_value`
14
+ # will return the keys in a Hash equal to the given value of any of an Array of
15
+ # values.
16
+ #
17
+ # It also provides a method for deleting all entries in a Hash whose value match
18
+ # a single value or any one of an Array of values in `#delete_with_value`
19
+ #
20
+ # Finally, it provides an `#each_pair`-like method, `#each_pair_with_flags`,
21
+ # that yields each key-value pair of the Hash along with two boolean flags that
22
+ # indicate whether the element is the first or last in the Hash.
1
23
  module FatCore
2
-
3
24
  module Hash
4
- # Yield each key-value pair in the Hash together with boolean flags that
5
- # indicate whether the item is the first or last yielded.
25
+ # @group Enumerable Extensions
26
+ #
27
+ # Yield each key-value pair in the Hash together with two boolean flags that
28
+ # indicate whether the item is the first or last item in the Hash.
29
+ #
30
+ # @example
31
+ # {a: 1, b: 2, c: 3}.each_pair_with_flags do |k, val, first, last|
32
+ # print "#{k} => #{val}"
33
+ # print " is first" if first
34
+ # print " is last" if last
35
+ # print " is nothing special" if !first && !last
36
+ # print "\n"
37
+ # end
38
+ #
39
+ # #=> output:
40
+ # a => 1 is first
41
+ # b => 2 is nothing special
42
+ # c => 3 is last
43
+ #
44
+ # @return [Hash] return self
6
45
  def each_pair_with_flags
7
46
  last_k = size - 1
8
47
  k = 0
@@ -12,10 +51,40 @@ module FatCore
12
51
  yield(key, val, first, last)
13
52
  k += 1
14
53
  end
54
+ self
55
+ end
56
+
57
+ # @group Deletion
58
+ #
59
+ # Remove from the hash all keys that have values == to given value or that
60
+ # include the given value if the hash has an Enumerable for a value
61
+ #
62
+ # @example
63
+ # h = { a: 1, b: 2, c: 3, d: 2, e: 1 }
64
+ # h.delete_with_value(2) #=> { a: 1, c: 3, e: 1 }
65
+ # h.delete_with_value([1, 3]) #=> { b: 2, d: 2 }
66
+ #
67
+ # @param v [Object, Enumerable<Object>] value to test for
68
+ # @return [Hash] hash having entries === v or including v deleted
69
+ def delete_with_value(v)
70
+ keys_with_value(v).each do |k|
71
+ delete(k)
72
+ end
73
+ self
15
74
  end
16
75
 
76
+ # @group Key Manipulation
77
+ #
17
78
  # Return all keys in hash that have a value == to the given value or have an
18
79
  # Enumerable value that includes the given value.
80
+ #
81
+ # @example
82
+ # h = { a: 1, b: 2, c: 3, d: 2, e: 1 }
83
+ # h.keys_with_value(2) #=> [:b, :d]
84
+ # h.keys_with_value([1, 3]) #=> [:a, :c, :e]
85
+ #
86
+ # @param val [Object, Enumerable<Object>] value to test for
87
+ # @return [Array<Object>] the keys with value or values v
19
88
  def keys_with_value(val)
20
89
  result = []
21
90
  each_pair do |k, v|
@@ -26,16 +95,16 @@ module FatCore
26
95
  result
27
96
  end
28
97
 
29
- # Remove from the hash all keys that have values == to given value or that
30
- # include the given value if the hash has an Enumerable for a value
31
- def delete_with_value(v)
32
- keys_with_value(v).each do |k|
33
- delete(k)
34
- end
35
- self
36
- end
37
-
38
- # Change each key of this Hash to its value in key_map
98
+ # Change each key of this Hash to its value in `key_map`. Keys not appearing in
99
+ # the `key_map` remain in the result Hash.
100
+ #
101
+ # @example
102
+ # h = { a: 1, b: 2, c: 3, d: 2, e: 1 }
103
+ # key_map = { a: 'alpha', b: 'beta' }
104
+ # h.remap_keys(key_map) #=> {"alpha"=>1, "beta"=>2, :c=>3, :d=>2, :e=>1}
105
+ #
106
+ # @param key_map [Hash] hash mapping old keys to new
107
+ # @return [Hash] new hash with remapped keys
39
108
  def remap_keys(key_map = {})
40
109
  new_hash = {}
41
110
  each_pair do |key, val|
@@ -48,7 +117,17 @@ module FatCore
48
117
  new_hash
49
118
  end
50
119
 
51
- # Change the keys of this Hash to new_keys, an array of keys
120
+ # Change the keys of this Hash to new_keys, an array of keys of the same size
121
+ # as the Array self.keys.
122
+ #
123
+ # @example
124
+ # h = { a: 1, b: 2, c: 3, d: 2, e: 1 }
125
+ # nk = [:z, :y, :x, :w, :v]
126
+ # h.replace_keys(nk) #=> {:z=>1, :y=>2, :x=>3, :w=>2, :v=>1}
127
+ #
128
+ # @raise [ArgumentError] if new_keys.size != self.keys.size
129
+ # @param new_keys [Array<Object>] replacement keys
130
+ # @return [Hash]
52
131
  def replace_keys(new_keys)
53
132
  unless keys.size == new_keys.size
54
133
  raise ArgumentError, 'replace_keys: new keys size differs from key size'
@@ -58,4 +137,8 @@ module FatCore
58
137
  end
59
138
  end
60
139
 
61
- Hash.include FatCore::Hash
140
+ class Hash
141
+ include FatCore::Hash
142
+ # @!parse include FatCore::Hash
143
+ # @!parse extend FatCore::Hash::ClassMethods
144
+ end
@@ -1,6 +1,19 @@
1
1
  require 'fat_core/numeric'
2
2
 
3
3
  module Kernel
4
+ # Run the given block and report the time it took to execute in
5
+ # hour-minute-second form.
6
+ #
7
+ # @example
8
+ # result = time_it 'Fibbonacci' do
9
+ # Fibbonacci.fib(30)
10
+ # end
11
+ # puts "For 30 its #{result}"
12
+ # => "Ran Fibonacci in 30:23"
13
+ #
14
+ # @param name [String, #to_s] an optional name to use for block in timing
15
+ # message.
16
+ # @return [Object] whatever the block returns
4
17
  def time_it(name = 'block', &block)
5
18
  start = Time.now
6
19
  result = yield block
data/lib/fat_core/nil.rb CHANGED
@@ -1,17 +1,31 @@
1
1
  module FatCore
2
2
  module NilClass
3
- def entitle
3
+ # Allow nils to respond to #as_string like String and Symbol
4
+ #
5
+ # @return [String] empty string
6
+ def as_string
4
7
  ''
5
8
  end
9
+ alias entitle as_string
6
10
 
11
+ # Allow nils to respond to #tex_quote for use in TeX documents
12
+ #
13
+ # @return [String] empty string
7
14
  def tex_quote
8
15
  ''
9
16
  end
10
17
 
18
+ # Allow nils to respond to #commas like String and Numeric
19
+ #
20
+ # @return [String] empty string
11
21
  def commas(_places = nil)
12
22
  ''
13
23
  end
14
24
  end
15
25
  end
16
26
 
17
- NilClass.include FatCore::NilClass
27
+ class NilClass
28
+ include FatCore::NilClass
29
+ # @!parse include FatCore::NilClass
30
+ # @!parse extend FatCore::NilClass::ClassMethods
31
+ end
@@ -1,7 +1,16 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
1
3
  module FatCore
2
4
  module Numeric
3
5
  # Return the signum function for this number, i.e., 1 for a positive number,
4
6
  # 0 for zero, and -1 for a negative number.
7
+ #
8
+ # @example
9
+ # -55.signum #=> -1
10
+ # 0.signum #=> 0
11
+ # 55.signum #=> 1
12
+ #
13
+ # @return [Integer] -1, 0, or 1 for negative, zero or positive self
5
14
  def signum
6
15
  if positive?
7
16
  1
@@ -13,28 +22,40 @@ module FatCore
13
22
  end
14
23
 
15
24
  # Convert this number into a string and insert grouping commas into the
16
- # whole number part and round the decimal part to +places+ decimal places,
17
- # with the number of places being zero for an integer and 4 for a
18
- # non-integer.
25
+ # whole number part and round the decimal part to `places` decimal places,
26
+ # with the default number of places being zero for an integer and 4 for a
27
+ # non-integer. The fractional part is padded with zeroes on the right to
28
+ # come out to `places` digits after the decimal place.
29
+ #
30
+ # @example
31
+ # 9324089.56.commas #=> '9,324,089.56'
32
+ # 88883.14159.commas #=>'88,883.1416'
33
+ # 88883.14159.commas(2) #=>'88,883.14'
34
+ #
35
+ # @param places [Integer] number of decimal place to round to
36
+ # @return [String]
19
37
  def commas(places = nil)
20
- # By default, use zero places for whole numbers; four places for
21
- # numbers containing a fractional part to 4 places.
22
- if places.nil?
23
- places =
24
- if abs.modulo(1).round(4) > 0.0
25
- 4
26
- else
27
- 0
28
- end
29
- end
30
38
  group(places, ',')
31
39
  end
32
40
 
33
41
  # Convert this number into a string and insert grouping delimiter character,
34
- # +delim+ into the whole number part and round the decimal part to +places+
35
- # decimal places, with the number of places being zero for an integer and 4
36
- # for a non-integer.
37
- def group(places = 0, delim = ',')
42
+ # `delim` into the whole number part and round the decimal part to `places`
43
+ # decimal places, with the default number of places being zero for an
44
+ # integer and 4 for a non-integer. The fractional part is padded with zeroes
45
+ # on the right to come out to `places` digits after the decimal place. This
46
+ # is the same as #commas, but allows the delimiter to be any string.
47
+ #
48
+ # @example
49
+ # 9324089.56.group #=> '9,324,089.56'
50
+ # 9324089.56.group(4) #=> '9,324,089.5600'
51
+ # 88883.14159.group #=>'88,883.1416'
52
+ # 88883.14159.group(2) #=>'88,883.14'
53
+ # 88883.14159.group(2, '_') #=>'88_883.14'
54
+ #
55
+ # @param places [Integer] number of decimal place to round to
56
+ # @param delim [String] use delim as group separator
57
+ # @return [String]
58
+ def group(places = nil, delim = ',')
38
59
  # Return number as a string with embedded commas
39
60
  # for nice printing; round to places places after
40
61
  # the decimal
@@ -43,43 +64,69 @@ module FatCore
43
64
  # less than 1 (to ensure that really small numbers round to 0.0)
44
65
  return to_s if abs > 1.0 && to_s =~ /e/
45
66
 
46
- str = to_f.round(places).to_s
67
+ # Round if places given
68
+ str =
69
+ if places.nil?
70
+ whole? ? to_i.to_s : to_f.to_s
71
+ else
72
+ to_f.round(places).to_s
73
+ end
47
74
 
48
- # Break the number into parts
49
- str =~ /^(-)?(\d*)((\.)?(\d*))?$/
50
- neg = $1 || ''
51
- whole = $2
52
- frac = $5
75
+ # Break the number into parts; underscores are possible in all components.
76
+ str =~ /\A([-+])?([\d_]*)((\.)?([\d_]*))?([eE][+-]?[\d_]+)?\z/
77
+ sig = $1 || ''
78
+ whole = $2 ? $2.delete('_') : ''
79
+ frac = $5 || ''
80
+ exp = $6 || ''
53
81
 
54
82
  # Pad out the fractional part with zeroes to the right
55
- n_zeroes = [places - frac.length, 0].max
56
- frac += '0' * n_zeroes if n_zeroes.positive?
83
+ unless places.nil?
84
+ n_zeroes = [places - frac.length, 0].max
85
+ frac += '0' * n_zeroes if n_zeroes.positive?
86
+ end
57
87
 
58
88
  # Place the commas in the whole part only
59
89
  whole = whole.reverse
60
90
  whole.gsub!(/([0-9]{3})/, "\\1#{delim}")
61
91
  whole.gsub!(/#{Regexp.escape(delim)}$/, '')
62
92
  whole.reverse!
63
- if frac.nil? || places <= 0
64
- neg + whole
93
+ if frac.blank? # || places <= 0
94
+ sig + whole + exp
65
95
  else
66
- neg + whole + '.' + frac
96
+ sig + whole + '.' + frac + exp
67
97
  end
68
98
  end
69
99
 
70
100
  # Return whether this is a whole number.
101
+ #
102
+ # @example
103
+ # 23.45.whole? #=> false
104
+ # 23.whole? #=> true
105
+ #
106
+ # @return [Boolean] is self whole?
71
107
  def whole?
72
108
  floor == self
73
109
  end
74
110
 
75
111
  # Return an Integer type, but only if the fractional part of self is zero;
76
112
  # otherwise just return self.
113
+ #
114
+ # @example
115
+ # 45.98.int_if_whole #=> 45.98
116
+ # 45.000.int_if_whole #=> 45
117
+ #
118
+ # @return [Numeric, Integer]
77
119
  def int_if_whole
78
120
  whole? ? floor : self
79
121
  end
80
122
 
81
- # Convert a number of seconds into a string of the form HH:MM:SS.dd, that is
82
- # to hours, minutes and seconds.
123
+ # Convert self, regarded as a number of seconds, into a string of the form
124
+ # HH:MM:SS.dd, that is to hours, minutes and seconds and fractions of seconds.
125
+ #
126
+ # @example
127
+ # 5488.secs_to_hms #=> "01:31:28"
128
+ #
129
+ # @return [String] formatted as HH:MM:SS.dd
83
130
  def secs_to_hms
84
131
  frac = self % 1
85
132
  mins, secs = divmod(60)
@@ -91,11 +138,16 @@ module FatCore
91
138
  end
92
139
  end
93
140
 
94
- # Allow erb documents to directly interpolate numbers
141
+ # Quote self for use in TeX documents. Since number components are not
142
+ # special to TeX, this just applies `#to_s`
95
143
  def tex_quote
96
144
  to_s
97
145
  end
98
146
  end
99
147
  end
100
148
 
101
- Numeric.include FatCore::Numeric
149
+ class Numeric
150
+ include FatCore::Numeric
151
+ # @!parse include FatCore::Numeric
152
+ # @!parse extend FatCore::Numeric::ClassMethods
153
+ end
@@ -1,7 +1,30 @@
1
+ # FatCore extends the Range class with methods that
2
+ #
3
+ # 1. provide some set operations operations on Ranges, union, intersection, and
4
+ # difference,
5
+ # 2. test for overlapping and contiguity between Ranges,
6
+ # 3. test for whether one Range is a subset or superset of another,
7
+ # 3. join contiguous Ranges,
8
+ # 4. find whether a set of Ranges spans a large range, and if not, to return
9
+ # a set of Ranges that represent gaps in the coverage or overlaps in
10
+ # coverage,
11
+ # 5. provide a definition for sorting Ranges based on sorting by the min values
12
+ # and sizes of the Ranges.
1
13
  module FatCore
2
14
  module Range
3
- # Return a range that concatenates this range with other; return nil
4
- # if the ranges are not contiguous.
15
+ # @group Operations
16
+
17
+ # Return a range that concatenates this range with other if it is contiguous
18
+ # with this range on the left or right; return nil if the ranges are not
19
+ # contiguous.
20
+ #
21
+ # @example
22
+ # (0..3).join(4..8) #=> (0..8)
23
+ #
24
+ # @param other [Range] the Range to join to this range
25
+ # @return [Range, nil] this range joined to other
26
+ #
27
+ # @see contiguous? For the definition of "contiguous"
5
28
  def join(other)
6
29
  if left_contiguous?(other)
7
30
  ::Range.new(min, other.max)
@@ -10,55 +33,112 @@ module FatCore
10
33
  end
11
34
  end
12
35
 
13
- # Is self on the left of and contiguous to other?
14
- def left_contiguous?(other)
15
- if max.respond_to?(:succ)
16
- max.succ == other.min
36
+ # If this range is not spanned by the `ranges` collectively, return an Array
37
+ # of ranges representing the gaps in coverage. The `ranges` can over-cover
38
+ # this range on the left or right without affecting the result, that is,
39
+ # each range in the returned array of gap ranges will always be subsets of
40
+ # this range.
41
+ #
42
+ # If the `ranges` span this range, return an empty array.
43
+ #
44
+ # @example
45
+ # (0..10).gaps([(0..3), (5..6), (9..10)]) #=> [(4..4), (7..8)]
46
+ # (0..10).gaps([(-4..3), (5..6), (9..15)]) #=> [(4..4), (7..8)]
47
+ # (0..10).gaps([(-4..3), (4..6), (7..15)]) #=> [] ranges span this one
48
+ # (0..10).gaps([(-4..-3), (11..16), (17..25)]) #=> [(0..10)] no overlap
49
+ # (0..10).gaps([]) #=> [(0..10)] no overlap
50
+ #
51
+ # @param ranges [Array<Range>]
52
+ # @return [Array<Range>]
53
+ def gaps(ranges)
54
+ if ranges.empty?
55
+ [clone]
56
+ elsif spanned_by?(ranges)
57
+ []
17
58
  else
18
- max == other.min
59
+ ranges = ranges.sort_by(&:min)
60
+ gaps = []
61
+ cur_point = min
62
+ ranges.each do |rr|
63
+ break if rr.min > max
64
+ if rr.min > cur_point
65
+ start_point = cur_point
66
+ end_point = rr.min.pred
67
+ gaps << (start_point..end_point)
68
+ cur_point = rr.max.succ
69
+ elsif rr.max >= cur_point
70
+ cur_point = rr.max.succ
71
+ end
72
+ end
73
+ gaps << (cur_point..max) if cur_point <= max
74
+ gaps
19
75
  end
20
76
  end
21
77
 
22
- # Is self on the right of and contiguous to other?
23
- def right_contiguous?(other)
24
- if other.max.respond_to?(:succ)
25
- other.max.succ == min
78
+ # Within this range return an Array of Ranges representing the overlaps
79
+ # among the given Array of Ranges `ranges`. If there are no overlaps, return
80
+ # an empty array. Don't consider overlaps in the `ranges` that occur outside
81
+ # of self.
82
+ #
83
+ # @example
84
+ # (0..10).overlaps([(-4..4), (2..7), (5..12)]) => [(2..4), (5..7)]
85
+ #
86
+ # @param ranges [Array<Range>] ranges to search for overlaps
87
+ # @return [Array<Range>] overlaps with ranges but inside this Range
88
+ def overlaps(ranges)
89
+ if ranges.empty? || spanned_by?(ranges)
90
+ []
26
91
  else
27
- other.max == min
92
+ ranges = ranges.sort_by(&:min)
93
+ overlaps = []
94
+ cur_point = nil
95
+ ranges.each do |rr|
96
+ # Skip ranges outside of self
97
+ next if rr.max < min || rr.min > max
98
+ # Initialize cur_point to max of first range
99
+ if cur_point.nil?
100
+ cur_point = rr.max
101
+ next
102
+ end
103
+ # We are on the second or later range
104
+ if rr.min < cur_point
105
+ start_point = rr.min
106
+ end_point = cur_point
107
+ overlaps << (start_point..end_point)
108
+ end
109
+ cur_point = rr.max
110
+ end
111
+ overlaps
28
112
  end
29
113
  end
30
114
 
31
- def contiguous?(other)
32
- left_contiguous?(other) || right_contiguous?(other)
33
- end
34
-
35
- def subset_of?(other)
36
- min >= other.min && max <= other.max
37
- end
38
-
39
- def proper_subset_of?(other)
40
- min > other.min && max < other.max
41
- end
42
-
43
- def superset_of?(other)
44
- min <= other.min && max >= other.max
45
- end
46
-
47
- def proper_superset_of?(other)
48
- min < other.min && max > other.max
49
- end
50
-
51
- def overlaps?(other)
52
- (cover?(other.min) || cover?(other.max) ||
53
- other.cover?(min) || other.cover?(max))
54
- end
55
-
115
+ # Return a Range that represents the intersection between this range and the
116
+ # `other` range. If there is no intersection, return nil.
117
+ #
118
+ # @example
119
+ # (0..10) & (5..20) #=> (5..10)
120
+ # (0..10).intersection((5..20)) #=> (5..10)
121
+ # (0..10) & (15..20) #=> nil
122
+ #
123
+ # @param other [Range] the Range self is intersected with
124
+ # @return [Range, nil] a Range representing the intersection
56
125
  def intersection(other)
57
126
  return nil unless overlaps?(other)
58
127
  ([min, other.min].max..[max, other.max].min)
59
128
  end
60
129
  alias & intersection
61
130
 
131
+ # Return a Range that represents the union between this range and the
132
+ # `other` range. If there is no overlap and self is not contiguous with
133
+ # `other`, return `nil`.
134
+ #
135
+ # @example
136
+ # (0..10) + (5..20) #=> (0..20)
137
+ # (0..10).union((5..20)) #=> (0..20)
138
+ # (0..10) + (15..20) #=> nil
139
+ #
140
+ # @param other [Range] the Range self is union-ed with
141
+ # @return [Range, nil] a Range representing the union
62
142
  def union(other)
63
143
  return nil unless overlaps?(other) || contiguous?(other)
64
144
  ([min, other.min].min..[max, other.max].max)
@@ -93,29 +173,165 @@ module FatCore
93
173
  end
94
174
  alias - difference
95
175
 
96
- # Return whether any of the ranges that are within self overlap one
97
- # another
98
- def has_overlaps_within?(ranges)
99
- result = false
100
- unless ranges.empty?
101
- ranges.each do |r1|
102
- next unless overlaps?(r1)
103
- result =
104
- ranges.any? do |r2|
105
- r1.object_id != r2.object_id && overlaps?(r2) &&
106
- r1.overlaps?(r2)
107
- end
108
- return true if result
109
- end
176
+ # Allow erb or erubis documents to directly interpolate a Range.
177
+ #
178
+ # @return [String]
179
+ def tex_quote
180
+ to_s
181
+ end
182
+
183
+ # @group Queries
184
+
185
+ # Is self on the left of and contiguous to other? Whether one range is
186
+ # "contiguous" to another has two cases:
187
+ #
188
+ # 1. If the elements of the Range on the left respond to the #succ method
189
+ # (that is, its values are discrete values such as Integers or Dates)
190
+ # test whether the succ to the max value of the Range on the left is
191
+ # equal to the min value of the Range on the right.
192
+ # 2. If the elements of the Range on the left do not respond to the #succ
193
+ # method (that is, its values are continuous values such as Floats) test
194
+ # whether the max value of the Range on the left is equal to the min
195
+ # value of the Range on the right
196
+ #
197
+ # @example
198
+ # (0..10).left_contiguous((11..20)) #=> true
199
+ # (11..20).left_contiguous((0..10)) #=> false, but right_contiguous
200
+ # (0.5..3.145).left_contiguous((3.145..18.4)) #=> true
201
+ # (0.5..3.145).left_contiguous((3.146..18.4)) #=> false
202
+ #
203
+ # @param other [Range] other range to test for contiguity
204
+ # @return [Boolean] is self left_contiguous with other
205
+ def left_contiguous?(other)
206
+ if max.respond_to?(:succ)
207
+ max.succ == other.min
208
+ else
209
+ max == other.min
110
210
  end
111
- result
211
+ end
212
+
213
+ # Is self on the right of and contiguous to other? Whether one range is
214
+ # "contiguous" to another has two cases:
215
+ #
216
+ # 1. If the elements of the Range on the left respond to the #succ method
217
+ # (that is, its values are discrete values such as Integers or Dates)
218
+ # test whether the succ to the max value of the Range on the left is
219
+ # equal to the min value of the Range on the right.
220
+ # 2. If the elements of the Range on the left do not respond to the #succ
221
+ # method (that is, its values are continuous values such as Floats) test
222
+ # whether the max value of the Range on the left is equal to the min
223
+ # value of the Range on the right
224
+ #
225
+ # @example
226
+ # (11..20).right_contiguous((0..10)) #=> true
227
+ # (0..10).right_contiguous((11..20)) #=> false, but left_contiguous
228
+ # (3.145..12.3).right_contiguous((0.5..3.145)) #=> true
229
+ # (3.146..12.3).right_contiguous((0.5..3.145)) #=> false
230
+ #
231
+ # @param other [Range] other range to test for contiguity
232
+ # @return [Boolean] is self right_contiguous with other
233
+ def right_contiguous?(other)
234
+ if other.max.respond_to?(:succ)
235
+ other.max.succ == min
236
+ else
237
+ other.max == min
238
+ end
239
+ end
240
+
241
+ # Is self contiguous to other either on the left or on the right? First, the
242
+ # two ranges are sorted by their min values, and the range with the lowest
243
+ # min value is considered to be on the "left" and the other range on the
244
+ # "right". Whether one range is "contiguous" to another then has two cases:
245
+ #
246
+ # 1. If the max element of the Range on the left respond to the #succ method
247
+ # (that is, its value is a discrete value such as Integer or Date)
248
+ # test whether the succ to the max value of the Range on the left is
249
+ # equal to the min value of the Range on the right.
250
+ # 2. If the max element of the Range on the left does not respond to the
251
+ # #succ method (that is, its values are continuous values such as Floats)
252
+ # test whether the max value of the Range on the left is equal to the min
253
+ # value of the Range on the right
254
+ #
255
+ # @example
256
+ # (0..10).contiguous?((11..20)) #=> true
257
+ # (11..20).contiguous?((0..10)) #=> true, right_contiguous
258
+ # (0..10).contiguous?((15..20)) #=> false
259
+ # (3.145..12.3).contiguous?((0.5..3.145)) #=> true
260
+ # (3.146..12.3).contiguous?((0.5..3.145)) #=> false
261
+ #
262
+ # @param other [Range] other range to test for contiguity
263
+ # @return [Boolean] is self contiguous with other
264
+ def contiguous?(other)
265
+ left_contiguous?(other) || right_contiguous?(other)
266
+ end
267
+
268
+ # Return whether self is contained within `other` range, even if their
269
+ # boundaries touch.
270
+ #
271
+ # @param other [Range] the containing range
272
+ # @return [Boolean] is self within other
273
+ def subset_of?(other)
274
+ min >= other.min && max <= other.max
275
+ end
276
+
277
+ # Return whether self is contained within `other` range, without their
278
+ # boundaries touching.
279
+ #
280
+ # @param other [Range] the containing range
281
+ # @return [Boolean] is self wholly within other
282
+ def proper_subset_of?(other)
283
+ min > other.min && max < other.max
284
+ end
285
+
286
+ # Return whether self contains `other` range, even if their
287
+ # boundaries touch.
288
+ #
289
+ # @param other [Range] the contained range
290
+ # @return [Boolean] does self contain other
291
+ def superset_of?(other)
292
+ min <= other.min && max >= other.max
293
+ end
294
+
295
+ # Return whether self contains `other` range, without their
296
+ # boundaries touching.
297
+ #
298
+ # @param other [Range] the contained range
299
+ # @return [Boolean] does self wholly contain other
300
+ def proper_superset_of?(other)
301
+ min < other.min && max > other.max
302
+ end
303
+
304
+ # Return whether self overlaps with other Range.
305
+ #
306
+ # @param other [Range] range to test for overlap with self
307
+ # @return [Boolean] is there an overlap?
308
+ def overlaps?(other)
309
+ (cover?(other.min) || cover?(other.max) ||
310
+ other.cover?(min) || other.cover?(max))
311
+ end
312
+
313
+ # Return whether any of the `ranges` that overlap self have overlaps among one
314
+ # another.
315
+ #
316
+ # This does the same thing as `Range.overlaps_among?`, except that it filters
317
+ # the `ranges` to only those overlapping self before testing for overlaps
318
+ # among them.
319
+ #
320
+ # @param ranges [Array<Range>] ranges to test for overlaps
321
+ # @return [Boolean] were there overlaps among ranges?
322
+ def overlaps_among?(ranges)
323
+ iranges = ranges.select { |r| overlaps?(r) }
324
+ ::Range.overlaps_among?(iranges)
112
325
  end
113
326
 
114
327
  # Return true if the given ranges collectively cover this range
115
- # without overlaps.
328
+ # without overlaps and without gaps.
329
+ #
330
+ # @param ranges [Array<Range>]
331
+ # @return [Boolean]
116
332
  def spanned_by?(ranges)
117
333
  joined_range = nil
118
- ranges.sort_by(&:min).each do |r|
334
+ ranges.sort.each do |r|
119
335
  unless joined_range
120
336
  joined_range = r
121
337
  next
@@ -130,69 +346,55 @@ module FatCore
130
346
  end
131
347
  end
132
348
 
133
- # If this range is not spanned by the ranges collectively, return an array
134
- # of ranges representing the gaps in coverage. Otherwise return an empty
135
- # array.
136
- def gaps(ranges)
137
- if ranges.empty?
138
- [clone]
139
- elsif spanned_by?(ranges)
140
- []
141
- else
142
- ranges = ranges.sort_by(&:min)
143
- gaps = []
144
- cur_point = min
145
- ranges.each do |rr|
146
- break if rr.min > max
147
- if rr.min > cur_point
148
- start_point = cur_point
149
- end_point = rr.min.pred
150
- gaps << (start_point..end_point)
151
- cur_point = rr.max.succ
152
- elsif rr.max >= cur_point
153
- cur_point = rr.max.succ
154
- end
155
- end
156
- gaps << (cur_point..max) if cur_point <= max
157
- gaps
158
- end
349
+ include Comparable
350
+
351
+ # @group Sorting
352
+
353
+ # Compare this range with other first by min values, then by max values.
354
+ #
355
+ # This causes a sort of Ranges with Comparable elements to sort from left to
356
+ # right on the number line, then for Ranges that start on the same number, from
357
+ # smallest to largest.
358
+ #
359
+ # @example
360
+ # (4..8) <=> (5..7) #=> -1
361
+ # (4..8) <=> (4..7) #=> 1
362
+ # (4..8) <=> (4..8) #=> 0
363
+ #
364
+ # @param other [Range] range to compare self with
365
+ # @return [-1, 0, 1] if self is less, equal, or greater than other
366
+ def <=>(other)
367
+ [min, max] <=> [other.min, other.max]
159
368
  end
160
369
 
161
- # Similar to gaps, but within this range return the /overlaps/ among the
162
- # given ranges. If there are no overlaps, return an empty array. Don't
163
- # consider overlaps in the ranges that occur outside of self.
164
- def overlaps(ranges)
165
- if ranges.empty? || spanned_by?(ranges)
166
- []
167
- else
168
- ranges = ranges.sort_by(&:min)
169
- overlaps = []
170
- cur_point = nil
171
- ranges.each do |rr|
172
- # Skip ranges outside of self
173
- next if rr.max < min || rr.min > max
174
- # Initialize cur_point to max of first range
175
- if cur_point.nil?
176
- cur_point = rr.max
177
- next
370
+ module ClassMethods
371
+ # Return whether any of the `ranges` overlap one another
372
+ #
373
+ # @param ranges [Array<Range>] ranges to test for overlaps
374
+ # @return [Boolean] were there overlaps among ranges?
375
+ def overlaps_among?(ranges)
376
+ result = false
377
+ unless ranges.empty?
378
+ ranges.each do |r1|
379
+ result = ranges.any? do |r2|
380
+ r1.object_id != r2.object_id && r1.overlaps?(r2)
381
+ end
382
+ return true if result
178
383
  end
179
- # We are on the second or later range
180
- if rr.min < cur_point
181
- start_point = rr.min
182
- end_point = cur_point
183
- overlaps << (start_point..end_point)
184
- end
185
- cur_point = rr.max
186
384
  end
187
- overlaps
385
+ result
188
386
  end
189
387
  end
190
388
 
191
- # Allow erb documents can directly interpolate ranges
192
- def tex_quote
193
- to_s
389
+ # @private
390
+ def self.included(base)
391
+ base.extend(ClassMethods)
194
392
  end
195
393
  end
196
394
  end
197
395
 
198
- Range.include FatCore::Range
396
+ class Range
397
+ include FatCore::Range
398
+ # @!parse include FatCore::Range
399
+ # @!parse extend FatCore::Range::ClassMethods
400
+ end