loose_tight_dictionary 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +4 -0
  3. data/README.rdoc +76 -23
  4. data/Rakefile +2 -38
  5. data/benchmark/before-with-free.txt +283 -0
  6. data/benchmark/before-without-last-result.txt +257 -0
  7. data/benchmark/before.txt +304 -0
  8. data/benchmark/memory.rb +54 -0
  9. data/examples/bts_aircraft/5-2-A.htm +10305 -0
  10. data/examples/bts_aircraft/5-2-B.htm +9576 -0
  11. data/examples/bts_aircraft/5-2-D.htm +7094 -0
  12. data/examples/bts_aircraft/5-2-E.htm +2349 -0
  13. data/examples/bts_aircraft/5-2-G.htm +2922 -0
  14. data/examples/bts_aircraft/blockings.csv +1 -0
  15. data/examples/bts_aircraft/identities.csv +1 -0
  16. data/examples/bts_aircraft/negatives.csv +1 -0
  17. data/examples/bts_aircraft/number_260.csv +334 -0
  18. data/examples/bts_aircraft/positives.csv +1 -0
  19. data/examples/bts_aircraft/test_bts_aircraft.rb +123 -0
  20. data/examples/bts_aircraft/tighteners.csv +1 -0
  21. data/examples/first_name_matching.rb +14 -22
  22. data/lib/loose_tight_dictionary/blocking.rb +36 -0
  23. data/lib/loose_tight_dictionary/extract_regexp.rb +30 -0
  24. data/lib/loose_tight_dictionary/identity.rb +25 -0
  25. data/lib/loose_tight_dictionary/result.rb +23 -0
  26. data/lib/loose_tight_dictionary/score.rb +28 -0
  27. data/lib/loose_tight_dictionary/similarity.rb +62 -0
  28. data/lib/loose_tight_dictionary/tightener.rb +30 -0
  29. data/lib/loose_tight_dictionary/version.rb +3 -0
  30. data/lib/loose_tight_dictionary/wrapper.rb +37 -0
  31. data/lib/loose_tight_dictionary.rb +178 -305
  32. data/loose_tight_dictionary.gemspec +19 -64
  33. data/test/helper.rb +6 -6
  34. data/test/test_blocking.rb +23 -0
  35. data/test/test_extract_regexp.rb +18 -0
  36. data/test/test_identity.rb +18 -0
  37. data/test/test_loose_tight_dictionary.rb +52 -245
  38. data/test/test_loose_tight_dictionary_convoluted.rb.disabled +268 -0
  39. data/test/test_tightening.rb +10 -0
  40. metadata +52 -65
  41. data/VERSION +0 -1
  42. data/examples/icao-bts.rb +0 -58
@@ -1,71 +1,26 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "loose_tight_dictionary/version"
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = %q{loose_tight_dictionary}
8
- s.version = "0.0.10"
6
+ s.name = "loose_tight_dictionary"
7
+ s.version = LooseTightDictionary::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Seamus Abshere"]
10
+ s.email = ["seamus@abshere.net"]
11
+ s.homepage = "https://github.com/seamusabshere/loose_tight_dictionary"
12
+ s.summary = %Q{Allows iterative development of dictionaries for big data sets.}
13
+ s.description = %Q{Create dictionaries that link rows between two tables using loose matching (string similarity) by default and tight matching (regexp) by request.}
9
14
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Seamus Abshere"]
12
- s.date = %q{2011-03-02}
13
- s.description = %q{Create dictionaries that link rows between two tables (left and right) using loose matching (string similarity) by default and tight matching (regexp) by request.}
14
- s.email = %q{seamus@abshere.net}
15
- s.extra_rdoc_files = [
16
- "LICENSE",
17
- "README.rdoc"
18
- ]
19
- s.files = [
20
- ".document",
21
- ".gitignore",
22
- "LICENSE",
23
- "README.rdoc",
24
- "Rakefile",
25
- "VERSION",
26
- "examples/first_name_matching.rb",
27
- "examples/icao-bts.rb",
28
- "examples/icao-bts.xls",
29
- "lib/loose_tight_dictionary.rb",
30
- "loose_tight_dictionary.gemspec",
31
- "test/helper.rb",
32
- "test/test_loose_tight_dictionary.rb"
33
- ]
34
- s.homepage = %q{http://github.com/seamusabshere/loose_tight_dictionary}
35
- s.rdoc_options = ["--charset=UTF-8"]
36
- s.require_paths = ["lib"]
37
- s.rubygems_version = %q{1.3.7}
38
- s.summary = %q{Allows iterative development of dictionaries for big data sets.}
39
- s.test_files = [
40
- "test/helper.rb",
41
- "test/test_loose_tight_dictionary.rb",
42
- "examples/first_name_matching.rb",
43
- "examples/icao-bts.rb"
44
- ]
15
+ s.rubyforge_project = "loose_tight_dictionary"
45
16
 
46
- if s.respond_to? :specification_version then
47
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
- s.specification_version = 3
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,floose_tight_dictionaryures}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
49
21
 
