red_amber 0.2.2 → 0.3.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +114 -39
  3. data/CHANGELOG.md +203 -31
  4. data/Gemfile +5 -2
  5. data/README.md +62 -29
  6. data/benchmark/basic.yml +86 -0
  7. data/benchmark/combine.yml +62 -0
  8. data/benchmark/dataframe.yml +62 -0
  9. data/benchmark/drop_nil.yml +15 -3
  10. data/benchmark/group.yml +39 -0
  11. data/benchmark/reshape.yml +31 -0
  12. data/benchmark/{csv_load_penguins.yml → rover/csv_load_penguins.yml} +3 -3
  13. data/benchmark/rover/flights.yml +23 -0
  14. data/benchmark/rover/penguins.yml +23 -0
  15. data/benchmark/rover/planes.yml +23 -0
  16. data/benchmark/rover/weather.yml +23 -0
  17. data/benchmark/vector.yml +60 -0
  18. data/doc/DataFrame.md +335 -53
  19. data/doc/Vector.md +91 -0
  20. data/doc/image/dataframe/join.png +0 -0
  21. data/doc/image/dataframe/set_and_bind.png +0 -0
  22. data/doc/image/dataframe_model.png +0 -0
  23. data/lib/red_amber/data_frame.rb +167 -51
  24. data/lib/red_amber/data_frame_combinable.rb +486 -0
  25. data/lib/red_amber/data_frame_displayable.rb +6 -4
  26. data/lib/red_amber/data_frame_indexable.rb +2 -2
  27. data/lib/red_amber/data_frame_loadsave.rb +4 -1
  28. data/lib/red_amber/data_frame_reshaping.rb +35 -10
  29. data/lib/red_amber/data_frame_selectable.rb +221 -116
  30. data/lib/red_amber/data_frame_variable_operation.rb +146 -82
  31. data/lib/red_amber/group.rb +108 -18
  32. data/lib/red_amber/helper.rb +53 -43
  33. data/lib/red_amber/refinements.rb +199 -0
  34. data/lib/red_amber/vector.rb +56 -46
  35. data/lib/red_amber/vector_functions.rb +23 -83
  36. data/lib/red_amber/vector_selectable.rb +116 -69
  37. data/lib/red_amber/vector_updatable.rb +189 -65
  38. data/lib/red_amber/version.rb +1 -1
  39. data/lib/red_amber.rb +3 -0
  40. data/red_amber.gemspec +4 -3
  41. metadata +24 -10
@@ -4,91 +4,122 @@
4
4
  # reference: https://arrow.apache.org/docs/cpp/compute.html
5
5
 
6
6
  module RedAmber
7
- # mix-ins for class Vector
8
- # Functions to select some data.
7
+ # mix-in for class Vector
8
+ # Functions to select some data.
9
9
  module VectorSelectable
10
- def drop_nil
11
- datum = find(:drop_null).execute([data])
12
- Vector.new(datum.value)
13
- end
10
+ using RefineArray
11
+ using RefineArrayLike
12
+
13
+ # Select elements in the self by indices.
14
+ #
15
+ # @param indices [Array<Numeric>, Vector] indices.
16
+ # @yield [Array<Numeric>, Vector] indices.
17
+ # @return [Vector] Vector by selected elements.
18
+ #
19
+ # TODO: support for the option `boundscheck: true`
20
+ def take(*indices, &block)
21
+ if block
22
+ unless indices.empty?
23
+ raise VectorArgumentError, 'Must not specify both arguments and block.'
24
+ end
14
25
 
15
- # vector calculation version of selection by indices
16
- # TODO: support for option {boundscheck: true}
17
- def take(*indices)
18
- indices.flatten!
19
- return Vector.new([]) if indices.empty?
26
+ indices = [yield]
27
+ end
20
28
 
21
- indices = indices[0] if indices.one? && !indices[0].is_a?(Numeric)
22
- indices = Vector.new(indices) unless indices.is_a?(Vector)
29
+ vector =
30
+ case indices
31
+ in [Vector => v] if v.numeric?
32
+ return Vector.create(take_by_vector(v))
33
+ in []
34
+ return Vector.new
35
+ in [(Arrow::Array | Arrow::ChunkedArray) => aa]
36
+ Vector.create(aa)
37
+ else
38
+ Vector.new(indices.flatten)
39
+ end
23
40
 
