fat_core 6.0.0 → 7.0.1

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/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
  require 'rdoc/task'
@@ -25,8 +27,19 @@ end
25
27
  ########################################################################
26
28
  # Rubocop tasks
27
29
  ########################################################################
28
- require 'rubocop/rake_task'
30
+ # Option A (recommended): Keep using Bundler and run rubocop via `bundle exec`.
31
+ # This wrapper task ensures the rubocop run uses the gems from your Gemfile,
32
+ # even when you invoke `rake rubocop` (no need to remember `bundle exec rake`).
33
+ #
34
+ # You can pass extra RuboCop CLI flags with the RUBOCOP_OPTS environment variable:
35
+ # RUBOCOP_OPTS="--format simple" rake rubocop
29
36
 
30
- RuboCop::RakeTask.new
37
+ desc "Run rubocop under `bundle exec`"
38
+ task :rubocop do
39
+ opts = (ENV['RUBOCOP_OPTS'] || '').split
40
+ Bundler.with_unbundled_env do
41
+ sh 'bundle', 'exec', 'rubocop', *opts
42
+ end
43
+ end
31
44
 
32
45
  task :default => [:spec, :rubocop]
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'fat_core/all'
data/fat_core.gemspec CHANGED
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative 'lib/fat_core/version'
4
5
 
data/lib/fat_core/all.rb CHANGED
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fat_core/array'
4
- require 'fat_core/bigdecimal'
5
- require 'fat_core/enumerable'
6
- require 'fat_core/hash'
7
- require 'fat_core/kernel'
8
- require 'fat_core/nil'
9
- require 'fat_core/numeric'
10
- require 'fat_core/range'
11
- require 'fat_core/string'
12
- require 'fat_core/symbol'
3
+ require_relative 'array'
4
+ require_relative 'bigdecimal'
5
+ require_relative 'enumerable'
6
+ require_relative 'hash'
7
+ require_relative 'nil'
8
+ require_relative 'numeric'
9
+ require_relative 'range'
10
+ require_relative 'string'
11
+ require_relative 'symbol'
@@ -21,10 +21,10 @@ module FatCore
21
21
  result
22
22
  end
23
23
 
24
- # Return an Array that is the difference between this Array and +other+, but
25
- # without removing duplicates as the Array#- method does. All items of this
26
- # Array are included in the result unless they also appear in the +other+
27
- # Array.
24
+ # Return an Array that is the difference between this Array and +other+,
25
+ # but without removing duplicates as the Array#- method does. All items of
26
+ # this Array are included in the result unless they also appear in any of
27
+ # the +other+ Arrays.
28
28
  def diff_with_dups(*others)
29
29
  result = []
30
30
  each do |itm|
@@ -35,7 +35,7 @@ module FatCore
35
35
 
36
36
  # Convert this array into a single string by (1) applying #to_s to each
37
37
  # element and (2) joining the elements with the string given by the sep:
38
- # paramater. By default the sep parameter is ', '. You may use a different
38
+ # parameter. By default the sep parameter is ', '. You may use a different
39
39
  # separation string in the case when there are only two items in the list
40
40
  # by supplying a two_sep parameter. You may also supply a difference
41
41
  # separation string to separate the second-last and last items in the
@@ -2,21 +2,6 @@
2
2
 
3
3
  # Useful extensions to the core Enumerable module.
4
4
  module Enumerable
5
- # Yield items in groups of n, for each group yield the group number, starting
6
- # with zero and an Array of n items, or all remaining items if less than n.
7
- #
8
- # ('a'..'z').to_a.groups_of(5) do |k, grp|
9
- # # On each iteration, grp is an Array of the next 5 items except the
10
- # # last group, which contains only ['z'].
11
- # end
12
- def groups_of(num)
13
- k = -1
14
- group_by do
15
- k += 1
16
- k.div(num)
17
- end
18
- end
19
-
20
5
  # Yield each item together with two booleans that indicate whether the item is
21
6
  # the first or last item in the Enumerable.
22
7
  #
data/lib/fat_core/hash.rb CHANGED
@@ -10,6 +10,9 @@
10
10
  # require 'fat_core/hash'
11
11
  # ```
12
12
  #
13
+
14
+ require_relative 'enumerable'
15
+
13
16
  module FatCore
14
17
  # It provides a couple of methods for manipulating the keys of a Hash:
15
18
  # `#remap_keys` for translating the current set of keys to a new set provided by
@@ -59,23 +62,50 @@ module FatCore
59
62
 
60
63
  # @group Deletion
61
64
  #
62
- # Remove from the hash all keys that have values == to given value or that
63
- # include the given value if the hash has an Enumerable for a value
65
+ # Remove from the hash in place all keys that have values == to the given
66
+ # value or values.
64
67
  #
