amplitude-experiment 1.6.0 → 1.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d05f9400e7dd25e87eb6f372c0c1c7fa5c083fe392a75dd25152e0ad0c010a1
4
- data.tar.gz: 19df424d3a56c5a44d6495282f3e1f5022e1a50c47b4a5e0fcfc53c0535576ea
3
+ metadata.gz: 6562a2b13fb38d240e3d83762e442a558478db59f3fff36bdff10b6589cfab0d
4
+ data.tar.gz: 432ea0ff64724c882ad97aae3ce95ae8d8a203f9c90bfe34afd5629e00dbba28
5
5
  SHA512:
6
- metadata.gz: 4e2e412e261f918990f23890200e9dc7b2cf0ed5b52e38fe585f10a3bd84f3b2d062c21a60dbf6811fdf27c395635f214f433d5dfb77c5573e3ae7e82a803352
7
- data.tar.gz: 3bde003e6e5cb0d8b139218d408c5235949bed823f6973fb3f45f3eb542d7adfc7cdfe74f416bf62f06d6cd910eace6490bb6e4e3e6a0b7ef8f4abe686c854b4
6
+ metadata.gz: 507368b7f4db8a28dfc866e0df203f724a1d2911af19dab24fada0bb7327e362d185057c891d534421c7816ec12953ad6555a0252b9bfa7d4922ffaea16bc935
7
+ data.tar.gz: d7ad16548702c918919a9d3b4afe874d396c8c5e98fb5010ebb3f8c0f0b476ad86d4568ed4660a74040a5ee85d4a9e01197560039b19d94659ec0e38d67b0868
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ['lib']
19
19
  spec.extra_rdoc_files = ['README.md']
20
20
 
21
- spec.add_dependency 'concurrent-ruby', '~> 1.2.2'
21
+ spec.add_dependency 'concurrent-ruby', '~> 1.2'
22
22
 
23
23
  spec.add_development_dependency 'psych', '~> 4.0'
24
24
  spec.add_development_dependency 'rake', '~> 13.0'
@@ -87,7 +87,7 @@ module AmplitudeAnalytics
87
87
  @configuration.callback.call(event, code, message) if @configuration.callback.respond_to?(:call)
88
88
  event.callback(code, message)
89
89
  rescue StandardError => e
90
- @configuration.logger.exception("Error callback for event #{event}: #{e.message}")
90
+ @configuration.logger.error("Error callback for event #{event}: #{e.message}")
91
91
  end
92
92
  end
93
93
 
@@ -49,7 +49,7 @@ module AmplitudeAnalytics
49
49
  @plugins[PluginType::DESTINATION].each do |destination|
50
50
  destination_futures << destination.flush
51
51
  rescue StandardError
52
- logger.exception('Error for flush events')
52
+ logger.error('Error for flush events')
53
53
  end
54
54
  end
55
55
  destination_futures
@@ -27,12 +27,7 @@ require 'experiment/cohort/cohort_storage'
27
27
  require 'experiment/cohort/cohort_sync_config'
28
28
  require 'experiment/deployment/deployment_runner'
29
29
  require 'experiment/util/poller'
30
- require 'experiment/evaluation/evaluation'
31
- require 'experiment/evaluation/flag'
32
- require 'experiment/evaluation/murmur3'
33
- require 'experiment/evaluation/select'
34
- require 'experiment/evaluation/semantic_version'
35
- require 'experiment/evaluation/topological_sort'
30
+ require 'experiment/evaluation'
36
31
 
37
32
  # Amplitude Experiment Module
38
33
  module AmplitudeExperiment
@@ -1,311 +1,313 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evaluation
4
- # Engine for evaluating feature flags based on context
5
- class Engine
6
- def evaluate(context, flags)
7
- results = {}
8
- target = {
9
- 'context' => context,
10
- 'result' => results
11
- }
12
-
13
- flags.each do |flag|
14
- variant = evaluate_flag(target, flag)
15
- results[flag.key] = variant if variant
16
- end
3
+ module AmplitudeExperiment
4
+ module Evaluation
5
+ # Engine for evaluating feature flags based on context
6
+ class Engine
7
+ def evaluate(context, flags)
8
+ results = {}
9
+ target = {
10
+ 'context' => context,
11
+ 'result' => results
12
+ }
13
+
14
+ flags.each do |flag|
15
+ variant = evaluate_flag(target, flag)
16
+ results[flag.key] = variant if variant
17
+ end
17
18
 