24
- take_by_vector(indices) # returns sub Vector
41
+ unless vector.numeric?
42
+ raise VectorArgumentError, "argument must be a integers: #{indices}"
43
+ end
44
+
45
+ Vector.create(take_by_vector(vector))
25
46
  end
26
47
 
27
- # TODO: support for option {null_selection_behavior: :drop}
48
+ # Select elements in the self by booleans.
49
+ #
50
+ # @param booleans [Array<true, false, nil>, Vector] booleans.
51
+ # @yield [Array<true, false, nil>, Vector] booleans.
52
+ # @return [Vector] Vector by selected elements.
53
+ #
54
+ # TODO: support for the option `null_selection_behavior: :drop`
28
55
  def filter(*booleans, &block)
29
56
  if block
30
- raise VectorArgumentError, 'Must not specify both arguments and block.' unless booleans.empty?
57
+ unless booleans.empty?
58
+ raise VectorArgumentError, 'Must not specify both arguments and block.'
59
+ end
31
60
 
32
61
  booleans = [yield]
33
62
  end
34
63
 
35
- booleans.flatten!
36
- return Vector.new([]) if booleans.empty?
37
-
38
- b = booleans[0]
39
- boolean_array =
40
- case b
41
- when Vector
42
- raise VectorTypeError, 'Argument is not a boolean.' unless b.boolean?
64
+ case booleans
65
+ in [Vector => v]
66
+ raise VectorTypeError, 'Argument is not a boolean.' unless v.boolean?
43
67
 
44
- b.data
45
- when Arrow::BooleanArray
46
- b
68
+ Vector.create(filter_by_array(v.data))
69
+ in [Arrow::BooleanArray => ba]
70
+ Vector.create(filter_by_array(ba))
71
+ in []
72
+ Vector.new
73
+ else
74
+ booleans.flatten!
75
+ a = Arrow::Array.new(booleans)
76
+ if a.boolean?
77
+ Vector.create(filter_by_array(a))
78
+ elsif booleans.compact.empty? # [nil, nil] becomes string array
79
+ Vector.new
47
80
  else
48
- raise VectorTypeError, 'Argument is not a boolean.' unless booleans?(booleans)
49
-
50
- Arrow::BooleanArray.new(booleans)
81
+ raise VectorTypeError, "Argument is not a boolean: #{booleans}"
51
82
  end
52
-
53
- filter_by_array(boolean_array) # returns sub Vector
83
+ end
54
84
  end
55
85
  alias_method :select, :filter
56
86
  alias_method :find_all, :filter
57
87
 
58
- # @param indices
59
- # @param booleans
88
+ # Select elements in the self by indices or booleans.
89
+ #
90
+ # @param args [Array<Numeric, true, false, nil>, Vector] specifier.
91
+ # @yield [Array<Numeric, true, false, nil>, Vector] specifier.
92
+ # @return [scalar, Array] returns scalar or array.
93
+ #
60
94
  def [](*args)
61
- args.flatten!
62
- return Vector.new([]) if args.empty?
63
-
64
- arg = args[0]
65
- case arg
66
- when Vector
67
- return take_by_vector(arg) if arg.numeric?
68
- return filter_by_array(arg.data) if arg.boolean?
69
-
70
- raise VectorTypeError, "Argument must be numeric or boolean: #{arg}"
71
- when Arrow::BooleanArray
72
- return filter_by_array(arg)
73
- when Arrow::Array
74
- array = arg
75
- when Range
76
- array = normalize_element(arg)
77
- else
78
- unless arg.is_a?(Numeric) || booleans?([arg])
79
- raise VectorArgumentError, "Argument must be numeric or boolean: #{args}"
95
+ array =
96
+ case args
97
+ in [Vector => v]
98
+ return scalar_or_array(take_by_vector(v)) if v.numeric?
99
+ return scalar_or_array(filter_by_array(v.data)) if v.boolean?
100
+
101
+ raise VectorTypeError, "Argument must be numeric or boolean: #{args}"
102
+ in [Arrow::BooleanArray => ba]
103
+ return scalar_or_array(filter_by_array(ba))
104
+ in []
105
+ return nil
106
+ in [Arrow::Array => arrow_array]
107
+ arrow_array
108
+ in [Range => r]
109
+ Arrow::Array.new(parse_range(r, size))
110
+ else
111
+ Arrow::Array.new(args.flatten)
80
112
  end
