aipp 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +8 -0
  5. data/README.md +40 -14
  6. data/exe/aip2aixm +1 -1
  7. data/exe/aip2ofmx +1 -1
  8. data/lib/aipp.rb +2 -0
  9. data/lib/aipp/aip.rb +17 -12
  10. data/lib/aipp/downloader.rb +1 -1
  11. data/lib/aipp/executable.rb +15 -12
  12. data/lib/aipp/parser.rb +58 -43
  13. data/lib/aipp/pdf.rb +1 -1
  14. data/lib/aipp/regions/LF/AD-1.3.rb +7 -6
  15. data/lib/aipp/regions/LF/AD-1.6.rb +7 -5
  16. data/lib/aipp/regions/LF/AD-2.rb +16 -9
  17. data/lib/aipp/regions/LF/AD-3.1.rb +6 -6
  18. data/lib/aipp/regions/LF/ENR-2.1.rb +81 -6
  19. data/lib/aipp/regions/LF/ENR-4.1.rb +3 -1
  20. data/lib/aipp/regions/LF/ENR-4.3.rb +2 -3
  21. data/lib/aipp/regions/LF/ENR-5.1.rb +15 -2
  22. data/lib/aipp/regions/LF/ENR-5.4.rb +90 -0
  23. data/lib/aipp/regions/LF/ENR-5.5.rb +12 -10
  24. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +2 -2
  25. data/lib/aipp/regions/LF/helpers/base.rb +17 -9
  26. data/lib/aipp/regions/LF/helpers/radio_AD.rb +21 -13
  27. data/lib/aipp/t_hash.rb +3 -3
  28. data/lib/aipp/version.rb +1 -1
  29. data/lib/core_ext/enumerable.rb +7 -7
  30. data/lib/core_ext/string.rb +9 -4
  31. metadata +156 -168
  32. metadata.gz.sig +1 -0
  33. data/.github/workflows/test.yml +0 -26
  34. data/.gitignore +0 -8
  35. data/.ruby-version +0 -1
  36. data/.yardopts +0 -3
  37. data/Guardfile +0 -7
  38. data/TODO.md +0 -6
  39. data/aipp.gemspec +0 -45
  40. data/gems.rb +0 -3
  41. data/rakefile.rb +0 -12
  42. data/spec/fixtures/border.geojson +0 -201
  43. data/spec/fixtures/document.pdf +0 -0
  44. data/spec/fixtures/document.pdf.json +0 -1
  45. data/spec/fixtures/new.html +0 -6
  46. data/spec/fixtures/new.pdf +0 -0
  47. data/spec/fixtures/new.txt +0 -1
  48. data/spec/fixtures/source.zip +0 -0
  49. data/spec/lib/aipp/airac_spec.rb +0 -98
  50. data/spec/lib/aipp/border_spec.rb +0 -135
  51. data/spec/lib/aipp/downloader_spec.rb +0 -81
  52. data/spec/lib/aipp/patcher_spec.rb +0 -46
  53. data/spec/lib/aipp/pdf_spec.rb +0 -124
  54. data/spec/lib/aipp/t_hash_spec.rb +0 -44
  55. data/spec/lib/aipp/version_spec.rb +0 -7
  56. data/spec/lib/core_ext/enumberable_spec.rb +0 -76
  57. data/spec/lib/core_ext/hash_spec.rb +0 -27
  58. data/spec/lib/core_ext/integer_spec.rb +0 -15
  59. data/spec/lib/core_ext/nil_class_spec.rb +0 -11
  60. data/spec/lib/core_ext/string_spec.rb +0 -112
  61. data/spec/sounds/failure.mp3 +0 -0
  62. data/spec/sounds/success.mp3 +0 -0
  63. data/spec/spec_helper.rb +0 -29
