aipp 0.2.4 → 1.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 (86) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +222 -88
  5. data/exe/aip2aixm +2 -2
  6. data/exe/aip2ofmx +2 -2
  7. data/lib/aipp/aip.rb +113 -31
  8. data/lib/aipp/border.rb +77 -46
  9. data/lib/aipp/debugger.rb +101 -0
  10. data/lib/aipp/downloader.rb +39 -26
  11. data/lib/aipp/executable.rb +41 -22
  12. data/lib/aipp/parser.rb +94 -21
  13. data/lib/aipp/patcher.rb +5 -2
  14. data/lib/aipp/pdf.rb +1 -1
  15. data/lib/aipp/regions/LF/README.md +49 -0
  16. data/lib/aipp/regions/LF/aerodromes.rb +223 -0
  17. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
  18. data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
  19. data/lib/aipp/regions/LF/designated_points.rb +47 -0
  20. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
  21. data/lib/aipp/regions/LF/helipads.rb +122 -0
  22. data/lib/aipp/regions/LF/helpers/base.rb +218 -0
  23. data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
  24. data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
  25. data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
  26. data/lib/aipp/regions/LF/obstacles.rb +153 -0
  27. data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
  28. data/lib/aipp/regions/LF/services.rb +172 -0
  29. data/lib/aipp/t_hash.rb +4 -5
  30. data/lib/aipp/version.rb +1 -1
  31. data/lib/aipp.rb +11 -5
  32. data/lib/core_ext/enumerable.rb +9 -9
  33. data/lib/core_ext/hash.rb +21 -5
  34. data/lib/core_ext/nokogiri.rb +54 -0
  35. data/lib/core_ext/string.rb +38 -66
  36. data.tar.gz.sig +2 -0
  37. metadata +180 -188
  38. metadata.gz.sig +0 -0
  39. data/.gitignore +0 -8
  40. data/.ruby-version +0 -1
  41. data/.travis.yml +0 -8
  42. data/.yardopts +0 -3
  43. data/Guardfile +0 -7
  44. data/TODO.md +0 -6
  45. data/aipp.gemspec +0 -44
  46. data/gems.rb +0 -3
  47. data/lib/aipp/airac.rb +0 -55
  48. data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
  49. data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
  50. data/lib/aipp/regions/LF/AD-2.rb +0 -313
  51. data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
  52. data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
  53. data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
  54. data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
  55. data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
  56. data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
  57. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
  58. data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
  59. data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
  60. data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
  61. data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
  62. data/lib/aipp/regions/LF/helpers/common.rb +0 -217
  63. data/lib/core_ext/object.rb +0 -43
  64. data/rakefile.rb +0 -12
  65. data/spec/fixtures/archive.zip +0 -0
  66. data/spec/fixtures/border.geojson +0 -201
  67. data/spec/fixtures/document.pdf +0 -0
  68. data/spec/fixtures/document.pdf.json +0 -1
  69. data/spec/fixtures/new.html +0 -6
  70. data/spec/fixtures/new.pdf +0 -0
  71. data/spec/fixtures/new.txt +0 -1
  72. data/spec/lib/aipp/airac_spec.rb +0 -98
  73. data/spec/lib/aipp/border_spec.rb +0 -135
  74. data/spec/lib/aipp/downloader_spec.rb +0 -81
  75. data/spec/lib/aipp/patcher_spec.rb +0 -46
  76. data/spec/lib/aipp/pdf_spec.rb +0 -124
  77. data/spec/lib/aipp/t_hash_spec.rb +0 -44
  78. data/spec/lib/aipp/version_spec.rb +0 -7
  79. data/spec/lib/core_ext/enumberable_spec.rb +0 -76
  80. data/spec/lib/core_ext/hash_spec.rb +0 -27
  81. data/spec/lib/core_ext/integer_spec.rb +0 -15
  82. data/spec/lib/core_ext/nil_class_spec.rb +0 -11
  83. data/spec/lib/core_ext/string_spec.rb +0 -112
  84. data/spec/sounds/failure.mp3 +0 -0
  85. data/spec/sounds/success.mp3 +0 -0
  86. data/spec/spec_helper.rb +0 -28