81
- end
82
- array ||= Arrow::Array.new(args)
83
- return filter_by_array(array) if array.is_a?(Arrow::BooleanArray)
113
+
114
+ return scalar_or_array(filter_by_array(array)) if array.boolean?
84
115
 
85
116
  vector = Vector.new(array)
86
- return take_by_vector(vector) if vector.numeric?
117
+ return scalar_or_array(take_by_vector(vector)) if vector.numeric?
87
118
 
88
119
  raise VectorArgumentError, "Invalid argument: #{args}"
89
120
  end
90
121
 
91
- # @param values [Array, Arrow::Array, Vector]
122
+ # @param values [Array, Arrow::Array, Vector]
92
123
  def is_in(*values)
93
124
  self_data = chunked? ? data.pack : data
94
125
 
@@ -100,7 +131,7 @@ module RedAmber
100
131
  Array(values).flatten
101
132
  end
102
133
 
103
- Vector.new(self_data.is_in(array))
134
+ Vector.create(self_data.is_in(array))
104
135
  end
105
136
 
106
137
  # Arrow's support required
@@ -108,28 +139,44 @@ module RedAmber
108
139
  to_a.index(element)
109
140
  end
110
141
 
142
+ def drop_nil
143
+ datum = find(:drop_null).execute([data])
144
+ Vector.create(datum.value)
145
+ end
146
+
111
147
  private
112
148
 
113
149
  # Accepts indices by numeric Vector
114
150
  def take_by_vector(indices)
115
- raise VectorTypeError, "Indices must be numeric Vector: #{indices}" unless indices.numeric?
116
- raise VectorArgumentError, "Index out of range: #{indices.min}" if indices.min <= -size - 1
151
+ indices = (indices < 0).if_else(indices + size, indices) if (indices < 0).any?
117
152
 
118
- normalized_indices = (indices < 0).if_else(indices + size, indices) # normalize index from tail
119
- raise VectorArgumentError, "Index out of range: #{normalized_indices.max}" if normalized_indices.max >= size
153
+ min, max = indices.min_max
154
+ raise VectorArgumentError, "Index out of range: #{min}" if min < 0
155
+ raise VectorArgumentError, "Index out of range: #{max}" if max >= size
120
156
 
121
- index_array = Arrow::UInt64ArrayBuilder.build(normalized_indices.data) # round to integer array
157
+ index_array =
158
+ if indices.float?
159
+ Arrow::UInt64ArrayBuilder.build(indices.data)
160
+ else
161
+ indices.data
162
+ end
122
163
 
123
- datum = find(:take).execute([data, index_array]) # :array_take will fail with ChunkedArray
124
- Vector.new(datum.value)
164
+ # :array_take will fail with ChunkedArray
165
+ find(:take).execute([data, index_array]).value
125
166
  end
126
167
 
127
168
  # Accepts booleans by Arrow::BooleanArray
128
169
  def filter_by_array(boolean_array)
129
- raise VectorArgumentError, 'Booleans must be same size as self.' unless boolean_array.length == size
170
+ unless boolean_array.length == size
171
+ raise VectorArgumentError, 'Booleans must be same size as self.'
172
+ end
173
+
174
+ find(:array_filter).execute([data, boolean_array]).value
175
+ end
130
176
 
131
- datum = find(:array_filter).execute([data, boolean_array])
132
- Vector.new(datum.value)
177
+ def scalar_or_array(arrow_array)
178
+ a = arrow_array.to_a
179
+ a.size > 1 ? a : a[0]
133
180
  end
134
181
  end
135
182
  end
@@ -4,41 +4,52 @@
4
4
  # reference: https://arrow.apache.org/docs/cpp/compute.html
5
5
 
6
6
  module RedAmber
7
- # mix-ins for class Vector
7
+ # mix-in for class Vector
8
8
  # Functions to make up some data (especially missing) for new data.
9
9
  module VectorUpdatable
10
+ # Add properties to Arrow::Array and Arrow::ChunkedArray
11
+ using RefineArrayLike
12
+
10
13
  # Replace data