18
- results
19
- end
19
+ results
20
+ end
20
21
 
21
- private
22
-
23
- def evaluate_flag(target, flag)
24
- result = nil
25
- flag.segments.each do |segment|
26
- result = evaluate_segment(target, flag, segment)
27
- next unless result
28
-
29
- # Merge all metadata into the result
30
- metadata = {}
31
- metadata.merge!(flag.metadata) if flag.metadata
32
- metadata.merge!(segment.metadata) if segment.metadata
33
- metadata.merge!(result.metadata) if result.metadata
34
- result.metadata = metadata
35
- break
22
+ private
23
+
24
+ def evaluate_flag(target, flag)
25
+ result = nil
26
+ flag.segments.each do |segment|
27
+ result = evaluate_segment(target, flag, segment)
28
+ next unless result
29
+
30
+ # Merge all metadata into the result
31
+ metadata = {}
32
+ metadata.merge!(flag.metadata) if flag.metadata
33
+ metadata.merge!(segment.metadata) if segment.metadata
34
+ metadata.merge!(result.metadata) if result.metadata
35
+ result.metadata = metadata
36
+ break
37
+ end
38
+ result
36
39
  end
37
- result
38
- end
39
40
 
40
- def evaluate_segment(target, flag, segment)
41
- if segment.conditions
42
- match = evaluate_conditions(target, segment.conditions)
43
- if match
41
+ def evaluate_segment(target, flag, segment)
42
+ if segment.conditions
43
+ match = evaluate_conditions(target, segment.conditions)
44
+ if match
45
+ variant_key = bucket(target, segment)
46
+ variant_key ? flag.variants[variant_key] : nil
47
+ end
48
+ else
49
+ # Null conditions always match
44
50
  variant_key = bucket(target, segment)
45
51
  variant_key ? flag.variants[variant_key] : nil
46
52
  end
47
- else
48
- # Null conditions always match
49
- variant_key = bucket(target, segment)
50
- variant_key ? flag.variants[variant_key] : nil
51
53
  end
52
- end
53
54
 
54
- def evaluate_conditions(target, conditions)
55
- # Outer list logic is "or" (||)
56
- conditions.any? do |inner_conditions|
57
- match = true
58
- inner_conditions.each do |condition|
59
- match = match_condition(target, condition)
60
- break unless match
55
+ def evaluate_conditions(target, conditions)
56
+ # Outer list logic is "or" (||)
57
+ conditions.any? do |inner_conditions|
58
+ match = true
59
+ inner_conditions.each do |condition|
60
+ match = match_condition(target, condition)
61
+ break unless match
62
+ end
63
+ match
61
64
  end
62
- match
63
65
  end
64
- end
65
66
 
66
- def match_condition(target, condition)
67
- prop_value = Evaluation.select(target, condition.selector)
68
- # Special matching for null properties and set type prop values and operators
69
- if !prop_value
70
- match_null(condition.op, condition.values)
71
- elsif set_operator?(condition.op)
72
- prop_value_string_list = coerce_string_array(prop_value)
73
- return false unless prop_value_string_list
74
-
75
- match_set(prop_value_string_list, condition.op, condition.values)
76
- else
77
- prop_value_string = coerce_string(prop_value)
78
- if prop_value_string
79
- match_string(prop_value_string, condition.op, condition.values)
67
+ def match_condition(target, condition)
68
+ prop_value = Evaluation.select(target, condition.selector)
69
+ # Special matching for null properties and set type prop values and operators
70
+ if !prop_value
71
+ match_null(condition.op, condition.values)
72
+ elsif set_operator?(condition.op)
73
+ prop_value_string_list = coerce_string_array(prop_value)
74
+ return false unless prop_value_string_list
75
+
76
+ match_set(prop_value_string_list, condition.op, condition.values)
80
77
  else
81
- false
78
+ prop_value_string = coerce_string(prop_value)
79
+ if prop_value_string
80
+ match_string(prop_value_string, condition.op, condition.values)
81
+ else
82
+ false
83
+ end
82
84
  end
83
85
  end
84
- end
85
-
86
- def get_hash(key)
87
- Murmur3.hash32x86(key)
88
- end
89
86
 
90
- def bucket(target, segment)
91
- unless segment.bucket
92
- # Null bucket means segment is fully rolled out
93
- return segment.variant
87
+ def get_hash(key)
88
+ Murmur3.hash32x86(key)
94
89
  end