65
68
  # @example
66
69
  # h = { a: 1, b: 2, c: 3, d: 2, e: 1 }
67
- # h.delete_with_value(2) #=> { a: 1, c: 3, e: 1 }
68
- # h.delete_with_value([1, 3]) #=> { b: 2, d: 2 }
70
+ # h.delete_with_value!(2)
71
+ # h #=> { a: 1, c: 3, e: 1 }
72
+ # h.delete_with_value!(1, 3)
73
+ # h #=> { b: 2, d: 2 }
69
74
  #
70
75
  # @param val [Object, Enumerable<Object>] value to test for
71
76
  # @return [Hash] hash having entries === v or including v deleted
72
- def delete_with_value(val)
73
- keys_with_value(val).each do |k|
74
- delete(k)
77
+ def delete_with_value!(*val)
78
+ val.each do |v|
79
+ keys_with_value(v).each do |k|
80
+ delete(k)
81
+ end
75
82
  end
76
83
  self
77
84
  end
78
85
 
86
+ #
87
+ # Return a copy of the Hash a Hash with all keys that have values == to the
88
+ # given value or values.
89
+ #
90
+ # @example
91
+ # h = { a: 1, b: 2, c: 3, d: 2, e: 1 }
92
+ # h.delete_with_value(2) #=> { a: 1, c: 3, e: 1 }
93
+ # h => { a: 1, b: 2, c: 3, d: 2, e: 1 }
94
+ # h.delete_with_value(1, 3) #=> { b: 2, d: 2 }
95
+ # h => { a: 1, b: 2, c: 3, d: 2, e: 1 }
96
+ #
97
+ # @param val [Object, Enumerable<Object>] value to test for
98
+ # @return [Hash] hash having entries === v or including v deleted
99
+ def delete_with_value(*val)
100
+ hsh = clone
101
+ val.each do |v|
102
+ hsh.keys_with_value(v).each do |k|
103
+ hsh.delete(k)
104
+ end
105
+ end
106
+ hsh
107
+ end
108
+
79
109
  # @group Key Manipulation
80
110
  #
81
111
  # Return all keys in hash that have a value == to the given value or have an
@@ -88,10 +118,12 @@ module FatCore
88
118
  #
89
119
  # @param val [Object, Enumerable<Object>] value to test for
90
120
  # @return [Array<Object>] the keys with value or values v
91
- def keys_with_value(val)
121
+ def keys_with_value(*vals)
92
122
  keys = []
93
- each_pair do |k, v|
94
- keys << k if self[k] == val || (v.respond_to?(:include?) && v.include?(val))
123
+ vals.each do |val|
124
+ each_pair do |k, v|
125
+ keys << k if self[k] == val || (v.respond_to?(:include?) && v.include?(val))
126
+ end
95
127
  end
96
128
  keys
97
129
  end
@@ -136,7 +168,12 @@ module FatCore
136
168
  end
137
169
 
138
170
  def <<(other)
139
- merge(other)
171
+ case other
172
+ when Hash
173
+ merge(other)
174
+ when Enumerable
175
+ merge(other.flatten.each_slice(2).to_h)
176
+ end
140
177
  end
141
178
  end
142
179
  end
@@ -153,7 +153,54 @@ module FatCore
153
153
  # Quote self for use in TeX documents. Since number components are not
154
154
  # special to TeX, this just applies `#to_s`
155
155
  def tex_quote
156
- to_s.tex_quote
156
+ case self
157
+ when Float
158
+ if self == Float::INFINITY
159
+ "$\\infty$"
160
+ elsif self == -Float::INFINITY
161
+ "$-\\infty$"
162
+ elsif self == Math::PI
163
+ "$\\pi$"
164
+ elsif self == Math::E
165
+ "$e$"
166
+ else
167
+ to_s.tex_quote
168
+ end
169
+ when Rational
170
+ "$\\frac{#{numerator}}{#{denominator}}$"
171
+ when Complex
172
+ if imaginary.zero?
173
+ real.int_if_whole.tex_quote
174
+ elsif imaginary == 1.0
175
+ if real == Math::PI
176
+ "$\\pi+i$"
177
+ elsif real == Math::E
178
+ "$e+i$"
179
+ else
180
+ "$#{real.int_if_whole}+i$"
181
+ end
182
+ elsif imaginary == Math::PI
183
+ if real == Math::PI
184
+ "$\\pi+\\pi i$"
185
+ elsif real == Math::E
186
+ "$e+\\pi i$"
187
+ else
188
+ "$#{real.int_if_whole}+\\pi i$"
189
+ end
190
+ elsif imaginary == Math::E
191
+ if real == Math::PI
192
+ "$\\pi+e i$"
193
+ elsif real == Math::E
194
+ "$e+e i$"
195
+ else
196
+ "$#{real.int_if_whole}+e i$"
197
+ end
198
+ else
199
+ "$#{real.int_if_whole}+#{imaginary.int_if_whole}i$"
200
+ end
201
+ else
202
+ to_s.tex_quote
203
+ end
157
204
  end
