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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.yardopts +5 -1
- data/README.md +124 -7
- data/Rakefile +17 -1
- data/bin/console +6 -7
- data/bin/easters +1 -1
- data/fat_core.gemspec +3 -2
- data/lib/fat_core/all.rb +1 -4
- data/lib/fat_core/array.rb +4 -1
- data/lib/fat_core/bigdecimal.rb +19 -0
- data/lib/fat_core/date.rb +913 -298
- data/lib/fat_core/hash.rb +98 -15
- data/lib/fat_core/kernel.rb +13 -0
- data/lib/fat_core/nil.rb +16 -2
- data/lib/fat_core/numeric.rb +84 -32
- data/lib/fat_core/range.rb +311 -109
- data/lib/fat_core/string.rb +246 -161
- data/lib/fat_core/symbol.rb +28 -4
- data/lib/fat_core/version.rb +2 -1
- data/spec/lib/{big_decimal_spec.rb → bigdecimal_spec.rb} +1 -1
- data/spec/lib/date_spec.rb +1 -1
- data/spec/lib/numeric_spec.rb +1 -1
- data/spec/lib/range_spec.rb +8 -6
- data/spec/lib/string_spec.rb +72 -58
- data/spec/spec_helper.rb +3 -2
- metadata +9 -10
- data/lib/core_extensions/date/fat_core.rb +0 -6
- data/lib/fat_core/big_decimal.rb +0 -12
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
|
-
#
|
5
|
-
#
|
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
|
-
#
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
#
|
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
|
-
|
140
|
+
class Hash
|
141
|
+
include FatCore::Hash
|
142
|
+
# @!parse include FatCore::Hash
|
143
|
+
# @!parse extend FatCore::Hash::ClassMethods
|
144
|
+
end
|
data/lib/fat_core/kernel.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
27
|
+
class NilClass
|
28
|
+
include FatCore::NilClass
|
29
|
+
# @!parse include FatCore::NilClass
|
30
|
+
# @!parse extend FatCore::NilClass::ClassMethods
|
31
|
+
end
|
data/lib/fat_core/numeric.rb
CHANGED
@@ -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
|
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
|
-
#
|
35
|
-
# decimal places, with the number of places being zero for an
|
36
|
-
# for a non-integer.
|
37
|
-
|
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
|
-
|
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 =~
|
50
|
-
|
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
|
-
|
56
|
-
|
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.
|
64
|
-
|
93
|
+
if frac.blank? # || places <= 0
|
94
|
+
sig + whole + exp
|
65
95
|
else
|
66
|
-
|
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
|
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
|
-
#
|
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
|
-
|
149
|
+
class Numeric
|
150
|
+
include FatCore::Numeric
|
151
|
+
# @!parse include FatCore::Numeric
|
152
|
+
# @!parse extend FatCore::Numeric::ClassMethods
|
153
|
+
end
|
data/lib/fat_core/range.rb
CHANGED
@@ -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
|
-
#
|
4
|
-
|
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
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
#
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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.
|
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
|
-
|
134
|
-
|
135
|
-
#
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
385
|
+
result
|
188
386
|
end
|
189
387
|
end
|
190
388
|
|
191
|
-
#
|
192
|
-
def
|
193
|
-
|
389
|
+
# @private
|
390
|
+
def self.included(base)
|
391
|
+
base.extend(ClassMethods)
|
194
392
|
end
|
195
393
|
end
|
196
394
|
end
|
197
395
|
|
198
|
-
|
396
|
+
class Range
|
397
|
+
include FatCore::Range
|
398
|
+
# @!parse include FatCore::Range
|
399
|
+
# @!parse extend FatCore::Range::ClassMethods
|
400
|
+
end
|