95
90
 
96
- bucketing_value = coerce_string(Evaluation.select(target, segment.bucket.selector))
97
- if !bucketing_value || bucketing_value.empty?
98
- # Null or empty bucketing value cannot be bucketed
99
- return segment.variant
100
- end
91
+ def bucket(target, segment)
92
+ unless segment.bucket
93
+ # Null bucket means segment is fully rolled out
94
+ return segment.variant
95
+ end
101
96
 
102
- key_to_hash = "#{segment.bucket.salt}/#{bucketing_value}"
103
- hash = get_hash(key_to_hash)
104
- allocation_value = hash % 100
105
- distribution_value = (hash / 100).floor
97
+ bucketing_value = coerce_string(Evaluation.select(target, segment.bucket.selector))
98
+ if !bucketing_value || bucketing_value.empty?
99
+ # Null or empty bucketing value cannot be bucketed
100
+ return segment.variant
101
+ end
102
+
103
+ key_to_hash = "#{segment.bucket.salt}/#{bucketing_value}"
104
+ hash = get_hash(key_to_hash)
105
+ allocation_value = hash % 100
106
+ distribution_value = (hash / 100).floor
106
107
 
107
- segment.bucket.allocations.each do |allocation|
108
- allocation_start = allocation.range[0]
109
- allocation_end = allocation.range[1]
110
- next unless allocation_value >= allocation_start && allocation_value < allocation_end
108
+ segment.bucket.allocations.each do |allocation|
109
+ allocation_start = allocation.range[0]
110
+ allocation_end = allocation.range[1]
111
+ next unless allocation_value >= allocation_start && allocation_value < allocation_end
111
112
 
112
- allocation.distributions.each do |distribution|
113
- distribution_start = distribution.range[0]
114
- distribution_end = distribution.range[1]
115
- return distribution.variant if distribution_value >= distribution_start && distribution_value < distribution_end
113
+ allocation.distributions.each do |distribution|
114
+ distribution_start = distribution.range[0]
115
+ distribution_end = distribution.range[1]
116
+ return distribution.variant if distribution_value >= distribution_start && distribution_value < distribution_end
117
+ end
116
118
  end
117
- end
118
119
 
119
- segment.variant
120
- end
120
+ segment.variant
121
+ end
121
122
 
122
- def match_null(op, filter_values)
123
- contains_none = contains_none?(filter_values)
124
- case op
125
- when Operator::IS, Operator::CONTAINS, Operator::LESS_THAN,
126
- Operator::LESS_THAN_EQUALS, Operator::GREATER_THAN,
127
- Operator::GREATER_THAN_EQUALS, Operator::VERSION_LESS_THAN,
128
- Operator::VERSION_LESS_THAN_EQUALS, Operator::VERSION_GREATER_THAN,
129
- Operator::VERSION_GREATER_THAN_EQUALS, Operator::SET_IS,
130
- Operator::SET_CONTAINS, Operator::SET_CONTAINS_ANY
131
- contains_none
132
- when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN,
133
- Operator::SET_DOES_NOT_CONTAIN, Operator::SET_DOES_NOT_CONTAIN_ANY
134
- !contains_none
135
- else
136
- false
123
+ def match_null(op, filter_values)
124
+ contains_none = contains_none?(filter_values)
125
+ case op
126
+ when Operator::IS, Operator::CONTAINS, Operator::LESS_THAN,
127
+ Operator::LESS_THAN_EQUALS, Operator::GREATER_THAN,
128
+ Operator::GREATER_THAN_EQUALS, Operator::VERSION_LESS_THAN,
129
+ Operator::VERSION_LESS_THAN_EQUALS, Operator::VERSION_GREATER_THAN,
130
+ Operator::VERSION_GREATER_THAN_EQUALS, Operator::SET_IS,
131
+ Operator::SET_CONTAINS, Operator::SET_CONTAINS_ANY
132
+ contains_none
133
+ when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN,
134
+ Operator::SET_DOES_NOT_CONTAIN, Operator::SET_DOES_NOT_CONTAIN_ANY
135
+ !contains_none
136
+ else
137
+ false
138
+ end
137
139
  end
138
- end
139
140
 