50
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
51
- s.add_development_dependency(%q<shoulda>, [">= 0"])
52
- s.add_development_dependency(%q<remote_table>, [">= 0.2.19"])
53
- s.add_runtime_dependency(%q<activesupport>, [">= 2.3.4"])
54
- s.add_runtime_dependency(%q<andand>, [">= 1.3.1"])
55
- s.add_runtime_dependency(%q<amatch>, [">= 0.2.5"])
56
- else
57
- s.add_dependency(%q<shoulda>, [">= 0"])
58
- s.add_dependency(%q<remote_table>, [">= 0.2.19"])
59
- s.add_dependency(%q<activesupport>, [">= 2.3.4"])
60
- s.add_dependency(%q<andand>, [">= 1.3.1"])
61
- s.add_dependency(%q<amatch>, [">= 0.2.5"])
62
- end
63
- else
64
- s.add_dependency(%q<shoulda>, [">= 0"])
65
- s.add_dependency(%q<remote_table>, [">= 0.2.19"])
66
- s.add_dependency(%q<activesupport>, [">= 2.3.4"])
67
- s.add_dependency(%q<andand>, [">= 1.3.1"])
68
- s.add_dependency(%q<amatch>, [">= 0.2.5"])
69
- end
22
+ s.add_development_dependency "shoulda"
23
+ s.add_development_dependency "remote_table"
24
+ s.add_dependency 'activesupport', '>=2.3.4'
25
+ s.add_dependency 'amatch'
70
26
  end
71
-
data/test/helper.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
2
4
  require 'test/unit'
3
- require 'shoulda'
4
- require 'logger'
5
- # require 'ruby-debug'
6
-
7
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ require 'stringio'
6
+ require 'remote_table'
8
7
  $LOAD_PATH.unshift(File.dirname(__FILE__))
9
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'loose_tight_dictionary'))
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+ require 'loose_tight_dictionary'
10
10
 
11
11
  class Test::Unit::TestCase
12
12
  end
