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