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.
Files changed (55) hide show
  1. checksums.yaml +8 -8
  2. data/.rspec +2 -0
  3. data/CHANGELOG +14 -0
  4. data/Gemfile +8 -0
  5. data/README.markdown +58 -38
  6. data/Rakefile +0 -9
  7. data/bin/fuzzy_match +106 -0
  8. data/fuzzy_match.gemspec +4 -4
  9. data/groupings-screenshot.png +0 -0
  10. data/highlevel.graffle +0 -0
  11. data/highlevel.png +0 -0
  12. data/lib/fuzzy_match/record.rb +58 -0
  13. data/lib/fuzzy_match/result.rb +11 -8
  14. data/lib/fuzzy_match/rule/grouping.rb +70 -12
  15. data/lib/fuzzy_match/rule/identity.rb +3 -3
  16. data/lib/fuzzy_match/rule.rb +1 -1
  17. data/lib/fuzzy_match/score/amatch.rb +0 -4
  18. data/lib/fuzzy_match/score/pure_ruby.rb +2 -8
  19. data/lib/fuzzy_match/score.rb +4 -0
  20. data/lib/fuzzy_match/similarity.rb +10 -32
  21. data/lib/fuzzy_match/version.rb +1 -1
  22. data/lib/fuzzy_match.rb +78 -94
  23. data/{test/test_amatch.rb → spec/amatch_spec.rb} +1 -2
  24. data/{test/test_cache.rb → spec/cache_spec.rb} +7 -7
  25. data/spec/foo.rb +9 -0
  26. data/spec/fuzzy_match_spec.rb +354 -0
  27. data/spec/grouping_spec.rb +60 -0
  28. data/spec/identity_spec.rb +29 -0
  29. data/{test/test_wrapper.rb → spec/record_spec.rb} +3 -7
  30. data/spec/spec_helper.rb +21 -0
  31. metadata +56 -50
  32. data/bin/fuzzy_match_checker +0 -71
  33. data/examples/bts_aircraft/5-2-A.htm +0 -10305
  34. data/examples/bts_aircraft/5-2-B.htm +0 -9576
  35. data/examples/bts_aircraft/5-2-D.htm +0 -7094
  36. data/examples/bts_aircraft/5-2-E.htm +0 -2349
  37. data/examples/bts_aircraft/5-2-G.htm +0 -2922
  38. data/examples/bts_aircraft/groupings.csv +0 -1
  39. data/examples/bts_aircraft/identities.csv +0 -1
  40. data/examples/bts_aircraft/negatives.csv +0 -1
  41. data/examples/bts_aircraft/normalizers.csv +0 -1
  42. data/examples/bts_aircraft/number_260.csv +0 -334
  43. data/examples/bts_aircraft/positives.csv +0 -1
  44. data/examples/bts_aircraft/test_bts_aircraft.rb +0 -116
  45. data/examples/first_name_matching.rb +0 -15
  46. data/examples/icao-bts.xls +0 -0
  47. data/lib/fuzzy_match/rule/normalizer.rb +0 -20
  48. data/lib/fuzzy_match/rule/stop_word.rb +0 -11
  49. data/lib/fuzzy_match/wrapper.rb +0 -73
  50. data/test/helper.rb +0 -12
  51. data/test/test_fuzzy_match.rb +0 -304
  52. data/test/test_fuzzy_match_convoluted.rb.disabled +0 -268
  53. data/test/test_grouping.rb +0 -28
  54. data/test/test_identity.rb +0 -34
  55. data/test/test_normalizer.rb +0 -10
@@ -1,11 +1,11 @@
1
1
  class FuzzyMatch
2
2
  class Similarity
3
- attr_reader :wrapper1
4
- attr_reader :wrapper2
3
+ attr_reader :record1
4
+ attr_reader :record2
5
5
 
6
- def initialize(wrapper1, wrapper2)
7
- @wrapper1 = wrapper1
8
- @wrapper2 = wrapper2
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(best_wrapper1_variant, best_wrapper2_variant)
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
- ((wrapper2.render.length < 3 or needle.render.length < 3) and best_score.levenshtein_similar > 0) or
27
- (needle.words & wrapper2.words).any?
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
- %{#<FuzzyMatch::Similarity #{wrapper2.render.inspect}=>#{best_wrapper2_variant.inspect} versus #{wrapper1.render.inspect}=>#{best_wrapper1_variant.inspect} original_weight=#{"%0.5f" % original_weight} best_score=#{best_score.inspect}>}
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 / (wrapper1.render.length * wrapper2.render.length))
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
@@ -1,3 +1,3 @@
1
1
  class FuzzyMatch