@@ -0,0 +1,23 @@
1
+ require 'helper'
2
+
3
+ class TestBlocking < Test::Unit::TestCase
4
+ def test_001_encompass_one
5
+ b = LooseTightDictionary::Blocking.new %r{apple}
6
+ assert_equal true, b.encompass?('2 apples')
7
+ end
8
+
9
+ def test_002_encompass_both
10
+ b = LooseTightDictionary::Blocking.new %r{apple}
11
+ assert_equal true, b.encompass?('apple', '2 apples')
12
+ end
13
+
14
+ def test_002_doesnt_encompass_both
15
+ b = LooseTightDictionary::Blocking.new %r{apple}
16
+ assert_equal false, b.encompass?('orange', '2 apples')
17
+ end
18
+
19
+ def test_003_no_information
20
+ b = LooseTightDictionary::Blocking.new %r{apple}
21
+ assert_equal nil, b.encompass?('orange', 'orange')
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+
3
+ class TestExtractRegexp < Test::Unit::TestCase
4
+ def test_001_regexp
5
+ i = LooseTightDictionary::Identity.new %r{\A\\?/(.*)etc/mysql\$$}
6
+ assert_equal %r{\A\\?/(.*)etc/mysql\$$}, i.regexp
7
+ end
8
+
9
+ def test_002_regexp_from_string
10
+ i = LooseTightDictionary::Identity.new '%r{\A\\\?/(.*)etc/mysql\$$}'
11
+ assert_equal %r{\A\\?/(.*)etc/mysql\$$}, i.regexp
12
+ end
13
+
14
+ def test_003_regexp_from_string_using_slash_delim
15
+ i = LooseTightDictionary::Identity.new '/\A\\\?\/(.*)etc\/mysql\$$/'
16
+ assert_equal %r{\A\\?/(.*)etc/mysql\$$}, i.regexp
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+
3
+ class TestIdentity < Test::Unit::TestCase
4
+ def test_001_identical
5
+ i = LooseTightDictionary::Identity.new %r{(A)[ ]*(\d)}
6
+ assert_equal true, i.identical?('A1', 'A 1foobar')
7
+ end
8
+
9
+ def test_002_certainly_different
10
+ i = LooseTightDictionary::Identity.new %r{(A)[ ]*(\d)}
11
+ assert_equal false, i.identical?('A1', 'A 2foobar')
12
+ end
13
+
14
+ def test_003_no_information_ie_possible_identical
15
+ i = LooseTightDictionary::Identity.new %r{(A)[ ]*(\d)}
16
+ assert_equal nil, i.identical?('B1', 'A 2foobar')
17
+ end
18
+ end
@@ -1,271 +1,78 @@
1
1
  require 'helper'
2
2
 
3
- require 'remote_table'
4
-
5
- # $logger = Logger.new STDERR
6
- # $logger.level = Logger::DEBUG
7
- # $tee = STDOUT
8
-
9
3
  class TestLooseTightDictionary < Test::Unit::TestCase
10
- def setup
11
- clear_ltd
12
-
13
- # dh 8 400
14
- @a_left = ['DE HAVILLAND CANADA DHC8400 Dash 8']
15
- @a_right = ['DEHAVILLAND DEHAVILLAND DHC8-400 DASH-8']
16
- # dh 88
17
- @b_left = ['ABCDEFG DH88 HIJKLMNOP']
18
- # dh 89
19
- @c_right = ['ABCDEFG DH89 HIJKLMNOP']
20
- # dh 8 200
21
- @d_left = ['DE HAVILLAND CANADA DHC8200 Dash 8']
22
- @d_right = ['BOMBARDIER DEHAVILLAND DHC8-200Q DASH-8']
23
- @d_lookalike = ['ABCD DHC8200 Dash 8']
24
-
25
- @t_1 = [ '/(dh)c?-?(\d{0,2})-?(\d{0,4})(?:.*?)(dash|\z)/i', 'good tightening for de havilland' ]
26
-
27
- @r_1 = [ '/(dh)c?-?(\d{0,2})-?(\d{0,4})(?:.*?)(dash|\z)/i', 'good identity for de havilland' ]
28
-
29
- @left = [
30
- @a_left,
31
- @b_left,
32
- ['DE HAVILLAND DH89 Dragon Rapide'],
33
- ['DE HAVILLAND CANADA DHC8100 Dash 8 (E9, CT142, CC142)'],
34
- @d_left,
35
- ['DE HAVILLAND CANADA DHC8300 Dash 8'],
36
- ['DE HAVILLAND DH90 Dragonfly']
37
- ]
38
- @right = [
39
- @a_right,
40
- @c_right,
41
- @d_right,
42
- ['DEHAVILLAND DEHAVILLAND DHC8-100 DASH-8'],
43
- ['DEHAVILLAND DEHAVILLAND TWIN OTTER DHC-6']
44
- ]
45
- @tightenings = []
46
- @identities = []
47
- @blockings = []
48
- @positives = []
49
- @negatives = []
50
- end
51
-
52
- def clear_ltd
53
- @_ltd = nil
54
- end
55
-
56
- def ltd
57
- @_ltd ||= LooseTightDictionary.new @right,
58
- :tightenings => @tightenings,
59
- :identities => @identities,
60
- :blockings => @blockings,
61
- :positives => @positives,
62
- :negatives => @negatives,
63
- :blocking_only => @blocking_only,
64
- :logger => $logger,
65
- :tee => $tee
66
- end
67
-
68
- should "optionally only pay attention to things that match blockings" do
69
- assert_equal @a_right, ltd.left_to_right(@a_left)
70
-
71
- clear_ltd
72
- @blocking_only = true
73
- assert_equal nil, ltd.left_to_right(@a_left)
74
-
75
- clear_ltd
76
- @blocking_only = true
77
- @blockings.push ['/dash/i']
78
- assert_equal @a_right, ltd.left_to_right(@a_left)
79
- end
4
+ # in case i start doing something with the log
5
+ # def setup
6
+ # @log = StringIO.new
7
+ # end
8
+ #
9
+ # def teardown
10
+ # @log.close
11
+ # end
80
12
 
