metar-parser 1.5.0 → 1.7.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
- 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 +29 -20
- 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 +92 -52
- metadata +53 -39
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
|
49
|
-
|
55
|
+
|
56
|
+
(m[3] == 'N' ? 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)
|