158
205
  end
159
206
  end
@@ -12,8 +12,13 @@
12
12
  # coverage,
13
13
  # 5. provide a definition for sorting Ranges based on sorting by the min values
14
14
  # and sizes of the Ranges.
15
+
16
+ require_relative 'string'
17
+
15
18
  module FatCore
16
19
  module Range
20
+ using StringPred
21
+
17
22
  # @group Operations
18
23
 
19
24
  # Return a range that concatenates this range with other if it is contiguous
@@ -29,9 +34,9 @@ module FatCore
29
34
  # @see contiguous? For the definition of "contiguous"
30
35
  def join(other)
31
36
  if left_contiguous?(other)
32
- ::Range.new(min, other.max)
33
- elsif right_contiguous?(other)
34
37
  ::Range.new(other.min, max)
38
+ elsif right_contiguous?(other)
39
+ ::Range.new(min, other.max)
35
40
  end
36
41
  end
37
42
 
@@ -53,30 +58,48 @@ module FatCore
53
58
  # @param ranges [Array<Range>]
54
59
  # @return [Array<Range>]
55
60
  def gaps(ranges)
56
- if ranges.empty?
57
- [clone]
58
- elsif spanned_by?(ranges)
59
- []
60
- else
61
- # TODO: does not work unless min and max respond to :succ
62
- ranges = ranges.sort_by(&:min)
63
- gaps = []
64
- cur_point = min
65
- ranges.each do |rr|
66
- break if rr.min > max
67
-
68
- if rr.min > cur_point
69
- start_point = cur_point
70
- end_point = rr.min.pred
71
- gaps << (start_point..end_point)
72
- cur_point = rr.max.succ
73
- elsif rr.max >= cur_point
74
- cur_point = rr.max.succ
75
- end
61
+ return [clone] if ranges.empty?
62
+
63
+ msg = "#{ranges.first.min.class} range incompatible with #{min.class} Range"
64
+ raise ArgumentError, msg unless compatible?(ranges)
65
+
66
+ return [] if spanned_by?(ranges)
67
+
68
+ ranges = ranges.select { |r| r.overlaps?(self) }.sort
69
+ self_is_continuous = ranges.map(&:minmax).flatten.any? { |p| !p.respond_to?(:succ) }
70
+ gaps = []
71
+ cur_point = min
72
+ ranges.each do |rr|
73
+ # Loop Invariant: cur_point is the last element in the self Range
74
+ # NOT covered by the given ranges or the gaps so far.
75
+ break if cur_point == max
76
+
77
+ if (self_is_continuous && rr.min > cur_point) ||
78
+ (!self_is_continuous && rr.min > cur_point)
79
+ # There is a gap between the cur_point within self and the start
80
+ # of this range, rr, so we need to record it.
81
+ start_point = cur_point
82
+ end_point = self_is_continuous ? rr.min : rr.min.pred
83
+ gaps << (start_point..end_point)
76
84
  end
77
- gaps << (cur_point..max) if cur_point <= max
78
- gaps
85
+ cur_point =
86
+ if rr.max.is_a?(String) && rr.max[-1].match?(/[Zz9]/)
87
+ # This is a real kludge that stems from the fact that 'z'.succ <
88
+ # 'z', so the test for gaps at the end of the self range
89
+ # believes there is a gap when there is none. This ensures that
90
+ # cur_point is set to something > rr.max when it is one of the
91
+ # problematic strings ending in 'Z', 'z', or '9', all of whose
92
+ # successors sort less than them.
93
+ rr.max + rr.max[-1]
94
+ else
95
+ self_is_continuous ? rr.max : rr.max.succ
96
+ end
79
97
  end
98
+ # Add any gap between the last of the ranges and the end of self.
99
+ if cur_point <= max
100
+ gaps << (cur_point..max)
101
+ end
102
+ gaps
80
103
  end
81
104
 
82
105
  # Within this range return an Array of Ranges representing the overlaps
@@ -90,31 +113,34 @@ module FatCore
90
113
  # @param ranges [Array<Range>] ranges to search for overlaps
91
114
  # @return [Array<Range>] overlaps with ranges but inside this Range
92
115
  def overlaps(ranges)