81
- # the example from the readme, considerably uglier here
82
- should "check a simple table" do
83
- @right = [ 'seamus', 'andy', 'ben' ]
84
- @positives = [ [ 'seamus', 'Mr. Seamus Abshere' ] ]
85
- left = [ 'Mr. Seamus Abshere', 'Sr. Andy Rossmeissl', 'Master BenT' ]
86
-
87
- assert_nothing_raised do
88
- ltd.check left
89
- end
13
+ def test_001_find
14
+ d = LooseTightDictionary.new %w{ NISSAN HONDA }
15
+ assert_equal 'NISSAN', d.find('MISSAM')
90
16
  end
91
17
 
92
- should "treat a String as a full record if passed through" do
93
- dash = 'DHC8-400'
94
- b747 = 'B747200/300'
95
- dc9 = 'DC-9-10'
96
- right_records = [ dash, b747, dc9 ]
97
- simple_ltd = LooseTightDictionary.new right_records, :logger => $logger, :tee => $tee
98
- assert_equal dash, simple_ltd.left_to_right('DeHavilland Dash-8 DHC-400')
99
- assert_equal b747, simple_ltd.left_to_right('Boeing 747-300')
100
- assert_equal dc9, simple_ltd.find('McDonnell Douglas MD81/DC-9')
18
+ def test_002_find_with_score
19
+ d = LooseTightDictionary.new %w{ NISSAN HONDA }
20
+ assert_equal ['NISSAN', 0.6], d.find_with_score('MISSAM')
101
21
  end
102
22
 
103
- should "call it a mismatch if you hit a blank positive" do
104
- @positives.push [@a_left[0], '']
105
- assert_raises(LooseTightDictionary::Mismatch) do
106
- ltd.left_to_right @a_left
107
- end
108
- end
109
-
110
- should "call it a false positive if you hit a blank negative" do
111
- @negatives.push [@a_left[0], '']
112
- assert_raises(LooseTightDictionary::FalsePositive) do
113
- ltd.left_to_right @a_left
114
- end
23
+ def test_003_last_result
24
+ d = LooseTightDictionary.new %w{ NISSAN HONDA }
25
+ d.find 'MISSAM'
26
+ assert_equal 0.6, d.last_result.score
27
+ assert_equal 'NISSAN', d.last_result.record
115
28
  end
116
29
 
117
- should "have a false match without blocking" do
118
- # @d_left will be our victim
119
- @right.push @d_lookalike
120
- @tightenings.push @t_1
121
-
122
- assert_equal @d_lookalike, ltd.left_to_right(@d_left)
30
+ def test_004_false_positive_without_tightener
31
+ d = LooseTightDictionary.new ['BOEING 737-100/200', 'BOEING 737-900']
32
+ assert_equal 'BOEING 737-900', d.find('BOEING 737100 number 900')
123
33
  end
