aipp 0.2.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -0
- data/CHANGELOG.md +38 -0
- data/README.md +222 -88
- data/exe/aip2aixm +2 -2
- data/exe/aip2ofmx +2 -2
- data/lib/aipp/aip.rb +113 -31
- data/lib/aipp/border.rb +77 -46
- data/lib/aipp/debugger.rb +101 -0
- data/lib/aipp/downloader.rb +39 -26
- data/lib/aipp/executable.rb +41 -22
- data/lib/aipp/parser.rb +94 -21
- data/lib/aipp/patcher.rb +5 -2
- data/lib/aipp/pdf.rb +1 -1
- data/lib/aipp/regions/LF/README.md +49 -0
- data/lib/aipp/regions/LF/aerodromes.rb +223 -0
- data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
- data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
- data/lib/aipp/regions/LF/designated_points.rb +47 -0
- data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
- data/lib/aipp/regions/LF/helipads.rb +122 -0
- data/lib/aipp/regions/LF/helpers/base.rb +218 -0
- data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
- data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
- data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
- data/lib/aipp/regions/LF/obstacles.rb +153 -0
- data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
- data/lib/aipp/regions/LF/services.rb +172 -0
- data/lib/aipp/t_hash.rb +4 -5
- data/lib/aipp/version.rb +1 -1
- data/lib/aipp.rb +11 -5
- data/lib/core_ext/enumerable.rb +9 -9
- data/lib/core_ext/hash.rb +21 -5
- data/lib/core_ext/nokogiri.rb +54 -0
- data/lib/core_ext/string.rb +38 -66
- data.tar.gz.sig +2 -0
- metadata +180 -188
- metadata.gz.sig +0 -0
- data/.gitignore +0 -8
- data/.ruby-version +0 -1
- data/.travis.yml +0 -8
- data/.yardopts +0 -3
- data/Guardfile +0 -7
- data/TODO.md +0 -6
- data/aipp.gemspec +0 -44
- data/gems.rb +0 -3
- data/lib/aipp/airac.rb +0 -55
- data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
- data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
- data/lib/aipp/regions/LF/AD-2.rb +0 -313
- data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
- data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
- data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
- data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
- data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
- data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
- data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
- data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
- data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
- data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
- data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
- data/lib/aipp/regions/LF/helpers/common.rb +0 -217
- data/lib/core_ext/object.rb +0 -43
- data/rakefile.rb +0 -12
- data/spec/fixtures/archive.zip +0 -0
- data/spec/fixtures/border.geojson +0 -201
- data/spec/fixtures/document.pdf +0 -0
- data/spec/fixtures/document.pdf.json +0 -1
- data/spec/fixtures/new.html +0 -6
- data/spec/fixtures/new.pdf +0 -0
- data/spec/fixtures/new.txt +0 -1
- data/spec/lib/aipp/airac_spec.rb +0 -98
- data/spec/lib/aipp/border_spec.rb +0 -135
- data/spec/lib/aipp/downloader_spec.rb +0 -81
- data/spec/lib/aipp/patcher_spec.rb +0 -46
- data/spec/lib/aipp/pdf_spec.rb +0 -124
- data/spec/lib/aipp/t_hash_spec.rb +0 -44
- data/spec/lib/aipp/version_spec.rb +0 -7
- data/spec/lib/core_ext/enumberable_spec.rb +0 -76
- data/spec/lib/core_ext/hash_spec.rb +0 -27
- data/spec/lib/core_ext/integer_spec.rb +0 -15
- data/spec/lib/core_ext/nil_class_spec.rb +0 -11
- data/spec/lib/core_ext/string_spec.rb +0 -112
- data/spec/sounds/failure.mp3 +0 -0
- data/spec/sounds/success.mp3 +0 -0
- 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, &
|
22
|
-
|
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 {
|
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 {
|
39
|
+
(fetch(node) - m).each { subhash_for(_1, m) }
|
41
40
|
end
|
42
41
|
end
|
43
42
|
|
data/lib/aipp/version.rb
CHANGED
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
|
-
|
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
|
data/lib/core_ext/enumerable.rb
CHANGED
@@ -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 {
|
9
|
-
# [1, 2, 0, 3, 4].split(0)
|
10
|
-
# [0, 0, 1, 0, 2].split(0)
|
11
|
-
# [1, 0, 0, 2, 3].split(0)
|
12
|
-
# [1, 0, 2, 0, 0].split(0)
|
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, &
|
23
|
+
def split(*args, &)
|
24
24
|
[].tap do |array|
|
25
|
-
while index = slice((start ||= 0)...length).find_index(*args, &
|
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 {
|
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 {
|
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
|
8
|
-
# in
|
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
|
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
|
26
|
+
return value if key.match? search
|
27
27
|
end
|
28
|
-
|
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
|
data/lib/core_ext/string.rb
CHANGED
@@ -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 {
|
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".
|
35
|
+
# " foo\n\nbar \r".compact # => "foo\nbar"
|
36
36
|
#
|
37
37
|
# @return [String] compacted string
|
38
38
|
def compact
|
39
|
-
split("\n").map {
|
39
|
+
split("\n").map { _1.squish.blank_to_nil }.compact.join("\n")
|
40
40
|
end
|
41
41
|
|
42
|
-
#
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
#
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
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
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
# @param
|
70
|
-
# @param
|
71
|
-
#
|
72
|
-
#
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
76
|
+
default
|
87
77
|
end
|
88
78
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
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