@@ -1,135 +0,0 @@
1
- require_relative '../../spec_helper'
2
-
3
- describe AIPP::Border::Position do
4
- subject do
5
- AIPP::Border::Position.new(
6
- geometries: [
7
- [AIXM.xy(long: 0, lat: 0), AIXM.xy(long: 1, lat: 1), AIXM.xy(long: 2, lat: 2)],
8
- [AIXM.xy(long: 10, lat: 10), AIXM.xy(long: 11, lat: 11), AIXM.xy(long: 12, lat: 12)]
9
- ],
10
- geometry_index: 0,
11
- coordinates_index: 0
12
- )
13
- end
14
-
15
- describe :xy do
16
- it "returns the coordinates" do
17
- _(subject.xy).must_equal AIXM.xy(long: 0, lat: 0)
18
- end
19
-
20
- it "returns nil if the geometry index is out of bounds" do
21
- _(subject.tap { |s| s.geometry_index = 2 }.xy).must_be_nil
22
- end
23
-
24
- it "returns nil if the coordinates index is out of bounds" do
25
- _(subject.tap { |s| s.coordinates_index = 3 }.xy).must_be_nil
26
- end
27
- end
28
- end
29
-
30
- describe AIPP::Border do
31
- let :fixtures_dir do
32
- Pathname(__FILE__).join('..', '..', '..', 'fixtures')
33
- end
34
-
35
- # The border.geojson fixture defines three geometries:
36
- # * index 0: closed geometry circumventing the airfield of Pujaut
37
- # * index 1: closed geometry circumventing the village of Pujaut
38
- # * index 2: unclosed I-shaped geometry following the TGV from the S to N bridges over the Rhône
39
- # * index 3: unclosed U-shaped geometry around Île de Bartelasse from N to S end of Pont Daladier
40
- subject do
41
- AIPP::Border.new(fixtures_dir.join('border.geojson'))
42
- end
43
-
44
- describe :initialize do
45
- it "fails for files unless the extension is .geojson" do
46
- _{ AIPP::Border.new("/path/to/another.txt") }.must_raise ArgumentError
47
- end
48
- end
49
-
50
- describe :name do
51
- it "returns the upcased file name" do
52
- _(subject.name).must_equal 'BORDER'
53
- end
54
- end
55
-
56
- describe :closed? do
57
- it "returns true for closed geometries" do
58
- _(subject.closed?(geometry_index: 0)).must_equal true
59
- _(subject.closed?(geometry_index: 1)).must_equal true
60
- end
61
-
62
- it "returns false for unclosed geometries" do
63
- _(subject.closed?(geometry_index: 2)).must_equal false
64
- _(subject.closed?(geometry_index: 3)).must_equal false
65
- end
66
- end
67
-
68
- describe :nearest do
69
- let :point do
70
- AIXM.xy(lat: 44.008187986625636, long: 4.759397506713866)
71
- end
72
-
73
- it "finds the nearest position on any geometry" do
74
- position = subject.nearest(xy: point)
75
- _(position.geometry_index).must_equal 1
76
- _(position.coordinates_index).must_equal 12
77
- _(position.xy).must_equal AIXM.xy(lat: 44.01065725159039, long: 4.760427474975586)
78
- end
79
-
80
- it "finds the nearest postition on a given geometry" do
81
- position = subject.nearest(xy: point, geometry_index: 0)
82
- _(position.geometry_index).must_equal 0
83
- _(position.coordinates_index).must_equal 2
84
- _(position.xy).must_equal AIXM.xy(lat: 44.00269350325321, long: 4.7519731521606445)
85
- end
86
- end
87
-
88
- describe :segment do
89
- it "fails if positions are not on the same geometry" do
90
- from_position = AIPP::Border::Position.new(geometries: subject.geometries, geometry_index: 0, coordinates_index: 0)
91
- to_position = AIPP::Border::Position.new(geometries: subject.geometries, geometry_index: 1, coordinates_index: 0)
92
- _{ subject.segment(from_position: from_position, to_position: to_position) }.must_raise ArgumentError
93
- end
94
-
95
- it "returns shortest segment on an unclosed I-shaped geometry" do
96
- from_position = subject.nearest(xy: AIXM.xy(lat: 44.002940457248556, long: 4.734249114990234))
97
- to_position = subject.nearest(xy: AIXM.xy(lat: 44.07155380033749, long: 4.7687530517578125), geometry_index: from_position.geometry_index)
98
- _(subject.segment(from_position: from_position, to_position: to_position)).must_equal [
99
- AIXM.xy(lat: 44.00516299694704, long: 4.7371673583984375),
100
- AIXM.xy(lat: 44.02195282780904, long: 4.743347167968749),
101
- AIXM.xy(lat: 44.037503870182896, long: 4.749870300292969),
102
- AIXM.xy(lat: 44.05379106204314, long: 4.755706787109375),
103
- AIXM.xy(lat: 44.070073775703484, long: 4.7646331787109375)
104
- ]
105
- end
106
-
107
- it "returns shortest segment on an unclosed U-shaped geometry" do
108
- from_position = subject.nearest(xy: AIXM.xy(lat: 43.96563876212758, long: 4.8126983642578125))
109
- to_position = subject.nearest(xy: AIXM.xy(lat: 43.956989327857265, long: 4.83123779296875), geometry_index: from_position.geometry_index)
110
- _(subject.segment(from_position: from_position, to_position: to_position)).must_equal [
111
- AIXM.xy(lat: 43.9646503190861, long: 4.815788269042969),
112
- AIXM.xy(lat: 43.98614524381678, long: 4.82025146484375),
113
- AIXM.xy(lat: 43.98491011404692, long: 4.840850830078125),
114
- AIXM.xy(lat: 43.99479043262446, long: 4.845314025878906),
115
- AIXM.xy(lat: 43.98367495857784, long: 4.8538970947265625),
116
- AIXM.xy(lat: 43.967121395851485, long: 4.851493835449218),
117
- AIXM.xy(lat: 43.96069638244953, long: 4.8442840576171875),
118
- AIXM.xy(lat: 43.96069638244953, long: 4.829521179199219)
119
- ]
120
- end
121
-
122
- it "returns shortest segment ignoring endings on a closed geometry" do
123
- from_position = subject.nearest(xy: AIXM.xy(lat: 44.00022390676026, long: 4.789009094238281))
124
- to_position = subject.nearest(xy: AIXM.xy(lat: 43.99800118202362, long: 4.765834808349609), geometry_index: from_position.geometry_index)
125
- _(subject.segment(from_position: from_position, to_position: to_position)).must_equal [
126
- AIXM.xy(lat: 44.00077957493397, long: 4.787635803222656),
127
- AIXM.xy(lat: 43.99818641226534, long: 4.784030914306641),
128
- AIXM.xy(lat: 43.994111213373934, long: 4.78205680847168),
129
- AIXM.xy(lat: 44.00115001749186, long: 4.777421951293944),
130
- AIXM.xy(lat: 44.002940457248556, long: 4.770212173461914)
131
- ]
132
- end
133
-
134
- end
135
- end
@@ -1,81 +0,0 @@
1
- require_relative '../../spec_helper'
2
-
3
- describe AIPP::Downloader do
4
- let :fixtures_dir do
5
- Pathname(__FILE__).join('..', '..', '..', 'fixtures')
6
- end
7
-
8
- let :tmp_dir do
9
- Pathname(Dir.mktmpdir).tap do |tmp_dir|
10
- (sources_dir = tmp_dir.join('sources')).mkpath
11
- FileUtils.cp(fixtures_dir.join('source.zip'), sources_dir)
12
- end
13
- end
14
-
15
- after do
16
- FileUtils.rm_rf(tmp_dir)
17
- end
18
-
19
- describe :read do
20
- context "source archive does not exist" do
21
- it "creates the source archive" do
22
- Spy.on(Kernel, open: File.open(fixtures_dir.join('new.html')))
23
- subject = AIPP::Downloader.new(storage: tmp_dir, source: 'new-source') do |downloader|
24
- _(File.exist?(tmp_dir.join('work'))).must_equal true
25
- downloader.read(document: 'new', url: 'http://localhost/new.html')
26
- end
27
- _(zip_entries(subject.source_file)).must_equal %w(new.html)
28
- _(subject.send(:sources_path).children.count).must_equal 2
29
- end
30
- end
31
-
32
- context "source archive does exist" do
33
- it "unzips and uses the source archive" do
34
- Spy.on(Kernel, open: File.open(fixtures_dir.join('new.html')))
35
- subject = AIPP::Downloader.new(storage: tmp_dir, source: 'source') do |downloader|
36
- _(File.exist?(tmp_dir.join('work'))).must_equal true
37
- downloader.read(document: 'new', url: 'http://localhost/new.html').tap do |content|
38
- _(content).must_be_instance_of Nokogiri::HTML5::Document
39
- _(content.text).must_match /fixture-html-new/
40
- end
41
- end
42
- _(zip_entries(subject.source_file)).must_equal %w(new.html one.html two.html)
43
- _(subject.send(:sources_path).children.count).must_equal 1
44
- end
45
-
46
- it "downloads HTML documents to Nokogiri::HTML5::Document" do
47
- Spy.on(Kernel, open: File.open(fixtures_dir.join('new.html')))
48
- AIPP::Downloader.new(storage: tmp_dir, source: 'source') do |downloader|
49
- downloader.read(document: 'new', url: 'http://localhost/new.html').tap do |content|
50
- _(content).must_be_instance_of Nokogiri::HTML5::Document
51
- _(content.text).must_match /fixture-html-new/
52
- end
53
- end
54
- end
55
-
56
- it "downloads and caches PDF documents to AIPP::PDF" do
57
- Spy.on(Kernel, open: File.open(fixtures_dir.join('new.pdf')))
58
- AIPP::Downloader.new(storage: tmp_dir, source: 'source') do |downloader|
59
- downloader.read(document: 'new', url: 'http://localhost/new.pdf').tap do |content|
60
- _(content).must_be_instance_of AIPP::PDF
61
- _(content.text).must_match /fixture-pdf-new/
62
- end
63
- end
64
- end
65
-
66
- it "downloads explicitly specified type" do
67
- Spy.on(Kernel, open: File.open(fixtures_dir.join('new.pdf')))
68
- AIPP::Downloader.new(storage: tmp_dir, source: 'source') do |downloader|
69
- downloader.read(document: 'new', url: 'http://localhost/new', type: :pdf).tap do |content|
70
- _(content).must_be_instance_of AIPP::PDF
71
- _(content.text).must_match /fixture-pdf-new/
72
- end
73
- end
74
- end
75
- end
76
- end
77
-
78
- def zip_entries(zip_file)
79
- Zip::File.open(zip_file).entries.map(&:name).sort
80
- end
81
- end
@@ -1,46 +0,0 @@
1
- require_relative '../../spec_helper'
2
-
3
- class Shoe
4
- include AIPP::Patcher
5
-
6
- attr_accessor :size
7
-
8
- patch Shoe, :size do |parser, object, value|
9
- case value
10
- when 'S' then 36
11
- when 'one-size-fits-all' then nil
12
- else throw(:abort)
13
- end
14
- end
15
- end
16
-
17
- describe AIPP::Patcher do
18
- subject do
19
- Shoe.new.attach_patches
20
- end
21
-
22
- context "with patches attached" do
23
- after do
24
- subject.detach_patches
25
- end
26
-
27
- it "overwrites with non-nil values" do
28
- _(subject.tap { |s| s.size = 'S' }.size).must_equal 36
29
- end
30
-
31
- it "overwrite with nil values" do
32
- _(subject.tap { |s| s.size = 'one-size-fits-all' }.size).must_be_nil
33
- end
34
-
35
- it "skips overwrite if abort is thrown" do
36
- _(subject.tap { |s| s.size = 42 }.size).must_equal 42
37
- end
38
- end
39
-
40
- context "with patches detached" do
41
- it "removes patches" do
42
- subject.detach_patches
43
- _(subject.tap { |s| s.size = 'S' }.size).must_equal 'S'
44
- end
45
- end
46
- end
@@ -1,124 +0,0 @@
1
- require_relative '../../spec_helper'
2
-
3
- describe AIPP::PDF do
4
- let :fixtures_dir do
5
- Pathname(__FILE__).join('..', '..', '..', 'fixtures')
6
- end
7
-
8
- subject do
9
- AIPP::PDF.new(fixtures_dir.join('document.pdf'))
10
- end
11
-
12
- describe :@page_ranges do
13
- it "returns an array of page end positions" do
14
- _(subject.instance_variable_get(:@page_ranges)).must_equal [74, 149, 225]
15
- end
16
- end
17
-
18
- describe :page_for do
19
- it "finds the page for any given position" do
20
- _(subject.send(:page_for, index: 0)).must_equal 1
21
- _(subject.send(:page_for, index: 50)).must_equal 1
22
- _(subject.send(:page_for, index: 74)).must_equal 1
23
- _(subject.send(:page_for, index: 75)).must_equal 2
24
- _(subject.send(:page_for, index: 149)).must_equal 2
25
- _(subject.send(:page_for, index: 150)).must_equal 3
26
- _(subject.send(:page_for, index: 223)).must_equal 3
27
- end
28
- end
29
-
30
- describe :from do
31
- it "fences beginning to any position" do
32
- _(subject.from(100).range).must_equal (100..223)
33
- end
34
-
35
- it "fences beginning to first existing position" do
36
- _(subject.from(:begin).range).must_equal (0..223)
37
- end
38
- end
39
-
40
- describe :to do
41
- it "fences beginning to any position" do
42
- _(subject.to(100).range).must_equal (0..100)
43
- end
44
-
45
- it "fences beginning to first existing position" do
46
- _(subject.to(:end).range).must_equal (0..223)
47
- end
48
- end
49
-
50
- context "without boundaries" do
51
- describe :text do
52
- it "returns the entire text" do
53
- _(subject.text).must_match /\Apage 1, line 1/
54
- _(subject.text).must_match /page 3, line 5\z/
55
- end
56
- end
57
-
58
- describe :each_line do
59
- it "maps lines to positions" do
60
- target = [
61
- ["page 1, line 1\n", 1, false],
62
- ["page 1, line 2\n", 1, false],
63
- ["page 1, line 3\n", 1, false],
64
- ["page 1, line 4\n", 1, false],
65
- ["page 1, line 5\f", 1, false],
66
- ["page 2, line 1\n", 2, false],
67
- ["page 2, line 2\n", 2, false],
68
- ["page 2, line 3\n", 2, false],
69
- ["page 2, line 4\n", 2, false],
70
- ["page 2, line 5\f", 2, false],
71
- ["page 3, line 1\n", 3, false],
72
- ["page 3, line 2\n", 3, false],
73
- ["page 3, line 3\n", 3, false],
74
- ["page 3, line 4\n", 3, false],
75
- ["page 3, line 5", 3, true]
76
- ]
77
- subject.each_line do |line, page, last|
78
- target_line, target_page, target_last = target.shift
79
- _(line).must_equal target_line
80
- _(page).must_equal target_page
81
- _(last).must_equal target_last
82
- end
83
- end
84
-
85
- it "returns an enumerator if no block is given" do
86
- _(subject.each_line).must_be_instance_of Enumerator
87
- end
88
- end
89
- end
90
-
91
- context "with boundaries" do
92
- before do
93
- subject.from(100).to(200)
94
- end
95
-
96
- describe :text do
97
- it "returns the entire text" do
98
- _(subject.text).must_match /\Ane 2/
99
- _(subject.text).must_match /page 3\z/
100
- end
101
- end
102
-
103
- describe :each_line do
104
- it "maps lines to positions" do
105
- target = [
106
- ["ne 2\n", 2, false],
107
- ["page 2, line 3\n", 2, false],
108
- ["page 2, line 4\n", 2, false],
109
- ["page 2, line 5\f", 2, false],
110
- ["page 3, line 1\n", 3, false],
111
- ["page 3, line 2\n", 3, false],
112
- ["page 3, line 3\n", 3, false],
113
- ["page 3", 3, true]
114
- ]
115
- subject.each_line do |line, page, last|
116
- target_line, target_page, target_last = target.shift
117
- _(line).must_equal target_line
118
- _(page).must_equal target_page
119
- _(last).must_equal target_last
120
- end
121
- end
122
- end
123
- end
124
- end
@@ -1,44 +0,0 @@
1
- require_relative '../../spec_helper'
2
-
3
- describe AIPP::THash do
4
- context "non-circular dependencies" do
5
- subject do
6
- AIPP::THash[
7
- dns: %i(net),
8
- webserver: %i(dns logger),
9
- net: [],
10
- logger: []
11
- ]
12
- end
13
-
14
- describe :tsort do
15
- it "must compile the overall dependency list" do
16
- _(subject.tsort).must_equal %i(net dns logger webserver)
17
- end
18
-
19
- it "must compile partial dependency lists" do
20
- _(subject.tsort(:dns)).must_equal %i(net dns)
21
- _(subject.tsort(:logger)).must_equal %i(logger)
22
- _(subject.tsort(:webserver)).must_equal %i(net dns logger webserver)
23
- end
24
- end
25
- end
26
-
27
- context "circular dependencies" do
28
- subject do
29
- AIPP::THash[
30
- dns: %i(net),
31
- webserver: %i(dns logger),
32
- net: %i(dns),
33
- logger: []
34
- ]
35
- end
36
-
37
- describe :tsort do
38
- it "must raise cyclic dependency error" do
39
- _{ subject.tsort }.must_raise TSort::Cyclic
40
- _{ subject.tsort(:dns) }.must_raise TSort::Cyclic
41
- end
42
- end
43
- end
44
- end
@@ -1,7 +0,0 @@
1
- require_relative '../../spec_helper'
2
-
3
- describe AIPP do
4
- it "must be defined" do
5
- _(AIPP::VERSION).wont_be_nil
6
- end
7
- end
@@ -1,76 +0,0 @@
1
- require_relative '../../spec_helper'
2
-
3
- describe Enumerable do
4
-
5
- describe :split do
6
- context "by object" do
7
- it "must split at matching element" do
8
- _([1, 2, 0, 3, 4].split(0)).must_equal [[1, 2], [3, 4]]
9
- end
10
-
11
- it "won't split when no element matches" do
12
- _([1, 2, 3].split(0)).must_equal [[1, 2, 3]]
13
- end
14
-
15
- it "won't split zero length enumerable" do
16
- _([].split(0)).must_equal []
17
- end
18
-
19
- it "must keep leading empty subarrays" do
20
- _([0, 1, 2, 0, 3, 4].split(0)).must_equal [[], [1, 2], [3, 4]]
21
- end
22
-
23
- it "must keep empty subarrays in the middle" do
24
- _([1, 2, 0, 0, 3, 4].split(0)).must_equal [[1, 2], [], [3, 4]]
25
- end
26
-
27
- it "must drop trailing empty subarrays" do
28
- _([1, 2, 0, 3, 4, 0].split(0)).must_equal [[1, 2], [3, 4]]
29
- end
30
- end
31
-
32
- context "by block" do
33
- it "must split at matching element" do
34
- _([1, 2, 0, 3, 4].split { |e| e.zero? }).must_equal [[1, 2], [3, 4]]
35
- end
36
-
37
- it "won't split when no element matches" do
38
- _([1, 2, 3].split { |e| e.zero? }).must_equal [[1, 2, 3]]
39
- end
40
-
41
- it "won't split zero length enumerable" do
42
- _([].split { |e| e.zero? }).must_equal []
43
- end
44
-
45
- it "must keep leading empty subarrays" do
46
- _([0, 1, 2, 0, 3, 4].split { |e| e.zero? }).must_equal [[], [1, 2], [3, 4]]
47
- end
48
-
49
- it "must keep empty subarrays in the middle" do
50
- _([1, 2, 0, 0, 3, 4].split { |e| e.zero? }).must_equal [[1, 2], [], [3, 4]]
51
- end
52
-
53
- it "must drop trailing empty subarrays" do
54
- _([1, 2, 0, 3, 4, 0].split { |e| e.zero? }).must_equal [[1, 2], [3, 4]]
55
- end
56
- end
57
- end
58
-
59
- describe :group_by_chunks do
60
- it "fails to group if the first element does not meet the chunk condition" do
61
- subject = [10, 11, 12, 2, 20, 21 ]
62
- _{ subject.group_by_chunks { |i| i < 10 } }.must_raise ArgumentError
63
- end
64
-
65
- it "must map matching elements to array of subsequent non-matching elements" do
66
- subject = [1, 10, 11, 12, 2, 20, 21, 3, 30, 31, 32]
67
- _(subject.group_by_chunks { |i| i < 10 }).must_equal(1 => [10, 11, 12], 2 => [20, 21], 3 => [30, 31, 32])
68
- end
69
-
70
- it "must map matching elements to empty array if no subsequent non-matching elements exist" do
71
- subject = [1, 10, 11, 12, 2, 3, 30]
72
- _(subject.group_by_chunks { |i| i < 10 }).must_equal(1 => [10, 11, 12], 2 => [], 3 => [30])
73
- end
74
- end
75
-
76
- end