124
34
 
125
- should "do blocking if the left matches a block" do
126
- # @d_left will be our victim
127
- @right.push @d_lookalike
128
- @tightenings.push @t_1
129
- @blockings.push ['/(bombardier|de ?havilland)/i']
130
-
131
- assert_equal @d_right, ltd.left_to_right(@d_left)
35
+ def test_005_correct_with_tightener
36
+ tighteners = [
37
+ %r{(7\d)(7|0)-?(\d{1,3})} # tighten 737-100/200 => 737100, which will cause it to win over 737-900
38
+ ]
39
+ d = LooseTightDictionary.new ['BOEING 737-100/200', 'BOEING 737-900'], :tighteners => tighteners
40
+ assert_equal 'BOEING 737-100/200', d.find('BOEING 737100 number 900')
132
41
  end
133
-
134
- should "treat blocks as exclusive" do
135
- @right = [ @d_left ]
136
- @tightenings.push @t_1
137
- @blockings.push ['/(bombardier|de ?havilland)/i']
138
42
 
139
- assert_equal nil, ltd.left_to_right(@d_lookalike)
140
- end
141
-
142
- should "only use identities if they stem from the same regexp" do
143
- @identities.push @r_1
144
- @identities.push [ '/(cessna)(?:.*?)(citation)/i' ]
145
- @identities.push [ '/(cessna)(?:.*?)(\d\d\d)/i' ]
146
- x_left = [ 'CESSNA D-333 CITATION V']
147
- x_right = [ 'CESSNA D-333' ]
148
- @right.push x_right
149
-
150
- assert_equal x_right, ltd.left_to_right(x_left)
43
+ def test_008_false_positive_without_identity
44
+ d = LooseTightDictionary.new %w{ foo bar }
45
+ assert_equal 'bar', d.find('baz')
151
46
  end
152
47
 
153
- should "use the best score from all of the tightenings" do
154
- x_left = ["BOEING 737100"]
155
- x_right = ["BOEING BOEING 737-100/200"]
156
- x_right_wrong = ["BOEING BOEING 737-900"]
157
- @right.push x_right
158
- @right.push x_right_wrong
159
- @tightenings.push ['/(7\d)(7|0)-?\d{1,3}\/(\d\d\d)/i']
160
- @tightenings.push ['/(7\d)(7|0)-?(\d{1,3}|[A-Z]{0,3})/i']
161
-
162
- assert_equal x_right, ltd.left_to_right(x_left)
48
+ def test_008_identify_false_positive
49
+ d = LooseTightDictionary.new %w{ foo bar }, :identities => [ /ba(.)/ ]
50
+ assert_equal 'foo', d.find('baz')
163
51
  end
164
52
 
165
- should "compare using prefixes if tightened key is shorter than correct match" do
166
- x_left = ["BOEING 720"]
167
- x_right = ["BOEING BOEING 720-000"]
168
- x_right_wrong = ["BOEING BOEING 717-200"]
169
- @right.push x_right
170
- @right.push x_right_wrong
171
- @tightenings.push @t_1
172
- @tightenings.push ['/(7\d)(7|0)-?\d{1,3}\/(\d\d\d)/i']
173
- @tightenings.push ['/(7\d)(7|0)-?(\d{1,3}|[A-Z]{0,3})/i']
53
+ def test_009_loose_blocking
54
+ # sanity check
55
+ d = LooseTightDictionary.new [ 'X' ]
56
+ assert_equal 'X', d.find('X')
57
+ assert_equal 'X', d.find('A')
58
+ # end sanity check
174
59
 
175
- assert_equal x_right, ltd.left_to_right(x_left)
60
+ d = LooseTightDictionary.new [ 'X' ], :blockings => [ /X/, /Y/ ]
61
+ assert_equal 'X', d.find('X')
62
+ assert_equal 'X', d.find('A')
176
63
  end