11
- # @param arg [Array, Vector, Arrow::Array] index specifier or boolean
12
- # @param replacer [Array, Vector, Arrow::Array] new data to replace for.
14
+ # @param specifier [Array, Vector, Arrow::Array] index or booleans.
15
+ # @param replacer [Scalar, Array, Vector, Arrow::Array] new data to replace for.
13
16
  # @return [Vector] Replaced new Vector.
14
- # If arg has no true, return self.
15
- def replace(args, replacer)
16
- args =
17
- case args
18
- when Array
19
- args
20
- when Range
21
- normalize_element(args)
22
- else
23
- Array(args)
24
- end
25
- replacer = Array(replacer)
26
- return self if args.empty? || args[0].nil?
17
+ # If specifier has no true, return self.
18
+ #
19
+ def replace(specifier, replacer)
20
+ vector = Vector.new(parse_args(Array(specifier), size))
21
+ return self if vector.empty? || empty?
27
22
 
28
- vector = parse_to_vector(args)
29
- replacer = nil if replacer.empty?
30
23
  booleans =
31
24
  if vector.boolean?
32
- return self unless vector.any
33
-
34
25
  vector
35
26
  elsif vector.numeric?
36
- replacer.sort_by! { |x| args[replacer.index(x)] } if replacer # rubocop:disable Style/SafeNavigation
37
27
  Vector.new(indices).is_in(vector)
38
28
  else
39
- raise VectorArgumentError, "Invalid data type #{args}"
29
+ raise VectorArgumentError, "Invalid data type #{specifier}"
30
+ end
31
+ return self if booleans.sum.zero?
32
+
33
+ replacer_array =
34
+ case replacer
35
+ in []
36
+ return self
37
+ in nil | [nil]
38
+ return replace_to_nil(booleans.data)
39
+ in Arrow::Array
40
+ # nop
41
+ in Vector
42
+ replacer.data
43
+ in Array
44
+ Arrow::Array.new(replacer)
45
+ else # Broadcast scalar to Array
46
+ Arrow::Array.new(Array(replacer) * booleans.to_a.count(true))
40
47
  end
41
- replace_with(booleans, replacer)
48
+ if booleans.sum != replacer_array.length
49
+ raise VectorArgumentError, 'Replacements size unmatch'
50
+ end
51
+
52
+ replace_with(booleans.data, replacer_array)
42
53
  end
43
54
 
44
55
  # (related functions)
@@ -51,7 +62,7 @@ module RedAmber
51
62
  raise VectorTypeError, 'Reciever must be a boolean' unless boolean?
52
63
 
53
64
  datum = find(:if_else).execute([data, true_choice, false_choice])
54
- Vector.new(datum.value)
65
+ Vector.create(datum.value)
55
66
  end
56
67
 
57
68
  # same behavior as Ruby's invert
@@ -74,55 +85,168 @@ module RedAmber
74
85
  end
75
86
  end
76
87
 
77
- private
88
+ # Split string Vector and returns Array of columns.
89
+ #
90
+ # @param sep [nil, String, Regexp] separater.
91
+ # If separator is nil (or no argeument given), the column will be splitted by
92
+ # Arrow's split function using any ASCII whitespace.
93
+ # Otherwise sep will passed to String#split.
94
+ # @param limit [Integer] maximum number to limit separation. Passed to String#split.
95
+ # @return [Array<Vector>] an Array of Vectors.
96
+ # @note nil will separated as nil's at same row. ex) `nil => [nil, nil]`
97
+ #
98
+ def split_to_columns(sep = nil, limit = 0)
99
+ l = split(sep, limit)
100
+ l.list_separate
101
+ end
102
+
103
+ # Split string Vector and flatten into rows.
104
+ #
105
+ # @param sep [nil, String, Regexp] separater.
106
+ # If separator is nil (or no argeument given), the column will be splitted by
107
+ # Arrow's split function using any ASCII whitespace.
108
+ # Otherwise sep will passed to String#split.
109
+ # @param limit [Integer] maximum number to limit separation. Passed to String#split.
110
+ # @return [Vector] a flatten Vector.
111
+ # @note nil will separated as nil's at same row. ex) `nil => [nil, nil]`
112
+ #
113
+ def split_to_rows(sep = nil, limit = 0)
114
+ l = split(sep, limit)
115
+ l.list_flatten
116
+ end
117
+
118
+ # return element size Array for list Vector.
119
+ #
120
+ # @api private
121
+ #
122
+ def list_sizes
123
+ Vector.create find(:list_value_length).execute([data]).value
124
+ end
125
+
126
+ # Separate list Vector by columns.
127
+ #
128
+ # @api private
129
+ #
130
+ def list_separate
131
+ len = list_sizes.data
132
+ min, max = Arrow::Function.find(:min_max).execute([len]).value.value.map(&:value)
133
+
134
+ result = []
135
+ (0...min).each do |i|
136
+ result << Vector.create(find(:list_element).execute([data, i]).value)
137
+ end
138
+ return result if min == max
139
+
140
+ (min...max).each do |i|
141
+ result << Vector.new(data.map { |e| e&.[](i) })
142
+ end
143
+ result
144
+ end
78
145
 