140
- def match_set(prop_values, op, filter_values)
141
- case op
142
- when Operator::SET_IS
143
- set_equals?(prop_values, filter_values)
144
- when Operator::SET_IS_NOT
145
- !set_equals?(prop_values, filter_values)
146
- when Operator::SET_CONTAINS
147
- matches_set_contains_all?(prop_values, filter_values)
148
- when Operator::SET_DOES_NOT_CONTAIN
149
- !matches_set_contains_all?(prop_values, filter_values)
150
- when Operator::SET_CONTAINS_ANY
151
- matches_set_contains_any?(prop_values, filter_values)
152
- when Operator::SET_DOES_NOT_CONTAIN_ANY
153
- !matches_set_contains_any?(prop_values, filter_values)
154
- else
155
- false
141
+ def match_set(prop_values, op, filter_values)
142
+ case op
143
+ when Operator::SET_IS
144
+ set_equals?(prop_values, filter_values)
145
+ when Operator::SET_IS_NOT
146
+ !set_equals?(prop_values, filter_values)
147
+ when Operator::SET_CONTAINS
148
+ matches_set_contains_all?(prop_values, filter_values)
149
+ when Operator::SET_DOES_NOT_CONTAIN
150
+ !matches_set_contains_all?(prop_values, filter_values)
151
+ when Operator::SET_CONTAINS_ANY
152
+ matches_set_contains_any?(prop_values, filter_values)
153
+ when Operator::SET_DOES_NOT_CONTAIN_ANY
154
+ !matches_set_contains_any?(prop_values, filter_values)
155
+ else
156
+ false
157
+ end
156
158
  end
157
- end
158
159
 
159
- def match_string(prop_value, op, filter_values)
160
- case op
161
- when Operator::IS
162
- matches_is?(prop_value, filter_values)
163
- when Operator::IS_NOT
164
- !matches_is?(prop_value, filter_values)
165
- when Operator::CONTAINS
166
- matches_contains?(prop_value, filter_values)
167
- when Operator::DOES_NOT_CONTAIN
168
- !matches_contains?(prop_value, filter_values)
169
- when Operator::LESS_THAN, Operator::LESS_THAN_EQUALS,
170
- Operator::GREATER_THAN, Operator::GREATER_THAN_EQUALS
171
- matches_comparable?(prop_value, op, filter_values,
172
- method(:parse_number),
173
- method(:comparator))
174
- when Operator::VERSION_LESS_THAN, Operator::VERSION_LESS_THAN_EQUALS,
175
- Operator::VERSION_GREATER_THAN, Operator::VERSION_GREATER_THAN_EQUALS
176
- matches_comparable?(prop_value, op, filter_values,
177
- SemanticVersion.method(:parse),
178
- method(:comparator))
179
- when Operator::REGEX_MATCH
180
- matches_regex?(prop_value, filter_values)
181
- when Operator::REGEX_DOES_NOT_MATCH
182
- !matches_regex?(prop_value, filter_values)
183
- else
184
- false
160
+ def match_string(prop_value, op, filter_values)
161
+ case op
162
+ when Operator::IS
163
+ matches_is?(prop_value, filter_values)
164
+ when Operator::IS_NOT
165
+ !matches_is?(prop_value, filter_values)
166
+ when Operator::CONTAINS
167
+ matches_contains?(prop_value, filter_values)
168
+ when Operator::DOES_NOT_CONTAIN
169
+ !matches_contains?(prop_value, filter_values)
170
+ when Operator::LESS_THAN, Operator::LESS_THAN_EQUALS,
171
+ Operator::GREATER_THAN, Operator::GREATER_THAN_EQUALS
172
+ matches_comparable?(prop_value, op, filter_values,
173
+ method(:parse_number),
174
+ method(:comparator))
175
+ when Operator::VERSION_LESS_THAN, Operator::VERSION_LESS_THAN_EQUALS,
176
+ Operator::VERSION_GREATER_THAN, Operator::VERSION_GREATER_THAN_EQUALS
177
+ matches_comparable?(prop_value, op, filter_values,
178
+ SemanticVersion.method(:parse),
179
+ method(:comparator))
180
+ when Operator::REGEX_MATCH
181
+ matches_regex?(prop_value, filter_values)
182
+ when Operator::REGEX_DOES_NOT_MATCH
183
+ !matches_regex?(prop_value, filter_values)
184
+ else
185
+ false
186
+ end
185
187
  end
186
- end
187
188
 
