metar-parser 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -48
- data/Rakefile +2 -1
- data/lib/metar/data/base.rb +16 -10
- data/lib/metar/data/density_altitude.rb +16 -10
- data/lib/metar/data/direction.rb +10 -4
- data/lib/metar/data/distance.rb +27 -20
- data/lib/metar/data/lightning.rb +69 -60
- data/lib/metar/data/observer.rb +26 -20
- data/lib/metar/data/pressure.rb +28 -22
- data/lib/metar/data/remark.rb +146 -130
- data/lib/metar/data/runway_visible_range.rb +98 -78
- data/lib/metar/data/sky_condition.rb +68 -57
- data/lib/metar/data/speed.rb +21 -14
- data/lib/metar/data/station_code.rb +8 -4
- data/lib/metar/data/temperature.rb +21 -14
- data/lib/metar/data/temperature_and_dew_point.rb +22 -16
- data/lib/metar/data/time.rb +57 -47
- data/lib/metar/data/variable_wind.rb +30 -19
- data/lib/metar/data/vertical_visibility.rb +27 -21
- data/lib/metar/data/visibility.rb +91 -79
- data/lib/metar/data/visibility_remark.rb +16 -5
- data/lib/metar/data/weather_phenomenon.rb +92 -74
- data/lib/metar/data/wind.rb +105 -93
- data/lib/metar/data.rb +25 -23
- data/lib/metar/i18n.rb +5 -2
- data/lib/metar/parser.rb +46 -21
- data/lib/metar/raw.rb +32 -44
- data/lib/metar/report.rb +31 -20
- data/lib/metar/station.rb +28 -19
- data/lib/metar/version.rb +3 -1
- data/lib/metar.rb +2 -1
- data/locales/de.yml +1 -0
- data/locales/en.yml +1 -0
- data/locales/it.yml +1 -0
- data/locales/pt-BR.yml +1 -0
- data/spec/data/density_altitude_spec.rb +2 -1
- data/spec/data/distance_spec.rb +2 -1
- data/spec/data/lightning_spec.rb +26 -9
- data/spec/data/pressure_spec.rb +2 -0
- data/spec/data/remark_spec.rb +26 -9
- data/spec/data/runway_visible_range_spec.rb +71 -35
- data/spec/data/sky_condition_spec.rb +63 -19
- data/spec/data/speed_spec.rb +2 -0
- data/spec/data/temperature_spec.rb +2 -1
- data/spec/data/variable_wind_spec.rb +2 -0
- data/spec/data/vertical_visibility_spec.rb +4 -4
- data/spec/data/visibility_remark_spec.rb +2 -1
- data/spec/data/visibility_spec.rb +46 -25
- data/spec/data/weather_phenomenon_spec.rb +79 -24
- data/spec/data/wind_spec.rb +156 -38
- data/spec/i18n_spec.rb +2 -0
- data/spec/parser_spec.rb +192 -64
- data/spec/raw_spec.rb +40 -68
- data/spec/report_spec.rb +27 -25
- data/spec/spec_helper.rb +5 -6
- data/spec/station_spec.rb +43 -44
- metadata +56 -42
data/lib/metar/raw.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'date'
|
2
4
|
require 'net/ftp'
|
3
5
|
require 'time'
|
@@ -7,21 +9,23 @@ module Metar
|
|
7
9
|
class Base
|
8
10
|
attr_reader :metar
|
9
11
|
attr_reader :time
|
10
|
-
alias
|
12
|
+
alias to_s metar
|
11
13
|
end
|
12
14
|
|
13
15
|
##
|
14
16
|
# Use this class when you have a METAR string and the date of reading
|
15
17
|
class Data < Base
|
16
18
|
def initialize(metar, time = nil)
|
17
|
-
if time
|
18
|
-
warn <<-
|
19
|
+
if time.nil?
|
20
|
+
warn <<-WARNING
|
19
21
|
Using Metar::Raw::Data without a time parameter is deprecated.
|
20
22
|
Please supply the reading time as the second parameter.
|
21
|
-
|
23
|
+
WARNING
|
22
24
|
time = Time.now
|
23
25
|
end
|
24
|
-
|
26
|
+
|
27
|
+
@metar = metar
|
28
|
+
@time = time
|
25
29
|
end
|
26
30
|
end
|
27
31
|
|
@@ -39,6 +43,7 @@ module Metar
|
|
39
43
|
|
40
44
|
def time
|
41
45
|
return @time if @time
|
46
|
+
|
42
47
|
dom = day_of_month
|
43
48
|
date = Date.today
|
44
49
|
loop do
|
@@ -57,60 +62,42 @@ module Metar
|
|
57
62
|
def datetime
|
58
63
|
datetime = metar[/^\w{4} (\d{6})Z/, 1]
|
59
64
|
raise "The METAR string must have a 6 digit datetime" if datetime.nil?
|
65
|
+
|
60
66
|
datetime
|
61
67
|
end
|
62
68
|
|
63
69
|
def day_of_month
|
64
70
|
dom = datetime[0..1].to_i
|
65
71
|
raise "Day of month must be at most 31" if dom > 31
|
66
|
-
raise "Day of month must be greater than 0" if dom
|
72
|
+
raise "Day of month must be greater than 0" if dom.zero?
|
73
|
+
|
67
74
|
dom
|
68
75
|
end
|
69
76
|
end
|
70
77
|
|
71
78
|
# Collects METAR data from the NOAA site via FTP
|
72
79
|
class Noaa < Base
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
@@connection.login
|
86
|
-
@@connection.chdir('data/observations/metar/stations')
|
87
|
-
@@connection.passive = true
|
88
|
-
end
|
89
|
-
|
90
|
-
def disconnect
|
91
|
-
return if @@connection.nil?
|
92
|
-
@@connection.close
|
93
|
-
@@connection = nil
|
94
|
-
end
|
95
|
-
|
96
|
-
def fetch(cccc)
|
97
|
-
attempts = 0
|
98
|
-
while attempts < 2
|
99
|
-
begin
|
100
|
-
s = ''
|
101
|
-
connection.retrbinary("RETR #{ cccc }.TXT", 1024) do |chunk|
|
102
|
-
s << chunk
|
103
|
-
end
|
104
|
-
disconnect
|
105
|
-
return s
|
106
|
-
rescue Net::FTPPermError, Net::FTPTempError, EOFError => e
|
107
|
-
connect
|
108
|
-
attempts += 1
|
80
|
+
def self.fetch(cccc)
|
81
|
+
connection = Net::FTP.new('tgftp.nws.noaa.gov')
|
82
|
+
connection.login
|
83
|
+
connection.chdir('data/observations/metar/stations')
|
84
|
+
connection.passive = true
|
85
|
+
|
86
|
+
attempts = 0
|
87
|
+
while attempts < 2
|
88
|
+
begin
|
89
|
+
s = ''
|
90
|
+
connection.retrbinary("RETR #{cccc}.TXT", 1024) do |chunk|
|
91
|
+
s += chunk
|
109
92
|
end
|
93
|
+
connection.close
|
94
|
+
return s
|
95
|
+
rescue Net::FTPPermError, Net::FTPTempError, EOFError
|
96
|
+
attempts += 1
|
110
97
|
end
|
111
|
-
raise "Net::FTP.retrbinary failed #{attempts} times"
|
112
98
|
end
|
113
99
|
|
100
|
+
raise "Net::FTP.retrbinary failed #{attempts} times"
|
114
101
|
end
|
115
102
|
|
116
103
|
# Station is a string containing the CCCC code, or
|
@@ -124,7 +111,7 @@ module Metar
|
|
124
111
|
@data
|
125
112
|
end
|
126
113
|
# #raw is deprecated, use #data
|
127
|
-
alias
|
114
|
+
alias raw data
|
128
115
|
|
129
116
|
def time
|
130
117
|
fetch
|
@@ -140,6 +127,7 @@ module Metar
|
|
140
127
|
|
141
128
|
def fetch
|
142
129
|
return if @data
|
130
|
+
|
143
131
|
@data = Noaa.fetch(@cccc)
|
144
132
|
parse
|
145
133
|
end
|
data/lib/metar/report.rb
CHANGED
@@ -1,24 +1,27 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "metar/data/remark"
|
2
4
|
|
3
5
|
module Metar
|
4
6
|
class Report
|
5
|
-
ATTRIBUTES =
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
7
|
+
ATTRIBUTES = %i(
|
8
|
+
station_name
|
9
|
+
station_country
|
10
|
+
time
|
11
|
+
wind
|
12
|
+
visibility
|
13
|
+
minimum_visibility
|
14
|
+
present_weather
|
15
|
+
sky_summary
|
16
|
+
temperature
|
17
|
+
).freeze
|
16
18
|
|
17
19
|
attr_reader :parser, :station
|
18
20
|
|
19
21
|
def initialize(parser)
|
20
22
|
@parser = parser
|
21
|
-
|
23
|
+
# TODO: parser should return the station
|
24
|
+
@station = Station.find_by_cccc(@parser.station_code)
|
22
25
|
end
|
23
26
|
|
24
27
|
def station_name
|
@@ -38,7 +41,10 @@ module Metar
|
|
38
41
|
end
|
39
42
|
|
40
43
|
def time
|
41
|
-
|
44
|
+
format(
|
45
|
+
"%<hour>u:%02<min>u",
|
46
|
+
hour: @parser.time.hour, min: @parser.time.min
|
47
|
+
)
|
42
48
|
end
|
43
49
|
|
44
50
|
def observer
|
@@ -62,7 +68,7 @@ module Metar
|
|
62
68
|
end
|
63
69
|
|
64
70
|
def runway_visible_range
|
65
|
-
@parser.runway_visible_range.
|
71
|
+
@parser.runway_visible_range.map(&:to_s).join(', ')
|
66
72
|
end
|
67
73
|
|
68
74
|
def present_weather
|
@@ -70,12 +76,15 @@ module Metar
|
|
70
76
|
end
|
71
77
|
|
72
78
|
def sky_summary
|
73
|
-
|
79
|
+
if @parser.sky_conditions.empty?
|
80
|
+
return I18n.t('metar.sky_conditions.clear skies')
|
81
|
+
end
|
82
|
+
|
74
83
|
@parser.sky_conditions[-1].to_summary
|
75
84
|
end
|
76
85
|
|
77
86
|
def sky_conditions
|
78
|
-
@parser.sky_conditions.
|
87
|
+
@parser.sky_conditions.map(&:to_s).join(', ')
|
79
88
|
end
|
80
89
|
|
81
90
|
def vertical_visibility
|
@@ -100,7 +109,9 @@ module Metar
|
|
100
109
|
|
101
110
|
def to_s
|
102
111
|
attributes.collect do |attribute|
|
103
|
-
I18n.t('metar.' + attribute[:attribute].to_s + '.title') +
|
112
|
+
I18n.t('metar.' + attribute[:attribute].to_s + '.title') +
|
113
|
+
': ' +
|
114
|
+
attribute[:value]
|
104
115
|
end.join("\n") + "\n"
|
105
116
|
end
|
106
117
|
|
@@ -108,8 +119,8 @@ module Metar
|
|
108
119
|
|
109
120
|
def attributes
|
110
121
|
a = Metar::Report::ATTRIBUTES.map do |key|
|
111
|
-
value =
|
112
|
-
{:
|
122
|
+
value = send(key).to_s
|
123
|
+
{attribute: key, value: value} if !value.empty?
|
113
124
|
end
|
114
125
|
a.compact
|
115
126
|
end
|
data/lib/metar/station.rb
CHANGED
@@ -1,19 +1,24 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
2
5
|
require 'set'
|
3
6
|
|
4
7
|
# A Station can be created without downloading data from the Internet.
|
5
|
-
# The class downloads and caches the NOAA station list
|
6
|
-
#
|
8
|
+
# The class downloads and caches the NOAA station list
|
9
|
+
# when it is first requested.
|
10
|
+
# As soon of any of the attributes are read, the data is downloaded
|
11
|
+
# (if necessary), and attributes are set.
|
7
12
|
|
8
13
|
module Metar
|
9
14
|
class Station
|
10
|
-
NOAA_STATION_LIST_URL = '
|
15
|
+
NOAA_STATION_LIST_URL = 'https://tgftp.nws.noaa.gov/data/nsd_cccc.txt'
|
11
16
|
|
12
17
|
class << self
|
13
18
|
@nsd_cccc = nil # Contains the text of the station list
|
14
19
|
|
15
20
|
def countries
|
16
|
-
all_structures.reduce(Set.new) { |a, s| a.add(s[
|
21
|
+
all_structures.reduce(Set.new) { |a, s| a.add(s[:country]) }.to_a.sort
|
17
22
|
end
|
18
23
|
|
19
24
|
def all
|
@@ -30,33 +35,36 @@ module Metar
|
|
30
35
|
|
31
36
|
# Does the given CCCC code exist?
|
32
37
|
def exist?(cccc)
|
33
|
-
|
38
|
+
!find_data_by_cccc(cccc).nil?
|
34
39
|
end
|
35
40
|
|
36
41
|
def find_all_by_country(country)
|
37
42
|
all.select { |s| s.country == country }
|
38
43
|
end
|
39
44
|
|
40
|
-
def to_longitude(
|
41
|
-
m =
|
45
|
+
def to_longitude(longitude)
|
46
|
+
m = longitude.match(/^(\d+)-(\d+)([EW])/)
|
42
47
|
return nil if !m
|
48
|
+
|
43
49
|
(m[3] == 'E' ? 1.0 : -1.0) * (m[1].to_f + m[2].to_f / 60.0)
|
44
50
|
end
|
45
51
|
|
46
|
-
def to_latitude(
|
47
|
-
m =
|
52
|
+
def to_latitude(latitude)
|
53
|
+
m = latitude.match(/^(\d+)-(\d+)([SN])/)
|
48
54
|
return nil if !m
|
55
|
+
|
49
56
|
(m[3] == 'E' ? 1.0 : -1.0) * (m[1].to_f + m[2].to_f / 60.0)
|
50
57
|
end
|
51
58
|
end
|
52
59
|
|
53
60
|
attr_reader :cccc, :name, :state, :country, :longitude, :latitude, :raw
|
54
|
-
alias
|
61
|
+
alias code cccc
|
55
62
|
|
56
63
|
# No check is made on the existence of the station
|
57
64
|
def initialize(cccc, noaa_data)
|
58
65
|
raise "Station identifier must not be nil" if cccc.nil?
|
59
66
|
raise "Station identifier must not be empty" if cccc.to_s == ''
|
67
|
+
|
60
68
|
@cccc = cccc
|
61
69
|
load! noaa_data
|
62
70
|
end
|
@@ -76,7 +84,9 @@ module Metar
|
|
76
84
|
@structures = nil
|
77
85
|
|
78
86
|
def download_stations
|
79
|
-
|
87
|
+
uri = URI.parse(NOAA_STATION_LIST_URL)
|
88
|
+
response = Net::HTTP.get_response(uri)
|
89
|
+
response.body
|
80
90
|
end
|
81
91
|
|
82
92
|
def all_structures
|
@@ -88,13 +98,13 @@ module Metar
|
|
88
98
|
@nsd_cccc.each_line do |station|
|
89
99
|
fields = station.split(';')
|
90
100
|
@structures << {
|
91
|
-
cccc:
|
92
|
-
name:
|
93
|
-
state:
|
94
|
-
country:
|
95
|
-
latitude:
|
101
|
+
cccc: fields[0],
|
102
|
+
name: fields[3],
|
103
|
+
state: fields[4],
|
104
|
+
country: fields[5],
|
105
|
+
latitude: fields[7],
|
96
106
|
longitude: fields[8],
|
97
|
-
raw:
|
107
|
+
raw: station.clone
|
98
108
|
}
|
99
109
|
end
|
100
110
|
|
@@ -104,7 +114,6 @@ module Metar
|
|
104
114
|
def find_data_by_cccc(cccc)
|
105
115
|
all_structures.find { |station| station[:cccc] == cccc }
|
106
116
|
end
|
107
|
-
|
108
117
|
end
|
109
118
|
|
110
119
|
def load!(noaa_data)
|
data/lib/metar/version.rb
CHANGED
data/lib/metar.rb
CHANGED
data/locales/de.yml
CHANGED
data/locales/en.yml
CHANGED
data/locales/it.yml
CHANGED
data/locales/pt-BR.yml
CHANGED
data/spec/data/distance_spec.rb
CHANGED
data/spec/data/lightning_spec.rb
CHANGED
@@ -1,13 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
|
3
5
|
describe Metar::Data::Lightning do
|
4
6
|
context '.parse_chunks' do
|
5
7
|
[
|
6
|
-
[
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
[
|
8
|
+
[
|
9
|
+
'direction', 'LTG SE',
|
10
|
+
[:default, nil, ['SE']]
|
11
|
+
],
|
12
|
+
[
|
13
|
+
'distance direction', 'LTG DSNT SE',
|
14
|
+
[:default, 16_093.44, ['SE']]
|
15
|
+
],
|
16
|
+
[
|
17
|
+
'distance direction and direction', 'LTG DSNT NE AND W',
|
18
|
+
[:default, 16_093.44, %w(NE W)]
|
19
|
+
],
|
20
|
+
[
|
21
|
+
'distance direction-direction', 'LTG DSNT SE-SW',
|
22
|
+
[:default, 16_093.44, %w(SE SW)]
|
23
|
+
],
|
24
|
+
[
|
25
|
+
'distance all quandrants', 'LTG DSNT ALQDS',
|
26
|
+
[:default, 16_093.44, %w(N E S W)]
|
27
|
+
]
|
11
28
|
].each do |docstring, section, expected|
|
12
29
|
example docstring do
|
13
30
|
chunks = section.split(' ')
|
@@ -25,9 +42,9 @@ describe Metar::Data::Lightning do
|
|
25
42
|
end
|
26
43
|
|
27
44
|
it 'removes parsed chunks' do
|
28
|
-
chunks =
|
45
|
+
chunks = %w(LTG DSNT SE FOO)
|
29
46
|
|
30
|
-
|
47
|
+
described_class.parse_chunks(chunks)
|
31
48
|
|
32
49
|
expect(chunks).to eq(['FOO'])
|
33
50
|
end
|
@@ -39,9 +56,9 @@ describe Metar::Data::Lightning do
|
|
39
56
|
end
|
40
57
|
|
41
58
|
it "doesn't not fail if all chunks are parsed" do
|
42
|
-
chunks =
|
59
|
+
chunks = %w(LTG DSNT SE)
|
43
60
|
|
44
|
-
|
61
|
+
described_class.parse_chunks(chunks)
|
45
62
|
|
46
63
|
expect(chunks).to eq([])
|
47
64
|
end
|
data/spec/data/pressure_spec.rb
CHANGED
data/spec/data/remark_spec.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
|
3
5
|
describe Metar::Data::Remark do
|
4
6
|
context '.parse' do
|
5
7
|
it 'delegate to subclasses' do
|
6
|
-
expect(described_class.parse('21012')).
|
8
|
+
expect(described_class.parse('21012')).
|
9
|
+
to be_a(Metar::Data::TemperatureExtreme)
|
7
10
|
end
|
8
11
|
|
9
12
|
it 'returns nil for unrecognised' do
|
@@ -12,13 +15,14 @@ describe Metar::Data::Remark do
|
|
12
15
|
|
13
16
|
context '6-hour maximum or minimum' do
|
14
17
|
[
|
15
|
-
['positive maximum', '10046', [:maximum,
|
18
|
+
['positive maximum', '10046', [:maximum, 4.6]],
|
16
19
|
['negative maximum', '11012', [:maximum, -1.2]],
|
17
|
-
['positive minimum', '20046', [:minimum,
|
18
|
-
['negative minimum', '21012', [:minimum, -1.2]]
|
20
|
+
['positive minimum', '20046', [:minimum, 4.6]],
|
21
|
+
['negative minimum', '21012', [:minimum, -1.2]]
|
19
22
|
].each do |docstring, raw, expected|
|
20
23
|
example docstring do
|
21
|
-
expect(described_class.parse(raw)).
|
24
|
+
expect(described_class.parse(raw)).
|
25
|
+
to be_temperature_extreme(*expected)
|
22
26
|
end
|
23
27
|
end
|
24
28
|
end
|
@@ -27,7 +31,7 @@ describe Metar::Data::Remark do
|
|
27
31
|
it 'returns minimum and maximum' do
|
28
32
|
max, min = described_class.parse('400461006')
|
29
33
|
|
30
|
-
expect(max).to be_temperature_extreme(:maximum,
|
34
|
+
expect(max).to be_temperature_extreme(:maximum, 4.6)
|
31
35
|
expect(min).to be_temperature_extreme(:minimum, -0.6)
|
32
36
|
end
|
33
37
|
end
|
@@ -63,10 +67,23 @@ describe Metar::Data::Remark do
|
|
63
67
|
end
|
64
68
|
|
65
69
|
context 'automated station' do
|
66
|
-
|
67
70
|
[
|
68
|
-
[
|
69
|
-
|
71
|
+
[
|
72
|
+
'with precipitation dicriminator',
|
73
|
+
'AO1',
|
74
|
+
[
|
75
|
+
Metar::Data::AutomatedStationType,
|
76
|
+
:with_precipitation_discriminator
|
77
|
+
]
|
78
|
+
],
|
79
|
+
[
|
80
|
+
'without precipitation dicriminator',
|
81
|
+
'AO2',
|
82
|
+
[
|
83
|
+
Metar::Data::AutomatedStationType,
|
84
|
+
:without_precipitation_discriminator
|
85
|
+
]
|
86
|
+
]
|
70
87
|
].each do |docstring, raw, expected|
|
71
88
|
example docstring do
|
72
89
|
aut = described_class.parse(raw)
|