79
- # [Ternary]: replace_with(booleans, replacements) => vector
80
- # Replace items selected with a boolean mask
81
- #
82
- # (from Arrow C++ inline doc.)
83
- # Given an array and a boolean mask (either scalar or of equal length),
84
- # along with replacement values (either scalar or array),
85
- # each element of the array for which the corresponding mask element is
86
- # true will be replaced by the next value from the replacements,
87
- # or with null if the mask is null.
88
- # Hence, for replacement arrays, len(replacements) == sum(mask == true).
89
-
90
- def replace_with(booleans, replacer = nil)
91
- specifier =
92
- if booleans.is_a?(Arrow::BooleanArray)
93
- booleans
94
- elsif booleans.is_a?(Vector) && booleans.boolean?
95
- booleans.data
96
- elsif booleans.is_a?(Array) && booleans?(booleans)
97
- Arrow::BooleanArray.new(booleans)
146
+ # Flatten list Vector for rows.
147
+ #
148
+ # @api private
149
+ #
150
+ def list_flatten
151
+ Vector.create find(:list_flatten).execute([data]).value
152
+ end
153
+
154
+ # Split string Vector by each element with separator and returns list Array.
155
+ #
156
+ # @note if sep is not specified, use Arrow's ascii_split_whitespace.
157
+ # It will separate string by ascii whitespaces.
158
+ # @note if sep specified, sep and limit will passed to String#split.
159
+ #
160
+ def split(sep = nil, limit = 0)
161
+ if empty? || !string?
162
+ raise VectorTypeError, "self is not a valid string Vector: #{self}"
163
+ end
164
+ if self[0].nil? && uniq.to_a == [nil] # Avoid heavy check to be activated always.
165
+ raise VectorTypeError, 'self contains only nil'
166
+ end
167
+
168
+ list =
169
+ if sep
170
+ Arrow::Array.new(to_a.map { |e| e&.split(sep, limit) })
98
171
  else
99
- raise VectorTypeError, 'Not a valid type'
172
+ find(:ascii_split_whitespace).execute([data]).value
100
173
  end
101
- raise VectorArgumentError, 'Booleans size unmatch' if specifier.length != size
102
- raise VectorArgumentError, 'Booleans not have any `true`' unless specifier.any?
103
-
104
- r = Array(replacer) # scalar to [scalar]
105
- r = [nil] if r.empty?
106
-
107
- replacer =
108
- if r.size == 1
109
- case replacer
110
- when Arrow::Array then replacer
111
- when Vector then replacer.data
112
- else
113
- Arrow::Array.new(r * specifier.to_a.count(true)) # broadcast
114
- end
174
+ Vector.create(list)
175
+ end
176
+
177
+ # Merge String or other string Vector to self.
178
+ # Self must be a string Vector.
179
+ #
180
+ # @param other [String, Vector]
181
+ # merger from right. It will be broadcasted if it is a scalar String.
182
+ # @param sep [String] separator.
183
+ # @return [Vector] merged Vector
184
+ #
185
+ def merge(other, sep: ' ')
186
+ if empty? || !string?
187
+ raise VectorTypeError,
188
+ "self is not a string Vector: #{self}"
189
+ end
190
+ unless sep.is_a?(String)
191
+ raise VectorArgumentError, "separator is not a String: #{sep}"
192
+ end
193
+
194
+ other_array =
195
+ case other
196
+ in String => s
197
+ [s] * size
198
+ in (Vector | Arrow::Array | Arrow::ChunkedArray) => x if x.string?
199
+ x.to_a
115
200
  else