2
- VERSION = '1.5.0'
2
+ VERSION = '2.0.0'
3
3
  end
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/wrapper'
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(competitors, options_and_rules = {})
73
- options_and_rules = options_and_rules.dup
68
+ def initialize(haystack, options_and_rules = {})
69
+ o = options_and_rules.dup
74
70
 
75
71
  # rules
76
- self.groupings = options_and_rules.delete(:groupings) || options_and_rules.delete(:blockings) || []
77
- self.identities = options_and_rules.delete(:identities) || []
78
- self.normalizers = options_and_rules.delete(:normalizers) || options_and_rules.delete(:tighteners) || []
79
- self.stop_words = options_and_rules.delete(:stop_words) || []
80
- @read = options_and_rules.delete(:read) || options_and_rules.delete(:haystack_reader)
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 = options_and_rules.delete(:first_blocking_decides)
84
- options_and_rules[:first_grouping_decides] = deprecated
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(options_and_rules).freeze
81
+ @default_options = DEFAULT_OPTIONS.merge(o).freeze
90
82
 
91
- # do this last
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 || raise(::RuntimeError, "[fuzzy_match] You can't access the last result until you've run a find with :gather_last_result => true")
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 = Wrapper.new self, needle, true
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 must_match_grouping and groupings.any? and groupings.none? { |grouping| grouping.match? needle }
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} grouping, which was a requirement.
178
- \tGroupings (first 3): #{groupings[0,3].map(&:inspect).join(', ')}
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 = haystack.select do |straw|
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(&:render).map(&:inspect).join(', ')}
198
- \tFailed (first 3): #{(haystack-passed_word_requirement)[0,3].map(&:render).map(&:inspect).join(', ')}
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 = haystack
191
+ passed_word_requirement = passed_grouping_requirement
203
192
  end
204
-
205
- if groupings.any?
193
+
194
+ if first_grouping
206
195
  joint = passed_word_requirement.select do |straw|
207
- if first_grouping_decides
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
- \tGroupings (first 3): #{groupings[0,3].map(&:inspect).join(', ')}
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(&:render).map(&:inspect).join(', ')}
256
- \tFailed (first 10 of #{(joint-possibly_identical).length}): #{(joint-possibly_identical)[0,9].map(&:render).map(&:inspect).join(', ')}
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
- \tSimilar (first 10 of #{similarities.length}): #{similarities[0,9].map { |s| "#{s.wrapper2.render.inspect} (#{[s.best_score.dices_coefficient_similar, s.best_score.levenshtein_similar].map { |v| '%0.5f' % v }.join('/')})" }.join(', ')}
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.wrapper2.record, bs.dices_coefficient_similar, bs.levenshtein_similar]
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.wrapper2.record
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.wrapper2.record
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.wrapper2.record
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.wrapper2
324
- best_similarity.wrapper2.record
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
- winner
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,6 +1,5 @@
1
1
  unless RUBY_PLATFORM == 'java'
2
- require 'helper'
3
- require 'test_fuzzy_match'
2
+ require 'spec_helper'
4
3
  require 'amatch'
5
4
 
6
5
  describe FuzzyMatch do
@@ -1,4 +1,4 @@
1
- require 'helper'
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.must_equal 2
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).must_equal 110
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).must_equal 5.45455
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).must_equal 2
122
- # FlightSegment.cohort(:aircraft_description => aircraft.flight_segments_foreign_keys).must_equal []
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.must_equal 2
129
+ # fs.aircraft.count.should == 2
130
130
  # end.must_raise ActiveRecord::StatementInvalid
131
131
  # end
132
132
  end
data/spec/foo.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'fileutils'
2
+ Dir['test*.rb'].each do |f|
3
+ n = File.basename(f, '.rb')
4
+ n.sub! 'test_', ''
5
+ n += '_spec.rb'
6
+ puts f
7
+ puts n
8
+ FileUtils.cp f, n
9
+ end