188
- def matches_is?(prop_value, filter_values)
189
- if contains_booleans?(filter_values)
190
- lower = prop_value.downcase
191
- return filter_values.any? { |value| value.downcase == lower } if %w[true false].include?(lower)
189
+ def matches_is?(prop_value, filter_values)
190
+ if contains_booleans?(filter_values)
191
+ lower = prop_value.downcase
192
+ return filter_values.any? { |value| value.downcase == lower } if %w[true false].include?(lower)
193
+ end
194
+ filter_values.any? { |value| prop_value == value }
192
195
  end
193
- filter_values.any? { |value| prop_value == value }
194
- end
195
196
 
196
- def matches_contains?(prop_value, filter_values)
197
- filter_values.any? do |filter_value|
198
- prop_value.downcase.include?(filter_value.downcase)
197
+ def matches_contains?(prop_value, filter_values)
198
+ filter_values.any? do |filter_value|
199
+ prop_value.downcase.include?(filter_value.downcase)
200
+ end
199
201
  end
200
- end
201
202
 
202
- def matches_comparable?(prop_value, op, filter_values, type_transformer, type_comparator)
203
- prop_value_transformed = type_transformer.call(prop_value)
204
- filter_values_transformed = filter_values
205
- .map { |filter_value| type_transformer.call(filter_value) }
206
- .compact
207
-
208
- if !prop_value_transformed || filter_values_transformed.empty?
209
- filter_values.any? { |filter_value| comparator(prop_value, op, filter_value) }
210
- else
211
- filter_values_transformed.any? do |filter_value_transformed|
212
- type_comparator.call(prop_value_transformed, op, filter_value_transformed)
203
+ def matches_comparable?(prop_value, op, filter_values, type_transformer, type_comparator)
204
+ prop_value_transformed = type_transformer.call(prop_value)
205
+ filter_values_transformed = filter_values
206
+ .map { |filter_value| type_transformer.call(filter_value) }
207
+ .compact
208
+
209
+ if !prop_value_transformed || filter_values_transformed.empty?
210
+ filter_values.any? { |filter_value| comparator(prop_value, op, filter_value) }
211
+ else
212
+ filter_values_transformed.any? do |filter_value_transformed|
213
+ type_comparator.call(prop_value_transformed, op, filter_value_transformed)
214
+ end
213
215
  end
214
216
  end
215
- end
216
217
 
217
- def comparator(prop_value, op, filter_value)
218
- case op
219
- when Operator::LESS_THAN, Operator::VERSION_LESS_THAN
220
- prop_value < filter_value
221
- when Operator::LESS_THAN_EQUALS, Operator::VERSION_LESS_THAN_EQUALS
222
- prop_value <= filter_value
223
- when Operator::GREATER_THAN, Operator::VERSION_GREATER_THAN
224
- prop_value > filter_value
225
- when Operator::GREATER_THAN_EQUALS, Operator::VERSION_GREATER_THAN_EQUALS
226
- prop_value >= filter_value
227
- else
228
- false
218
+ def comparator(prop_value, op, filter_value)
219
+ case op
220
+ when Operator::LESS_THAN, Operator::VERSION_LESS_THAN
221
+ prop_value < filter_value
222
+ when Operator::LESS_THAN_EQUALS, Operator::VERSION_LESS_THAN_EQUALS
223
+ prop_value <= filter_value
224
+ when Operator::GREATER_THAN, Operator::VERSION_GREATER_THAN
225
+ prop_value > filter_value
226
+ when Operator::GREATER_THAN_EQUALS, Operator::VERSION_GREATER_THAN_EQUALS
227
+ prop_value >= filter_value
228
+ else
229
+ false
230
+ end
229
231
  end
230
- end
231
232
 
232
- def matches_regex?(prop_value, filter_values)
233
- filter_values.any? { |filter_value| !!(Regexp.new(filter_value) =~ prop_value) }
234
- end
233
+ def matches_regex?(prop_value, filter_values)
234
+ filter_values.any? { |filter_value| !!(Regexp.new(filter_value) =~ prop_value) }
235
+ end
235
236
 
236
- def contains_none?(filter_values)
237
- filter_values.any? { |filter_value| filter_value == '(none)' }
238
- end
237
+ def contains_none?(filter_values)
238
+ filter_values.any? { |filter_value| filter_value == '(none)' }
239
+ end
239
240
 
