fuzzy_match 1.5.0 → 2.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 +8 -8
- data/.rspec +2 -0
- data/CHANGELOG +14 -0
- data/Gemfile +8 -0
- data/README.markdown +58 -38
- data/Rakefile +0 -9
- data/bin/fuzzy_match +106 -0
- data/fuzzy_match.gemspec +4 -4
- data/groupings-screenshot.png +0 -0
- data/highlevel.graffle +0 -0
- data/highlevel.png +0 -0
- data/lib/fuzzy_match/record.rb +58 -0
- data/lib/fuzzy_match/result.rb +11 -8
- data/lib/fuzzy_match/rule/grouping.rb +70 -12
- data/lib/fuzzy_match/rule/identity.rb +3 -3
- data/lib/fuzzy_match/rule.rb +1 -1
- data/lib/fuzzy_match/score/amatch.rb +0 -4
- data/lib/fuzzy_match/score/pure_ruby.rb +2 -8
- data/lib/fuzzy_match/score.rb +4 -0
- data/lib/fuzzy_match/similarity.rb +10 -32
- data/lib/fuzzy_match/version.rb +1 -1
- data/lib/fuzzy_match.rb +78 -94
- data/{test/test_amatch.rb → spec/amatch_spec.rb} +1 -2
- data/{test/test_cache.rb → spec/cache_spec.rb} +7 -7
- data/spec/foo.rb +9 -0
- data/spec/fuzzy_match_spec.rb +354 -0
- data/spec/grouping_spec.rb +60 -0
- data/spec/identity_spec.rb +29 -0
- data/{test/test_wrapper.rb → spec/record_spec.rb} +3 -7
- data/spec/spec_helper.rb +21 -0
- metadata +56 -50
- data/bin/fuzzy_match_checker +0 -71
- data/examples/bts_aircraft/5-2-A.htm +0 -10305
- data/examples/bts_aircraft/5-2-B.htm +0 -9576
- data/examples/bts_aircraft/5-2-D.htm +0 -7094
- data/examples/bts_aircraft/5-2-E.htm +0 -2349
- data/examples/bts_aircraft/5-2-G.htm +0 -2922
- data/examples/bts_aircraft/groupings.csv +0 -1
- data/examples/bts_aircraft/identities.csv +0 -1
- data/examples/bts_aircraft/negatives.csv +0 -1
- data/examples/bts_aircraft/normalizers.csv +0 -1
- data/examples/bts_aircraft/number_260.csv +0 -334
- data/examples/bts_aircraft/positives.csv +0 -1
- data/examples/bts_aircraft/test_bts_aircraft.rb +0 -116
- data/examples/first_name_matching.rb +0 -15
- data/examples/icao-bts.xls +0 -0
- data/lib/fuzzy_match/rule/normalizer.rb +0 -20
- data/lib/fuzzy_match/rule/stop_word.rb +0 -11
- data/lib/fuzzy_match/wrapper.rb +0 -73
- data/test/helper.rb +0 -12
- data/test/test_fuzzy_match.rb +0 -304
- data/test/test_fuzzy_match_convoluted.rb.disabled +0 -268
- data/test/test_grouping.rb +0 -28
- data/test/test_identity.rb +0 -34
- data/test/test_normalizer.rb +0 -10
@@ -1,11 +1,11 @@
|
|
1
1
|
class FuzzyMatch
|
2
2
|
class Similarity
|
3
|
-
attr_reader :
|
4
|
-
attr_reader :
|
3
|
+
attr_reader :record1
|
4
|
+
attr_reader :record2
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@
|
8
|
-
@
|
6
|
+
def initialize(record1, record2)
|
7
|
+
@record1 = record1
|
8
|
+
@record2 = record2
|
9
9
|
end
|
10
10
|
|
11
11
|
def <=>(other)
|
@@ -18,44 +18,22 @@ class FuzzyMatch
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def best_score
|
21
|
-
@best_score ||= FuzzyMatch.score_class.new(
|
21
|
+
@best_score ||= FuzzyMatch.score_class.new(record1.clean, record2.clean)
|
22
22
|
end
|
23
23
|
|
24
24
|
def satisfy?(needle, threshold)
|
25
25
|
best_score.dices_coefficient_similar > threshold or
|
26
|
-
((
|
27
|
-
(needle.words &
|
26
|
+
((record2.clean.length < 3 or needle.clean.length < 3) and best_score.levenshtein_similar > 0) or
|
27
|
+
(needle.words & record2.words).any?
|
28
28
|
end
|
29
29
|
|
30
30
|
def inspect
|
31
|
-
%{
|
31
|
+
%{#{record2.clean.inspect} ~ #{record1.clean.inspect} => #{best_score.inspect}}
|
32
32
|
end
|
33
33
|
|
34
34
|
# Weight things towards short original strings
|
35
35
|
def original_weight
|
36
|
-
@original_weight ||= (1.0 / (
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def best_wrapper1_variant
|
42
|
-
best_variants[0]
|
43
|
-
end
|
44
|
-
|
45
|
-
def best_wrapper2_variant
|
46
|
-
best_variants[1]
|
47
|
-
end
|
48
|
-
|
49
|
-
def best_variants
|
50
|
-
@best_variants ||= begin
|
51
|
-
wrapper1.variants.product(wrapper2.variants).sort do |tuple1, tuple2|
|
52
|
-
wrapper1_variant1, wrapper2_variant1 = tuple1
|
53
|
-
wrapper1_variant2, wrapper2_variant2 = tuple2
|
54
|
-
score1 = FuzzyMatch.score_class.new wrapper1_variant1, wrapper2_variant1
|
55
|
-
score2 = FuzzyMatch.score_class.new wrapper1_variant2, wrapper2_variant2
|
56
|
-
score1 <=> score2
|
57
|
-
end.last
|
58
|
-
end
|
36
|
+
@original_weight ||= (1.0 / (record1.clean.length * record2.clean.length))
|
59
37
|
end
|
60
38
|
end
|
61
39
|
end
|
data/lib/fuzzy_match/version.rb
CHANGED
data/lib/fuzzy_match.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
require 'fuzzy_match/rule'
|
2
|
-
require 'fuzzy_match/rule/normalizer'
|
3
|
-
require 'fuzzy_match/rule/stop_word'
|
4
2
|
require 'fuzzy_match/rule/grouping'
|
5
3
|
require 'fuzzy_match/rule/identity'
|
6
4
|
require 'fuzzy_match/result'
|
7
|
-
require 'fuzzy_match/
|
5
|
+
require 'fuzzy_match/record'
|
8
6
|
require 'fuzzy_match/similarity'
|
9
7
|
require 'fuzzy_match/score'
|
10
8
|
|
@@ -33,8 +31,8 @@ class FuzzyMatch
|
|
33
31
|
|
34
32
|
DEFAULT_ENGINE = :pure_ruby
|
35
33
|
|
34
|
+
#TODO refactor at least all the :find_X things
|
36
35
|
DEFAULT_OPTIONS = {
|
37
|
-
:first_grouping_decides => false,
|
38
36
|
:must_match_grouping => false,
|
39
37
|
:must_match_at_least_one_word => false,
|
40
38
|
:gather_last_result => false,
|
@@ -42,6 +40,7 @@ class FuzzyMatch
|
|
42
40
|
:find_all_with_score => false,
|
43
41
|
:threshold => 0,
|
44
42
|
:find_best => false,
|
43
|
+
:find_with_score => false,
|
45
44
|
}
|
46
45
|
|
47
46
|
self.engine = DEFAULT_ENGINE
|
@@ -49,7 +48,6 @@ class FuzzyMatch
|
|
49
48
|
attr_reader :haystack
|
50
49
|
attr_reader :groupings
|
51
50
|
attr_reader :identities
|
52
|
-
attr_reader :normalizers
|
53
51
|
attr_reader :stop_words
|
54
52
|
attr_accessor :read
|
55
53
|
attr_reader :default_options
|
@@ -57,7 +55,6 @@ class FuzzyMatch
|
|
57
55
|
# haystack - a bunch of records that will compete to see who best matches the needle
|
58
56
|
#
|
59
57
|
# Rules (can only be specified at initialization or by using a setter)
|
60
|
-
# * :<tt>normalizers</tt> - regexps (see README)
|
61
58
|
# * :<tt>identities</tt> - regexps
|
62
59
|
# * :<tt>groupings</tt> - regexps
|
63
60
|
# * :<tt>stop_words</tt> - regexps
|
@@ -66,70 +63,53 @@ class FuzzyMatch
|
|
66
63
|
# Options (can be specified at initialization or when calling #find)
|
67
64
|
# * :<tt>must_match_grouping</tt> - don't return a match unless the needle fits into one of the groupings you specified
|
68
65
|
# * :<tt>must_match_at_least_one_word</tt> - don't return a match unless the needle shares at least one word with the match
|
69
|
-
# * :<tt>first_grouping_decides</tt> - force records into the first grouping they match, rather than choosing a grouping that will give them a higher score
|
70
66
|
# * :<tt>gather_last_result</tt> - enable <tt>last_result</tt>
|
71
67
|
# * :<tt>threshold</tt> - set a score threshold below which not to return results (not generally recommended - please test the results of setting a threshold thoroughly - one set of results and their scores probably won't be enough to determine the appropriate number). Only checked against the Pair Distance score and ignored when one string or the other is of length 1.
|
72
|
-
def initialize(
|
73
|
-
|
68
|
+
def initialize(haystack, options_and_rules = {})
|
69
|
+
o = options_and_rules.dup
|
74
70
|
|
75
71
|
# rules
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
72
|
+
@read = o.delete(:read) || o.delete(:haystack_reader)
|
73
|
+
@groupings = (o.delete(:groupings) || o.delete(:blockings) || []).map { |regexp| Rule::Grouping.make(regexp) }.flatten
|
74
|
+
@identities = (o.delete(:identities) || []).map { |regexp| Rule::Identity.new(regexp) }
|
75
|
+
@stop_words = o.delete(:stop_words) || []
|
76
|
+
|
82
77
|
# options
|
83
|
-
if deprecated =
|
84
|
-
|
85
|
-
end
|
86
|
-
if deprecated = options_and_rules.delete(:must_match_blocking)
|
87
|
-
options_and_rules[:must_match_grouping] = deprecated
|
78
|
+
if deprecated = o.delete(:must_match_blocking)
|
79
|
+
o[:must_match_grouping] = deprecated
|
88
80
|
end
|
89
|
-
@default_options = DEFAULT_OPTIONS.merge(
|
81
|
+
@default_options = DEFAULT_OPTIONS.merge(o).freeze
|
90
82
|
|
91
|
-
|
92
|
-
self.haystack = competitors
|
93
|
-
end
|
94
|
-
|
95
|
-
def groupings=(ary)
|
96
|
-
@groupings = ary.map { |regexp| Rule::Grouping.new regexp }
|
97
|
-
end
|
98
|
-
|
99
|
-
def identities=(ary)
|
100
|
-
@identities = ary.map { |regexp| Rule::Identity.new regexp }
|
101
|
-
end
|
102
|
-
|
103
|
-
def normalizers=(ary)
|
104
|
-
@normalizers = ary.map { |regexp| Rule::Normalizer.new regexp }
|
105
|
-
end
|
106
|
-
|
107
|
-
def stop_words=(ary)
|
108
|
-
@stop_words = ary.map { |regexp| Rule::StopWord.new regexp }
|
109
|
-
end
|
110
|
-
|
111
|
-
def haystack=(ary)
|
112
|
-
@haystack = ary.map { |competitor| Wrapper.new self, competitor }
|
83
|
+
@haystack = haystack.map { |original| Record.new original, stop_words: @stop_words, read: @read }
|
113
84
|
end
|
114
|
-
|
85
|
+
|
115
86
|
def last_result
|
116
|
-
@last_result
|
87
|
+
@last_result or raise("You can't access the last result until you've run a find with :gather_last_result => true")
|
117
88
|
end
|
118
89
|
|
90
|
+
# Return everything in sorted order
|
119
91
|
def find_all(needle, options = {})
|
120
92
|
options = options.merge(:find_all => true)
|
121
93
|
find needle, options
|
122
94
|
end
|
123
95
|
|
96
|
+
# Return the top results with the same score
|
124
97
|
def find_best(needle, options = {})
|
125
98
|
options = options.merge(:find_best => true)
|
126
99
|
find needle, options
|
127
100
|
end
|
128
101
|
|
102
|
+
# Return everything in sorted order with score
|
129
103
|
def find_all_with_score(needle, options = {})
|
130
104
|
options = options.merge(:find_all_with_score => true)
|
131
105
|
find needle, options
|
132
106
|
end
|
107
|
+
|
108
|
+
# Return one with score
|
109
|
+
def find_with_score(needle, options = {})
|
110
|
+
options = options.merge(:find_with_score => true)
|
111
|
+
find needle, options
|
112
|
+
end
|
133
113
|
|
134
114
|
def find(needle, options = {})
|
135
115
|
options = default_options.merge options
|
@@ -137,9 +117,9 @@ class FuzzyMatch
|
|
137
117
|
threshold = options[:threshold]
|
138
118
|
gather_last_result = options[:gather_last_result]
|
139
119
|
is_find_all_with_score = options[:find_all_with_score]
|
120
|
+
is_find_with_score = options[:find_with_score]
|
140
121
|
is_find_best = options[:find_best]
|
141
122
|
is_find_all = options[:find_all] || is_find_all_with_score || is_find_best
|
142
|
-
first_grouping_decides = options[:first_grouping_decides]
|
143
123
|
must_match_grouping = options[:must_match_grouping]
|
144
124
|
must_match_at_least_one_word = options[:must_match_at_least_one_word]
|
145
125
|
|
@@ -148,37 +128,38 @@ class FuzzyMatch
|
|
148
128
|
last_result.read = read
|
149
129
|
last_result.haystack = haystack
|
150
130
|
last_result.options = options
|
151
|
-
last_result.timeline << <<-EOS
|
152
|
-
Options were set, either by you or by falling back to defaults.
|
153
|
-
\tOptions: #{options.inspect}
|
154
|
-
EOS
|
155
131
|
end
|
156
132
|
|
157
133
|
if gather_last_result
|
158
|
-
last_result.normalizers = normalizers
|
159
134
|
last_result.identities = identities
|
160
135
|
last_result.groupings = groupings
|
161
136
|
last_result.stop_words = stop_words
|
162
137
|
end
|
163
138
|
|
164
|
-
needle =
|
139
|
+
needle = Record.new needle
|
165
140
|
|
166
141
|
if gather_last_result
|
167
142
|
last_result.needle = needle
|
168
|
-
last_result.timeline << <<-EOS
|
169
|
-
The needle's #{needle.variants.length} variants were enumerated.
|
170
|
-
\tVariants: #{needle.variants.map(&:inspect).join(', ')}
|
171
|
-
EOS
|
172
143
|
end
|
173
|
-
|
174
|
-
if
|
144
|
+
|
145
|
+
if groupings.any?
|
146
|
+
first_grouping = groupings.detect { |grouping| grouping.xmatch? needle }
|
147
|
+
if gather_last_result
|
148
|
+
if first_grouping
|
149
|
+
last_result.timeline << "Grouping: #{first_grouping.inspect}"
|
150
|
+
else
|
151
|
+
last_result.timeline << "No grouping."
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
if must_match_grouping and not first_grouping
|
175
157
|
if gather_last_result
|
176
158
|
last_result.timeline << <<-EOS
|
177
|
-
The needle didn't match any of the #{groupings.length}
|
178
|
-
\
|
159
|
+
The needle didn't match any of the #{groupings.length} groupings, which was a requirement.
|
160
|
+
\t#{groupings.map(&:inspect).join("\n\t")}
|
179
161
|
EOS
|
180
162
|
end
|
181
|
-
|
182
163
|
if is_find_all
|
183
164
|
return []
|
184
165
|
else
|
@@ -186,38 +167,39 @@ EOS
|
|
186
167
|
end
|
187
168
|
end
|
188
169
|
|
170
|
+
if groupings.any? and not first_grouping
|
171
|
+
passed_grouping_requirement = haystack.reject do |straw|
|
172
|
+
groupings.any? { |grouping| grouping.xmatch? straw }
|
173
|
+
end
|
174
|
+
else
|
175
|
+
passed_grouping_requirement = haystack
|
176
|
+
end
|
177
|
+
|
189
178
|
if must_match_at_least_one_word
|
190
|
-
passed_word_requirement =
|
179
|
+
passed_word_requirement = passed_grouping_requirement.select do |straw|
|
191
180
|
(needle.words & straw.words).any?
|
192
181
|
end
|
193
182
|
if gather_last_result
|
194
183
|
last_result.timeline << <<-EOS
|
195
184
|
Since :must_match_at_least_one_word => true, the competition was reduced to records sharing at least one word with the needle.
|
196
185
|
\tNeedle words: #{needle.words.map(&:inspect).join(', ')}
|
197
|
-
\tPassed (first 3): #{passed_word_requirement[0,3].map(&:
|
198
|
-
\tFailed (first 3): #{(
|
186
|
+
\tPassed (first 3): #{passed_word_requirement[0,3].map(&:inspect).join(', ')}
|
187
|
+
\tFailed (first 3): #{(passed_grouping_requirement-passed_word_requirement)[0,3].map(&:inspect).join(', ')}
|
199
188
|
EOS
|
200
189
|
end
|
201
190
|
else
|
202
|
-
passed_word_requirement =
|
191
|
+
passed_word_requirement = passed_grouping_requirement
|
203
192
|
end
|
204
|
-
|
205
|
-
if
|
193
|
+
|
194
|
+
if first_grouping
|
206
195
|
joint = passed_word_requirement.select do |straw|
|
207
|
-
|
208
|
-
if first_grouping = groupings.detect { |grouping| grouping.match? needle }
|
209
|
-
first_grouping.join? needle, straw
|
210
|
-
end
|
211
|
-
else
|
212
|
-
groupings.any? { |grouping| grouping.join? needle, straw }
|
213
|
-
end
|
196
|
+
first_grouping.xjoin? needle, straw
|
214
197
|
end
|
198
|
+
# binding.pry
|
215
199
|
if gather_last_result
|
216
200
|
last_result.timeline << <<-EOS
|
217
|
-
Since there were groupings, the competition was reduced to records in the same group as the needle.
|
218
|
-
\
|
219
|
-
\tPassed (first 3): #{joint[0,3].map(&:render).map(&:inspect).join(', ')}
|
220
|
-
\tFailed (first 3): #{(passed_word_requirement-joint)[0,3].map(&:render).map(&:inspect).join(', ')}
|
201
|
+
Since there were groupings, the competition was reduced to #{joint.length} records in the same group as the needle.
|
202
|
+
\t#{joint.map(&:inspect).join("\n\t")}
|
221
203
|
EOS
|
222
204
|
end
|
223
205
|
else
|
@@ -252,8 +234,8 @@ EOS
|
|
252
234
|
last_result.timeline << <<-EOS
|
253
235
|
Since there were identities, the competition was reduced to records that might be identical to the needle (in other words, are not certainly different)
|
254
236
|
\tIdentities (first 10 of #{identities.length}): #{identities[0,9].map(&:inspect).join(', ')}
|
255
|
-
\tPassed (first 10 of #{possibly_identical.length}): #{possibly_identical[0,9].map(&:
|
256
|
-
\tFailed (first 10 of #{(joint-possibly_identical).length}): #{(joint-possibly_identical)[0,9].map(&:
|
237
|
+
\tPassed (first 10 of #{possibly_identical.length}): #{possibly_identical[0,9].map(&:inspect).join(', ')}
|
238
|
+
\tFailed (first 10 of #{(joint-possibly_identical).length}): #{(joint-possibly_identical)[0,9].map(&:inspect).join(', ')}
|
257
239
|
EOS
|
258
240
|
end
|
259
241
|
else
|
@@ -265,7 +247,7 @@ EOS
|
|
265
247
|
if gather_last_result
|
266
248
|
last_result.timeline << <<-EOS
|
267
249
|
The competition was sorted in order of similarity to the needle.
|
268
|
-
\
|
250
|
+
\t#{similarities[0,9].map { |s| "#{s.record2.similarity(needle).inspect}" }.join("\n\t")}
|
269
251
|
EOS
|
270
252
|
end
|
271
253
|
|
@@ -274,7 +256,7 @@ EOS
|
|
274
256
|
similarities.each do |similarity|
|
275
257
|
if similarity.satisfy?(needle, threshold)
|
276
258
|
bs = similarity.best_score
|
277
|
-
memo << [similarity.
|
259
|
+
memo << [similarity.record2.original, bs.dices_coefficient_similar, bs.levenshtein_similar]
|
278
260
|
end
|
279
261
|
end
|
280
262
|
return memo
|
@@ -288,7 +270,7 @@ EOS
|
|
288
270
|
bs = similarity.best_score
|
289
271
|
best_bs ||= bs
|
290
272
|
if bs >= best_bs
|
291
|
-
memo << similarity.
|
273
|
+
memo << similarity.record2.original
|
292
274
|
else
|
293
275
|
break
|
294
276
|
end
|
@@ -301,7 +283,7 @@ EOS
|
|
301
283
|
memo = []
|
302
284
|
similarities.each do |similarity|
|
303
285
|
if similarity.satisfy?(needle, threshold)
|
304
|
-
memo << similarity.
|
286
|
+
memo << similarity.record2.original
|
305
287
|
end
|
306
288
|
end
|
307
289
|
return memo
|
@@ -311,24 +293,30 @@ EOS
|
|
311
293
|
winner = nil
|
312
294
|
|
313
295
|
if best_similarity and best_similarity.satisfy?(needle, threshold)
|
314
|
-
winner = best_similarity.
|
296
|
+
winner = best_similarity.record2.original
|
315
297
|
if gather_last_result
|
316
298
|
last_result.winner = winner
|
317
299
|
last_result.score = best_similarity.best_score.dices_coefficient_similar
|
318
300
|
last_result.timeline << <<-EOS
|
319
|
-
A winner was determined because the Dice's Coefficient similarity (#{best_similarity.best_score.dices_coefficient_similar}) is greater than zero or because it shared a word with the needle.
|
301
|
+
A winner was determined because the Dice's Coefficient similarity (#{'%0.5f' % best_similarity.best_score.dices_coefficient_similar}) is greater than zero or because it shared a word with the needle.
|
320
302
|
EOS
|
321
303
|
end
|
304
|
+
if is_find_with_score
|
305
|
+
bs = best_similarity.best_score
|
306
|
+
return [winner, bs.dices_coefficient_similar, bs.levenshtein_similar]
|
307
|
+
else
|
308
|
+
return winner
|
309
|
+
end
|
322
310
|
elsif gather_last_result
|
323
|
-
best_similarity_record = if best_similarity and best_similarity.
|
324
|
-
best_similarity.
|
311
|
+
best_similarity_record = if best_similarity and best_similarity.record2
|
312
|
+
best_similarity.record2.original
|
325
313
|
end
|
326
314
|
last_result.timeline << <<-EOS
|
327
315
|
No winner assigned because the score of the best similarity (#{best_similarity_record.inspect}) was zero and it didn't match any words with the needle (#{needle.inspect}).
|
328
316
|
EOS
|
329
317
|
end
|
330
|
-
|
331
|
-
|
318
|
+
|
319
|
+
nil # ugly
|
332
320
|
end
|
333
321
|
|
334
322
|
# Explain is like mysql's EXPLAIN command. You give it a needle and it tells you about how it was located (successfully or not) in the haystack.
|
@@ -339,8 +327,4 @@ EOS
|
|
339
327
|
find needle, options.merge(:gather_last_result => true)
|
340
328
|
last_result.explain
|
341
329
|
end
|
342
|
-
|
343
|
-
# DEPRECATED
|
344
|
-
def free
|
345
|
-
end
|
346
330
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
require 'active_record'
|
4
4
|
require 'cohort_analysis'
|
@@ -102,31 +102,31 @@ end
|
|
102
102
|
describe FuzzyMatch::CachedResult do
|
103
103
|
it %{joins aircraft to flight segments} do
|
104
104
|
aircraft = Aircraft.find('B742')
|
105
|
-
aircraft.flight_segments.count.
|
105
|
+
aircraft.flight_segments.count.should == 2
|
106
106
|
end
|
107
107
|
|
108
108
|
it %{allow simple SQL operations} do
|
109
109
|
aircraft = Aircraft.find('B742')
|
110
|
-
aircraft.flight_segments.sum(:passengers).
|
110
|
+
aircraft.flight_segments.sum(:passengers).should == 110
|
111
111
|
end
|
112
112
|
|
113
113
|
it %{works with weighted_average} do
|
114
114
|
aircraft = Aircraft.find('B742')
|
115
|
-
aircraft.flight_segments.weighted_average(:seats, :weighted_by => :passengers).
|
115
|
+
aircraft.flight_segments.weighted_average(:seats, :weighted_by => :passengers).should == 5.45455
|
116
116
|
end
|
117
117
|
|
118
118
|
it %{works with cohort_scope (albeit rather clumsily)} do
|
119
119
|
aircraft = Aircraft.find('B742')
|
120
120
|
cohort = FlightSegment.cohort({:aircraft_description => aircraft.flight_segments_foreign_keys}, :minimum_size => 2)
|
121
|
-
FlightSegment.connection.select_value(cohort.project('COUNT(*)').to_sql).
|
122
|
-
# FlightSegment.cohort(:aircraft_description => aircraft.flight_segments_foreign_keys).
|
121
|
+
FlightSegment.connection.select_value(cohort.project('COUNT(*)').to_sql).should == 2
|
122
|
+
# FlightSegment.cohort(:aircraft_description => aircraft.flight_segments_foreign_keys).should == []
|
123
123
|
end
|
124
124
|
|
125
125
|
# def test_006_you_can_get_aircraft_from_flight_segments
|
126
126
|
# fs = FlightSegment.first
|
127
127
|
# # you need to add an aircraft_description column
|
128
128
|
# lambda do
|
129
|
-
# fs.aircraft.count.
|
129
|
+
# fs.aircraft.count.should == 2
|
130
130
|
# end.must_raise ActiveRecord::StatementInvalid
|
131
131
|
# end
|
132
132
|
end
|