177
64
 
178
- should "use the shortest original input" do
179
- x_left = ['De Havilland DHC8-777 Dash-8 Superstar']
180
- x_right = ['DEHAVILLAND DEHAVILLAND DHC8-777 DASH-8 Superstar']
181
- x_right_long = ['DEHAVILLAND DEHAVILLAND DHC8-777 DASH-8 Superstar/Supernova']
182
-
183
- @right.push x_right_long
184
- @right.push x_right
185
- @tightenings.push @t_1
186
-
187
- assert_equal x_right, ltd.left_to_right(x_left)
65
+ def test_010_strict_blocking
66
+ d = LooseTightDictionary.new [ 'X' ], :blockings => [ /X/, /Y/ ], :strict_blocking => true
67
+ assert_equal 'X', d.find('X')
68
+ assert_equal nil, d.find('A')
188
69
  end
189
70
 
190
- should "perform lookups left to right" do
191
- assert_equal @a_right, ltd.left_to_right(@a_left)
192
- end
193
-
194
- should "succeed if there are no checks" do
195
- assert_nothing_raised do
196
- ltd.check @left
197
- end
198
- end
199
-
200
- should "succeed if the positive checks just work" do
201
- @positives.push [ @a_left[0], @a_right[0] ]
202
-
203
- assert_nothing_raised do
204
- ltd.check @left
205
- end
206
- end
207
-
208
- should "fail if positive checks don't work" do
209
- @positives.push [ @d_left[0], @d_right[0] ]
210
-
211
- assert_raises(LooseTightDictionary::Mismatch) do
212
- ltd.check @left
213
- end
214
- end
215
-
216
- should "succeed if proper tightening is applied" do
217
- @positives.push [ @d_left[0], @d_right[0] ]
218
- @tightenings.push @t_1
219
-
220
- assert_nothing_raised do
221
- ltd.check @left
222
- end
223
- end
224
-
225
- should "use a Google Docs spreadsheet as a source of tightenings" do
226
- @positives.push [ @d_left[0], @d_right[0] ]
227
- @tightenings = RemoteTable.new :url => 'http://spreadsheets.google.com/pub?key=tiS_6CCDDM_drNphpYwE_iw&single=true&gid=0&output=csv', :headers => false
228
-
229
- # sabshere 9/30/10 this shouldn't raise anything
230
- # but the tightenings have been changed... we should be using test-only tightenings, not production ones
231
- # assert_nothing_raised do
232
- assert_raises(LooseTightDictionary::Mismatch) do
233
- ltd.check @left
234
- end
235
- end
236
-
237
- should "fail if negative checks don't work" do
238
- @negatives.push [ @b_left[0], @c_right[0] ]
239
-
240
- assert_raises(LooseTightDictionary::FalsePositive) do
241
- ltd.check @left
242
- end
243
- end
244
-
245
- should "do inline checking" do
246
- @negatives.push [ @b_left[0], @c_right[0] ]
247
-
248
- assert_raises(LooseTightDictionary::FalsePositive) do
249
- ltd.left_to_right @b_left
250
- end
251
- end
252
-
253
- should "fail if negative checks don't work, even with tightening" do
254
- @negatives.push [ @b_left[0], @c_right[0] ]
255
- @tightenings.push @t_1
256
-
257
- assert_raises(LooseTightDictionary::FalsePositive) do
258
- ltd.check @left
259
- end
260
- end
261
-
262
- should "succeed if proper identity is applied" do
263
- @negatives.push [ @b_left[0], @c_right[0] ]
264
- @positives.push [ @d_left[0], @d_right[0] ]
265
- @identities.push @r_1
266
-
267
- assert_nothing_raised do
268
- ltd.check @left
71
+ def test_011_free
72
+ d = LooseTightDictionary.new %w{ NISSAN HONDA }
73
+ d.free
74
+ assert_raises(LooseTightDictionary::Freed) do
75
+ d.find('foobar')
269
76
  end
270
77
  end
271
78
  end