@@ -0,0 +1,70 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ class ServicedAirspaces < AIP
5
+
6
+ include AIPP::LF::Helpers::Base
7
+
8
+ # Map source types to type and optional local type and skip regexp
9
+ SOURCE_TYPES = {
10
+ 'FIR' => { type: 'FIR' },
11
+ 'UIR' => { type: 'UIR' },
12
+ 'UTA' => { type: 'UTA' },
13
+ 'CTA' => { type: 'CTA' },
14
+ 'LTA' => { type: 'CTA', local_type: 'LTA' },
15
+ 'TMA' => { type: 'TMA', skip: /geneve/i }, # Geneva listed FYI only
16
+ 'SIV' => { type: 'SECTOR', local_type: 'FIZ/SIV' }, # providing FIS
17
+ 'CTR' => { type: 'CTR' },
18
+ 'RMZ' => { type: 'RAS', local_type: 'RMZ' },
19
+ 'TMZ' => { type: 'RAS', local_type: 'TMZ' },
20
+ 'RMZ-TMZ' => { type: 'RAS', local_type: 'RMZ-TMZ' }
21
+ }.freeze
22
+
23
+ # Map airspace "<type> <name>" to location indicator
24
+ FIR_LOCATION_INDICATORS = {
25
+ 'BORDEAUX' => 'LFBB',
26
+ 'BREST' => 'LFRR',
27
+ 'MARSEILLE' => 'LFMM',
28
+ 'PARIS' => 'LFFF',
29
+ 'REIMS' => 'LFRR'
30
+ }.freeze
31
+
32
+ def parse
33
+ SOURCE_TYPES.each do |source_type, target|
34
+ verbose_info("processing #{source_type}")
35
+ cache.espace.css(%Q(Espace[lk^="[LF][#{source_type} "])).each do |espace_node|
36
+ # Skip all delegated airspaces
37
+ next if espace_node.(:Nom).match? /deleg/i
38
+ next if (re = target[:skip]) && espace_node.(:Nom).match?(re)
39
+ # Build airspaces and layers
40
+ cache.partie.css(%Q(Partie:has(Espace[pk="#{espace_node['pk']}"]))).each do |partie_node|
41
+ add(
42
+ AIXM.airspace(
43
+ source: source(section: 'ENR', position: espace_node.line),
44
+ name: [
45
+ espace_node.(:Nom),
46
+ partie_node.(:NomPartie).remove(/^\.$/).blank_to_nil
47
+ ].compact.join(' '),
48
+ type: target[:type],
49
+ local_type: target[:local_type]
50
+ ).tap do |airspace|
51
+ airspace.meta = espace_node.attr('pk')
52
+ airspace.geometry = geometry_from(partie_node.(:Contour))
53
+ fail("geometry is not closed") unless airspace.geometry.closed?
54
+ cache.volume.css(%Q(Volume:has(Partie[pk="#{partie_node['pk']}"]))).each do |volume_node|
55
+ airspace.add_layer(
56
+ layer_from(volume_node).tap do |layer|
57
+ layer.location_indicator = FIR_LOCATION_INDICATORS.fetch(airspace.name) if airspace.type == :flight_information_region
58
+ end
59
+ )
60
+ end
61
+ end
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,172 @@
1
+ module AIPP
2
+ module LF
3
+
4
+ class Services < AIP
5
+
6
+ include AIPP::LF::Helpers::Base
7
+
8
+ DEPENDS = %w(aerodromes serviced_airspaces)
9
+
10
+ # Service types and how to treat them
11
+ SOURCE_TYPES = {
12
+ 'A/A' => :address,
13
+ 'AFIS' => :service,
14
+ 'APP' => :service,
15
+ 'ATIS' => :service,
16
+ 'CCM' => :ignore, # centre de contrôle militaire
17
+ 'CEV' => :ignore, # centre d’essai en vol
18
+ 'D-ATIS' => :ignore, # data link ATIS
19
+ 'FIS' => :service,
20
+ 'PAR' => :service,
21
+ 'SRE' => :ignore, # surveillance radar element of PAR
22
+ 'TWR' => :service,
23
+ 'UAC' => :ignore, # upper area control
24
+ 'VDF' => :service,
25
+ 'OTHER' => :service # no <Service> specified at source
26
+ }.freeze
27
+
28
+ # Map French callsigns to English and service type
29
+ CALLSIGNS = {
30
+ 'Approche' => { en: 'Approach', service_type: 'APP' },
31
+ 'Contrôle' => { en: 'Control', service_type: 'ACS' },
32
+ 'Information' => { en: 'Information', service_type: 'FIS' },
33
+ 'GCA' => { en: 'GCA', service_type: 'GCA' },
34
+ 'Gonio' => { en: 'Gonio', service_type: 'VDF' },
35
+ 'Prévol' => { en: 'Delivery', service_type: 'SMC' },
36
+ 'Sol' => { en: 'Ground', service_type: 'SMC' },
37
+ 'Tour' => { en: 'Tower', service_type: 'TWR' },
38
+ 'Trafic' => { en: 'Apron', service_type: 'SMC' }
39
+ }.freeze
40
+
41
+ def parse
42
+ cache.service.css(%Q(Service[lk^="[LF]"][pk])).each do |service_node|
43
+ # Ignore private services/frequencies
44
+ next if service_node.(:IndicLieu) == 'XX'
45
+ # Load referenced airport
46
+ airport = given(service_node.at_css('Ad')&.attr('pk')) do
47
+ find_by(:airport, meta: _1).first
48
+ end
49
+ # Build addresses and services
50
+ case SOURCE_TYPES.fetch(type_for(service_node))
51
+ when :address
52
+ fail "dangling address without airport" unless airport
53
+ addresses_from(service_node).each { airport.add_address(_1) }
54
+ when :service
55
+ given service_from(service_node) do |service|
56
+ cache.frequence.css(%Q(Frequence:has(Service[pk="#{service_node['pk']}"]))).each do |frequence_node|
57
+ if frequency = frequency_from(frequence_node, service_node)
58
+ service.add_frequency frequency
59
+ end
60
+ end
61
+ if airport
62
+ airport.add_unit(service.unit) if airport.units.find(service.unit).none?
63
+ airport.add_service(service) if airport.services.find(service).none?
64
+ end
65
+ given service_node.at_css('Espace')&.attr('pk') do |espace_pk|
66
+ find_by(:airspace, meta: espace_pk).each do |airspace|
67
+ airspace.layers.each { _1.add_service(service) }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ # Assign fallback address (default A/A frequency) to all yet radioless airports
74
+ find_by(:airport).each do |airport|
75
+ unless airport.services.find_by(:service, type: :aerodrome_control_tower_service).any? || airport.addresses.any?
76
+ airport.add_address(fallback_address_for(airport.name))
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def type_for(service_node)
84
+ SOURCE_TYPES.include?(type = service_node.(:Service)) ? type : 'OTHER'
85
+ end
86
+
87
+ def addresses_from(service_node)
88
+ cache.frequence.css(%Q(Frequence:has(Service[pk="#{service_node['pk']}"]))).map do |frequence_node|
89
+ if frequency = frequency_from(frequence_node, service_node)
90
+ AIXM.address(
91
+ type: :radio_frequency,
92
+ address: frequency.transmission_f
93
+ ).tap do |address|
94
+ address.remarks = {
95
+ 'type' => service_node.(:Service),
96
+ 'indicatif/callsign' => frequency.callsigns.map { "#{_2} (#{_1})" }.join("/n")
97
+ }.to_remarks
98
+ end
99
+ end
100
+ end.compact
101
+ end
102
+
103
+ def fallback_address_for(callsign)
104
+ AIXM.address(
105
+ type: :radio_frequency,
106
+ address: AIXM.f(123.5, :mhz)
107
+ ).tap do |address|
108
+ address.remarks = {
109
+ 'type' => 'A/A',
110
+ 'indicatif/callsign' => callsign
111
+ }.to_remarks
112
+ end
113
+ end
114
+
115
+ def service_from(service_node)
116
+ service_type = CALLSIGNS.dig(service_node.(:IndicService), :service_type) || type_for(service_node)
117
+ service = find_by(:service, type: service_type).first
118
+ unit = service&.unit
119
+ unless service
120
+ service = AIXM.service(type: service_type)
121
+ unit = find_by(:unit, name: service_node.(:IndicLieu), type: service.guessed_unit_type).first
122
+ unless unit
123
+ unit = AIXM.unit(
124
+ source: source(section: 'GEN', position: service_node.line),
125
+ organisation: organisation_lf,
126
+ name: service_node.(:IndicLieu),
127
+ type: service.guessed_unit_type,
128
+ class: :icao
129
+ )
130
+ add unit
131
+ end
132
+ unit.add_service service
133
+ service
134
+ end
135
+ end
136
+
137
+ def frequency_from(frequence_node, service_node)
138
+ frequency = frequence_node.(:Frequence).to_f
139
+ case
140
+ when frequency >= 137
141
+ nil
142
+ when frequency < 108
143
+ warn("ignoring too low frequency `#{frequency}'", severe: false)
144
+ nil
145
+ else
146
+ AIXM.frequency(
147
+ transmission_f: AIXM.f(frequency, :mhz),
148
+ callsigns: callsigns_from(service_node)
149
+ ).tap do |frequency|
150
+ frequency.timetable = timetable_from(frequence_node.(:HorCode))
151
+ frequency.remarks = frequence_node.(:Remarque)
152
+ end
153
+ end
154
+ end
155
+
156
+ def callsigns_from(service_node)
157
+ if service_node.(:IndicService) == '.' # auto-information
158
+ %i(fr en).to_h { [_1, '(auto)'] }
159
+ else
160
+ warn("language other than french") unless service_node.(:Langue) == 'fr'
161
+ english = CALLSIGNS.fetch(service_node.(:IndicService)).fetch(:en)
162
+ warn("no english translation for callsign `#{service_node.(:IndicService)}'") unless english
163
+ {
164
+ fr: "#{service_node.(:IndicLieu)} #{service_node.(:IndicService)}",
165
+ en: ("#{service_node.(:IndicLieu)} #{english}" if english)
166
+ }.compact
167
+ end
168
+ end
169
+
170
+ end
171
+ end
172
+ end
data/lib/aipp/t_hash.rb CHANGED
@@ -18,14 +18,14 @@ module AIPP
18
18
 
19
19
  alias_method :tsort_each_node, :each_key
20
20
 
21
- def tsort_each_child(node, &block)
22
- fetch(node).each(&block)
21
+ def tsort_each_child(node, &)
22
+ fetch(node).each(&)
23
23
  end
24
24
 
25
25
  def tsort(node=nil)
26
26
  if node
27
27
  subhash = subhash_for node
28
- super().select { |n| subhash.include? n }
28
+ super().select { subhash.include? _1 }
29
29
  else
30
30
  super()
31
31
  end
@@ -35,9 +35,8 @@ module AIPP
35
35
 
36
36
  def subhash_for(node, memo=[])
37
37
  memo.tap do |m|
38
- fail TSort::Cyclic if m.include? node
39
38
  m << node
40
- fetch(node).each { |n| subhash_for(n, m) }
39
+ (fetch(node) - m).each { subhash_for(_1, m) }
41
40
  end
42
41
  end
43
42
 
data/lib/aipp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module AIPP
2
- VERSION = "0.2.4".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
data/lib/aipp.rb CHANGED
@@ -1,44 +1,50 @@
1
+ require 'debug/session'
1
2
  require 'forwardable'
2
3
  require 'colorize'
3
- require 'pry'
4
- require 'pry-rescue'
5
4
  require 'optparse'
6
5
  require 'yaml'
6
+ require 'csv'
7
+ require 'roo'
7
8
  require 'pathname'
9
+ require 'tmpdir'
10
+ require 'net/http'
8
11
  require 'open-uri'
9
12
  require 'securerandom'
10
13
  require 'tsort'
11
14
  require 'ostruct'
12
15
  require 'date'
13
16
  require 'nokogiri'
14
- require 'nokogumbo'
15
17
  require 'pdf-reader'
16
18
  require 'json'
17
19
  require 'zip'
20
+ require 'airac'
18
21
  require 'aixm'
19
22
 
20
23
  require 'active_support'
21
24
  require 'active_support/core_ext/object/blank'
22
25
  require 'active_support/core_ext/string'
23
- require_relative 'core_ext/object'
26
+
24
27
  require_relative 'core_ext/integer'
25
28
  require_relative 'core_ext/string'
26
29
  require_relative 'core_ext/nil_class'
27
30
  require_relative 'core_ext/enumerable'
28
31
  require_relative 'core_ext/hash'
32
+ require_relative 'core_ext/nokogiri'
29
33
 
30
34
  require_relative 'aipp/version'
35
+ require_relative 'aipp/debugger'
31
36
  require_relative 'aipp/pdf'
32
37
  require_relative 'aipp/border'
33
38
  require_relative 'aipp/t_hash'
34
39
  require_relative 'aipp/executable'
35
- require_relative 'aipp/airac'
36
40
  require_relative 'aipp/patcher'
37
41
  require_relative 'aipp/aip'
38
42
  require_relative 'aipp/parser'
39
43
  require_relative 'aipp/downloader'
40
44
 
41
45
  # Disable "did you mean?" suggestions
46
+ #
47
+ # @!visibility private
42
48
  module DidYouMean::Correctable
43
49
  remove_method :to_s
44
50
  end
@@ -5,11 +5,11 @@ module Enumerable
5
5
  # returning an array of these sub-enumerables.
6
6
  #
7
7
  # @example
8
- # [1, 2, 0, 3, 4].split { |e| e == 0 } # => [[1, 2], [3, 4]]
9
- # [1, 2, 0, 3, 4].split(0) # => [[1, 2], [3, 4]]
10
- # [0, 0, 1, 0, 2].split(0) # => [[], [] [1], [2]]
11
- # [1, 0, 0, 2, 3].split(0) # => [[1], [], [2], [3]]
12
- # [1, 0, 2, 0, 0].split(0) # => [[1], [2]]
8
+ # [1, 2, 0, 3, 4].split { _1 == 0 } # => [[1, 2], [3, 4]]
9
+ # [1, 2, 0, 3, 4].split(0) # => [[1, 2], [3, 4]]
10
+ # [0, 0, 1, 0, 2].split(0) # => [[], [] [1], [2]]
11
+ # [1, 0, 0, 2, 3].split(0) # => [[1], [], [2], [3]]
12
+ # [1, 0, 2, 0, 0].split(0) # => [[1], [2]]
13
13
  #
14
14
  # @note While similar to +Array#split+ from ActiveSupport, this core
15
15
  # extension works for all enumerables and therefore works fine with.
@@ -20,9 +20,9 @@ module Enumerable
20
20
  # @yield [Object] element to analyze
21
21
  # @yieldreturn [Boolean] whether to split at this element or not
22
22
  # @return [Array]
23
- def split(*args, &block)
23
+ def split(*args, &)
24
24
  [].tap do |array|
25
- while index = slice((start ||= 0)...length).find_index(*args, &block)
25
+ while index = slice((start ||= 0)...length).find_index(*args, &)
26
26
  array << slice(start...start+index)
27
27
  start += index + 1
28
28
  end
@@ -35,7 +35,7 @@ module Enumerable
35
35
  # an array of subsequent elements which don't match the chunk condition.
36
36
  #
37
37
  # @example
38
- # [1, 10, 11, 12, 2, 20, 21, 3, 30, 31, 32].group_by_chunks { |i| i < 10 }
38
+ # [1, 10, 11, 12, 2, 20, 21, 3, 30, 31, 32].group_by_chunks { _1 < 10 }
39
39
  # # => { 1 => [10, 11, 12], 2 => [20, 21], 3 => [30, 31, 32] }
40
40
  #
41
41
  # @note The first element must match the chunk condition.
@@ -46,7 +46,7 @@ module Enumerable
46
46
  # @return [Hash]
47
47
  def group_by_chunks
48
48
  fail(ArgumentError, "first element must match chunk condition") unless yield(first)
49
- slice_when { |_, e| yield(e) }.map { |e| [e.first, e[1..]] }.to_h
49
+ slice_when { yield(_2) }.map { [_1.first, _1[1..]] }.to_h
50
50
  end
51
51
 
52
52
  end
data/lib/core_ext/hash.rb CHANGED
@@ -4,8 +4,8 @@ class Hash
4
4
  #
5
5
  # Similar to +fetch+, search the hash keys for the search string and return
6
6
  # the corresponding value. Unlike +fetch+, however, if a hash key is a Regexp,
7
- # the search string is matched against this Regexp. The hash is searched
8
- # in it's natural order.
7
+ # the search argument is matched against this Regexp. The hash is searched
8
+ # in its natural order.
9
9
  #
10
10
  # @example
11
11
  # h = { /aa/ => :aa, /a/ => :a, 'b' => :b }
@@ -18,14 +18,30 @@ class Hash
18
18
  # @param default [Object] fallback value if no key matched
19
19
  # @return [Object] hash value
20
20
  # @raise [KeyError] no key matched and no default given
21
- def metch(search, default=nil)
21
+ def metch(search, default=:__n_o_n_e__)
22
22
  fetch search
23
23
  rescue KeyError
24
24
  each do |key, value|
25
25
  next unless key.is_a? Regexp
26
- return value if search.match? key
26
+ return value if key.match? search
27
27
  end
28
- default ? default : raise(KeyError, "no match found: #{search.inspect}")
28
+ raise(KeyError, "no match found: #{search.inspect}") if default == :__n_o_n_e__
29
+ default
29
30
  end
30
31
 
32
+ # Compile a titles/texts hash to remarks Markdown string
33
+ #
34
+ # @example
35
+ # { name: 'foobar', ignore: => nil, 'count/quantité' => 3 }.to_remarks
36
+ # # => "NAME\nfoobar\n\nCOUNT/QUANTITÉ\n3"
37
+ # { ignore: nil, ignore_as_well: "" }.to_remarks
38
+ # # => nil
39
+ #
40
+ # @return [String, nil] compiled remarks
41
+ def to_remarks
42
+ map { |k, v| "**#{k.to_s.upcase}**\n#{v}" unless v.blank? }.
43
+ compact.
44
+ join("\n\n").
45
+ blank_to_nil
46
+ end
31
47
  end
@@ -0,0 +1,54 @@
1
+ module Nokogiri
2
+ module XML
3
+ class Element
4
+
5
+ BOOLEANIZE_AS_TRUE_RE = /^(true|yes|oui|ja)$/i.freeze
6
+ BOOLEANIZE_AS_FALSE_RE = /^(false|no|non|nein)$/i.freeze
7
+
8
+ # Traverse all child elements and build a hash mapping the symbolized
9
+ # child node name to the child content.
10
+ #
11
+ # @return [Hash]
12
+ def contents
13
+ @contents ||= elements.to_h { [_1.name.to_sym, _1.content] }
14
+ end
15
+
16
+ # Shortcut to query +contents+ array which accepts both String or
17
+ # Symbol queries as well as query postfixes.
18
+ #
19
+ # @example query optional content for :key
20
+ # element.(:key) # same as element.contents[:key]
21
+ #
22
+ # @example query mandatory content for :key
23
+ # element.(:key!) # fails if the key does not exist
24
+ #
25
+ # @example query boolean content for :key
26
+ # element.(:key?) # returns true or false
27
+ #
28
+ # @see +BOOLEANIZE_AS_TRUE_RE+ and +BOOLEANIZE_AS_FALSE_RE+ define the
29
+ # regular expressions which convert the content to boolean. Furthermore,
30
+ # nil is interpreted as false as well.
31
+ #
32
+ # @raise KeyError mandatory or boolean content not found
33
+ # @return [String, Boolean]
34
+ def call(query)
35
+ case query
36
+ when /\?$/ then booleanize(contents.fetch(query[...-1].to_sym))
37
+ when /\!$/ then contents.fetch(query[...-1].to_sym)
38
+ else contents[query.to_sym]
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def booleanize(content)
45
+ case content
46
+ when nil then false
47
+ when BOOLEANIZE_AS_TRUE_RE then true
48
+ when BOOLEANIZE_AS_FALSE_RE then false
49
+ else fail(KeyError, "`#{content}' not recognized as boolean")
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -23,7 +23,7 @@ class String
23
23
  gsub(/[#{AIXM::MIN}]{2}|[#{AIXM::SEC}]/, '"'). # unify quotes
24
24
  gsub(/[#{AIXM::MIN}]/, "'"). # unify apostrophes
25
25
  gsub(/"[[:blank:]]*(.*?)[[:blank:]]*"/m, '"\1"'). # remove whitespace within quotes
26
- split(/\r?\n/).map { |s| s.strip.blank_to_nil }.compact.join("\n") # remove blank lines
26
+ split(/\r?\n/).map { _1.strip.blank_to_nil }.compact.join("\n") # remove blank lines
27
27
  end
28
28
 
29
29
  # Strip and collapse unnecessary whitespace
@@ -32,64 +32,53 @@ class String
32
32
  # are preserved and not collapsed into one space.
33
33
  #
34
34
  # @example
35
- # " foo\n\nbar \r".copact # => "foo\nbar"
35
+ # " foo\n\nbar \r".compact # => "foo\nbar"
36
36
  #
37
37
  # @return [String] compacted string
38
38
  def compact
39
- split("\n").map { |s| s.squish.blank_to_nil }.compact.join("\n")
39
+ split("\n").map { _1.squish.blank_to_nil }.compact.join("\n")
40
40
  end
41
41
 
42
- # Calculate the correlation of two strings by counting mutual words
43
- #
44
- # Both strings are normalized as follows:
45
- # * remove accents, umlauts etc
46
- # * remove everything but members of the +\w+ class
47
- # * downcase
48
- #
49
- # The normalized strings are split into words. Only words fulfilling either
50
- # of the following conditions are taken into consideration:
51
- # * words present in and translated by the +synonyms+ map
52
- # * words of at least 5 characters length
53
- # * words consisting of exactly one letter followed by any number of digits
54
- # (an optional whitespace between the two is ignored, e.g. "D 25" is the
55
- # same as "D25")
56
- #
57
- # The +synonyms+ map is an array where terms in even positions map to their
58
- # synonym in the following (odd) position:
59
- #
60
- # SYNONYMS = ['term1', 'synonym1', 'term2', 'synonym2']
42
+ # Similar to +strip+, but remove any leading or trailing non-letters/numbers
43
+ # which includes whitespace
44
+ def full_strip
45
+ remove(/\A[^\p{L}\p{N}]*|[^\p{L}\p{N}]*\z/)
46
+ end
47
+
48
+ # Similar to +scan+, but remove matches from the string
49
+ def extract(pattern)
50
+ scan(pattern).tap { remove! pattern }
51
+ end
52
+
53
+ # Apply the patterns in the given order and return...
54
+ # * first capture group - if a pattern matches and contains a capture group
55
+ # * entire match - if a pattern matches and contains no capture group
56
+ # * +default+ - if no pattern matches and a +default+ is set
57
+ # * +nil+ - if no pattern matches and no +default+ is set
61
58
  #
62
59
  # @example
63
- # subject = "Truck en route on N 3 sud"
64
- # subject.correlate("my car is on D25") # => 0
65
- # subject.correlate("my truck is on D25") # => 1
66
- # subject.correlate("my truck is on N3") # => 2
67
- # subject.correlate("south", ['sud', 'south']) # => 1
68
- #
69
- # @param other [String] string to compare with
70
- # @param synonyms [Array<String>] array of synonym pairs
71
- # @return [Integer] 0 for unrelated strings and positive integers for related
72
- # strings with higher numbers indicating tighter correlation
73
- def correlate(other, synonyms=[])
74
- self_words, other_words = [self, other].map do |string|
75
- string.
76
- unicode_normalize(:nfd).
77
- downcase.gsub(/[-\u2013]/, ' ').
78
- remove(/[^\w\s]/).
79
- gsub(/\b(\w)\s?(\d+)\b/, '\1\2').
80
- compact.
81
- split(/\W+/).
82
- map { |w| (i = synonyms.index(w)).nil? ? w : (i.odd? ? w : synonyms[i + 1]).upcase }.
83
- keep_if { |w| w.match?(/\w{5,}|\w\d+|[[:upper:]]/) }.
84
- uniq
60
+ # "A/A: 123.5 mhz".first_match(/123\.5/) # => "123.5"
61
+ # "A/A: 123.5 mhz".first_match(/:\s+([\d.]+)/) # => "123.5"
62
+ # "A/A: 123.5 mhz".first_match(/121\.5/) # nil
63
+ # "A/A: 123.5 mhz".first_match(/(121\.5)/) # nil
64
+ # "A/A: 123.5 mhz".first_match(/121\.5/, default: "123") # "123"
65
+ #
66
+ # @param patterns [Array<Regexp>] one or more patterns to apply in order
67
+ # @param default [String] string to return instead of +nil+ if the pattern
68
+ # doesn't match
69
+ # @return [String, nil]
70
+ def first_match(*patterns, default: nil)
71
+ patterns.each do |pattern|
72
+ if captures = match(pattern)
73
+ return captures[1] || captures[0]
74
+ end
85
75
  end
86
- (self_words & other_words).count
76
+ default
87
77
  end
88
78
 
89
- # Similar to +strip+, but remove any leading or trailing non-letters/numbers
90
- # which includes whitespace
91
- def full_strip
92
- remove(/\A[^\p{L}\p{N}]*|[^\p{L}\p{N}]*\z/)
79
+ # Remove all XML/HTML tags and entities from the string
80
+ def strip_markup
81
+ self.gsub(/<.*?>|&[#\da-z]+;/i, '')
93
82
  end
94
83
 
95
84
  # Same as +to_f+ but accept both dot and comma as decimal separator
@@ -104,21 +93,4 @@ class String
104
93
  sub(/,/, '.').to_f
105
94
  end
106
95
 
107
- # Add spaces between obviously glued words:
108
- # * camel glued words
109
- # * three-or-more-letter and number-only words
110
- #
111
- # @example
112
- # "thisString has spaceProblems".unglue # => "this String has space problems"
113
- # "the first123meters of D25".unglue # => "the first 123 meters of D25"
114
- #
115
- # @return [String] unglued string
116
- def unglue
117
- self.dup.tap do |string|
118
- [/([[:lower:]])([[:upper:]])/, /([[:alpha:]]{3,})(\d)/, /(\d)([[:alpha:]]{3,})/].freeze.each do |regexp|
119
- string.gsub!(regexp, '\1 \2')
120
- end
121
- end
122
- end
123
-
124
96
  end
data.tar.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+  h�ƅ:�/n�!�Ud�����.u��<-���"譈!�����Z<��V�����Z�{ � ����c��M$���J�Z . �n��C�5[��f��XR̘�H@y9d�!�
2
+ \�ۃ�{W!`��m���nC��uk2���qOܱsAy��l;��U�?y�!�4�=;v��4�[���]P�j]s*ݷ!�g�:�\ӬD��8�,�*,^��zA�4��,1�:�x�)��š�!z�t���?W$��1�k