aipp 0.2.5 → 0.2.6

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 (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