93
- if ranges.empty? || spanned_by?(ranges)
94
- []
95
- else
96
- ranges = ranges.sort_by(&:min)
97
- overlaps = []
98
- cur_point = nil
99
- ranges.each do |rr|
100
- # Skip ranges outside of self
101
- next if rr.max < min || rr.min > max
102
-
103
- # Initialize cur_point to max of first range
104
- if cur_point.nil?
105
- cur_point = rr.max
106
- next
107
- end
108
- # We are on the second or later range
109
- if rr.min < cur_point
110
- start_point = rr.min
111
- end_point = cur_point
112
- overlaps << (start_point..end_point)
113
- end
116
+ return [] if ranges.empty?
117
+
118
+ msg = "#{ranges.first.min.class} range incompatible with #{min.class} Range"
119
+ raise ArgumentError, msg unless compatible?(ranges)
120
+
121
+ return [] if spanned_by?(ranges)
122
+
123
+ ranges = ranges.sort_by(&:min)
124
+ overlaps = []
125
+ cur_point = nil
126
+ ranges.each do |rr|
127
+ # Skip ranges outside of self
128
+ next if rr.max < min || rr.min > max
129
+
130
+ # Initialize cur_point to max of first range
131
+ if cur_point.nil?
114
132
  cur_point = rr.max
133
+ next
134
+ end
135
+ # We are on the second or later range
136
+ if rr.min < cur_point
137
+ start_point = rr.min
138
+ end_point = cur_point
139
+ overlaps << (start_point..end_point)
115
140
  end
116
- overlaps
141
+ cur_point = rr.max
117
142
  end
143
+ overlaps
118
144
  end
119
145
 
120
146
  # Return a Range that represents the intersection between this range and the
@@ -193,12 +219,24 @@ module FatCore
193
219
  #
194
220
  # @return [String]
195
221
  def tex_quote
196
- to_s.tex_quote
222
+ minq =
223
+ if min.respond_to?(:tex_quote)
224
+ min.tex_quote
225
+ else
226
+ min.to_s
227
+ end
228
+ maxq =
229
+ if max.respond_to?(:tex_quote)
230
+ max.tex_quote
231
+ else
232
+ max.to_s
233
+ end
234
+ "(#{minq}..#{maxq})"
197
235
  end
198
236
 
199
237
  # @group Queries
200
238
 
201
- # Is self on the left of and contiguous to other? Whether one range is
239
+ # Is other on the left of and contiguous to self? Whether one range is
202
240
  # "contiguous" to another has two cases:
203
241
  #
204
242
  # 1. If the elements of the Range on the left respond to the #succ method
@@ -219,14 +257,14 @@ module FatCore
219
257
  # @param other [Range] other range to test for contiguity
220
258
  # @return [Boolean] is self left_contiguous with other
221
259
  def left_contiguous?(other)
222
- if max.respond_to?(:succ)
223
- max.succ == other.min
260
+ if other.max.respond_to?(:succ)
261
+ other.max.succ == min
224
262
  else
225
- max == other.min
263
+ other.max == min
226
264
  end
227
265
  end
228
266
 
229
- # Is self on the right of and contiguous to other? Whether one range is
267
+ # Is other on the right of and contiguous to self? Whether one range is
230
268
  # "contiguous" to another has two cases:
231
269
  #
232
270
  # 1. If the elements of the Range on the left respond to the #succ method
@@ -247,10 +285,10 @@ module FatCore
247
285
  # @param other [Range] other range to test for contiguity
248
286
  # @return [Boolean] is self right_contiguous with other
249
287
  def right_contiguous?(other)
250
- if other.max.respond_to?(:succ)
251
- other.max.succ == min
288
+ if max.respond_to?(:succ)
289
+ max.succ == other.min
252
290
  else
253
- other.max == min
291
+ max == other.min
254
292
  end
255
293
  end
256
294
 
@@ -348,6 +386,11 @@ module FatCore
348
386
  # @param ranges [Array<Range>]
349
387
  # @return [Boolean]
350
388
  def spanned_by?(ranges)
389
+ return empty? if ranges.empty?
390
+
391
+ msg = "#{ranges.first.min.class} range incompatible with #{min.class} Range"
392
+ raise ArgumentError, msg unless compatible?(ranges)
393
+
351
394
  joined_range = nil
352
395
  ranges.sort.each do |r|
353
396
  unless joined_range
@@ -385,6 +428,16 @@ module FatCore
385
428
  [min, max] <=> other.minmax
386
429
  end
387
430
 
431
+ def compatible?(ranges)
432
+ numeric_ok = min.is_a?(Numeric) && max.is_a?(Numeric)
433
+ if numeric_ok
434
+ ranges.map.all? { |r| r.min.is_a?(Numeric) && r.max.is_a?(Numeric) }
435
+ else
436
+ self_class = min.class
437
+ ranges.map.all? { |r| r.min.is_a?(self_class) && r.max.is_a?(self_class) }
438
+ end
439
+ end
440
+
388
441
  module ClassMethods
389
442
  # Return whether any of the `ranges` overlap one another
390
443
  #