116
- Arrow::Array.new(r)
201
+ raise VectorArgumentError,
202
+ "other is not a String or a string Vector: #{self}"
117
203
  end
118
- replacer = data.class.new(replacer) if replacer.uniq == [nil]
119
204
 
120
- raise VectorArgumentError, 'Replacements size unmatch' if Array(specifier).count(true) != replacer.length
205
+ list = Arrow::Array.new(to_a.zip(other_array))
206
+ datum = find(:binary_join).execute([list, sep])
207
+ Vector.create(datum.value)
208
+ end
209
+
210
+ private
211
+
212
+ # Replace elements selected with a boolean mask
213
+ #
214
+ # @param boolean_mask [Arrow::BooleanArray]
215
+ # Boolean mask which indicates the position to be replaced.
216
+ # - Position with true will be replaced.
217
+ # - Position with nil will be nil.
218
+ #
219
+ # @param replacer [Arrow::Array] Values after replaced
220
+ # (either scalar or array). If Array is given, original values are replaced by
221
+ # each element of the array at the corresponding position of mask element.
222
+ # - `replacer.size` must be equal to `mask.count(true)`.
223
+ # - Types of self and replacer must be same
224
+ #
225
+ # @return [Vector] Replaced vector.
226
+ # Type of returned Vector is upcasted if needed.
227
+ #
228
+ def replace_with(boolean_mask, replacer)
229
+ raise VectorArgumentError, 'Booleans size unmatch' if boolean_mask.length != size
230
+ raise VectorArgumentError, 'Booleans not have any `true`' unless boolean_mask.any?
231
+
232
+ values = replacer.class.new(data) # Upcast
121
233
 
122
- values = replacer.class.new(data)
234
+ datum = find(:replace_with_mask).execute([values, boolean_mask, replacer])
235
+ Vector.create(datum.value)
236
+ end
123
237
 
124
- datum = find('replace_with_mask').execute([values, specifier, replacer])
125
- Vector.new(datum.value)
238
+ # Replace elements selected with a boolean mask by nil
239
+ #
240
+ # @param boolean_mask [Arrow::BooleanArray]
241
+ # Boolean mask which indicates the position to be replaced.
242
+ # - Position with true will be replaced by nil
243
+ # - Position with nil will remain as nil.
244
+ # @return [Vector] Replaced vector.
245
+ #
246
+ def replace_to_nil(boolean_mask)
247
+ nil_array = data.class.new([nil] * size) # Casted nil Array
248
+ datum = find(:if_else).execute([boolean_mask, nil_array, data])
249
+ Vector.create(datum.value)
126
250
  end
127
251
  end
128
252
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RedAmber
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/red_amber.rb CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  require 'arrow'
4
4
 
5
+ require_relative 'red_amber/refinements'
5
6
  require_relative 'red_amber/helper'
7
+
8
+ require_relative 'red_amber/data_frame_combinable'
6
9
  require_relative 'red_amber/data_frame_displayable'
7
10
  require_relative 'red_amber/data_frame_indexable'
8
11
  require_relative 'red_amber/data_frame_loadsave'
data/red_amber.gemspec CHANGED
@@ -9,10 +9,11 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['heronshoes877@gmail.com']
10
10
 
11
11
  spec.summary = 'Simple dataframe library for Ruby'
12
- spec.description = 'RedAmber is a simple dataframe library inspired by Rover-df and powered by Red Arrow.'
12
+ spec.description = 'RedAmber is a simple dataframe library' \
13
+ 'inspired by Rover-df and powered by Red Arrow.'
13
14
  spec.homepage = 'https://github.com/heronshoes/red_amber'
14
15
  spec.license = 'MIT'
15
- spec.required_ruby_version = '>= 2.7'
16
+ spec.required_ruby_version = '>= 3.0'
16
17
 
17
18
  spec.metadata['homepage_uri'] = spec.homepage
18
19
  spec.metadata['source_code_uri'] = 'https://github.com/heronshoes/red_amber'
@@ -30,7 +31,7 @@ Gem::Specification.new do |spec|
30
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
32
  spec.require_paths = ['lib']
32
33
 
33
- spec.add_dependency 'red-arrow', '>= 9.0.0'
34
+ spec.add_dependency 'red-arrow', '~> 10.0.0'
34
35
 
35
36
  # Development dependency has gone to the Gemfile (rubygems/bundler#7237)
36
37