240
- def contains_booleans?(filter_values)
241
- filter_values.any? do |filter_value|
242
- case filter_value.downcase
243
- when 'true', 'false'
244
- true
245
- else
246
- false
241
+ def contains_booleans?(filter_values)
242
+ filter_values.any? do |filter_value|
243
+ case filter_value.downcase
244
+ when 'true', 'false'
245
+ true
246
+ else
247
+ false
248
+ end
247
249
  end
248
250
  end
249
- end
250
251
 
251
- def parse_number(value)
252
- Float(value)
253
- rescue StandardError
254
- nil
255
- end
252
+ def parse_number(value)
253
+ Float(value)
254
+ rescue StandardError
255
+ nil
256
+ end
256
257
 
257
- def coerce_string(value)
258
- return nil if value.nil?
259
- return value.to_json if value.is_a?(Hash)
258
+ def coerce_string(value)
259
+ return nil if value.nil?
260
+ return value.to_json if value.is_a?(Hash)
260
261
 
261
- value.to_s
262
- end
262
+ value.to_s
263
+ end
263
264
 
264
- def coerce_string_array(value)
265
- if value.is_a?(Array)
266
- value.map { |e| coerce_string(e) }.compact
267
- else
268
- string_value = value.to_s
269
- begin
270
- parsed_value = JSON.parse(string_value)
271
- if parsed_value.is_a?(Array)
272
- parsed_value.map { |e| coerce_string(e) }.compact
273
- else
265
+ def coerce_string_array(value)
266
+ if value.is_a?(Array)
267
+ value.map { |e| coerce_string(e) }.compact
268
+ else
269
+ string_value = value.to_s
270
+ begin
271
+ parsed_value = JSON.parse(string_value)
272
+ if parsed_value.is_a?(Array)
273
+ parsed_value.map { |e| coerce_string(e) }.compact
274
+ else
275
+ s = coerce_string(string_value)
276
+ s ? [s] : nil
277
+ end
278
+ rescue JSON::ParserError
274
279
  s = coerce_string(string_value)
275
280
  s ? [s] : nil
276
281
  end
277
- rescue JSON::ParserError
278
- s = coerce_string(string_value)
279
- s ? [s] : nil
280
282
  end
281
283
  end
282
- end
283
284
 
284
- def set_operator?(op)
285
- case op
286
- when Operator::SET_IS, Operator::SET_IS_NOT,
287
- Operator::SET_CONTAINS, Operator::SET_DOES_NOT_CONTAIN,
288
- Operator::SET_CONTAINS_ANY, Operator::SET_DOES_NOT_CONTAIN_ANY
289
- true
290
- else
291
- false
285
+ def set_operator?(op)
286
+ case op
287
+ when Operator::SET_IS, Operator::SET_IS_NOT,
288
+ Operator::SET_CONTAINS, Operator::SET_DOES_NOT_CONTAIN,
289
+ Operator::SET_CONTAINS_ANY, Operator::SET_DOES_NOT_CONTAIN_ANY
290
+ true
291
+ else
292
+ false
293
+ end
292
294
  end
293
- end
294
295
 
295
- def set_equals?(xa, ya)
296
- xs = Set.new(xa)
297
- ys = Set.new(ya)
298
- xs.size == ys.size && ys.all? { |y| xs.include?(y) }
299
- end
296
+ def set_equals?(xa, ya)
297
+ xs = Set.new(xa)
298
+ ys = Set.new(ya)
299
+ xs.size == ys.size && ys.all? { |y| xs.include?(y) }
300
+ end
300
301
 
301
- def matches_set_contains_all?(prop_values, filter_values)
302
- return false if prop_values.length < filter_values.length
302
+ def matches_set_contains_all?(prop_values, filter_values)
303
+ return false if prop_values.length < filter_values.length
303
304
 
304
- filter_values.all? { |filter_value| matches_is?(filter_value, prop_values) }
305
- end
305
+ filter_values.all? { |filter_value| matches_is?(filter_value, prop_values) }
306
+ end
306
307
 
307
- def matches_set_contains_any?(prop_values, filter_values)
308
- filter_values.any? { |filter_value| matches_is?(filter_value, prop_values) }
308
+ def matches_set_contains_any?(prop_values, filter_values)
309
+ filter_values.any? { |filter_value| matches_is?(filter_value, prop_values) }
310
+ end
309
311
  end
310
312
  end
311
313
  end