metar-parser 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/metar/data/base.rb +15 -0
  3. data/lib/metar/data/density_altitude.rb +15 -0
  4. data/lib/metar/data/direction.rb +9 -0
  5. data/lib/metar/data/distance.rb +27 -0
  6. data/lib/metar/data/lightning.rb +61 -0
  7. data/lib/metar/data/observer.rb +24 -0
  8. data/lib/metar/data/pressure.rb +23 -0
  9. data/lib/metar/data/remark.rb +98 -0
  10. data/lib/metar/data/runway_visible_range.rb +85 -0
  11. data/lib/metar/data/sky_condition.rb +61 -0
  12. data/lib/metar/data/speed.rb +22 -0
  13. data/lib/metar/data/station_code.rb +7 -0
  14. data/lib/metar/data/temperature.rb +21 -0
  15. data/lib/metar/data/temperature_and_dew_point.rb +18 -0
  16. data/lib/metar/data/time.rb +54 -0
  17. data/lib/metar/data/variable_wind.rb +25 -0
  18. data/lib/metar/data/vertical_visibility.rb +26 -0
  19. data/lib/metar/data/visibility.rb +71 -0
  20. data/lib/metar/data/visibility_remark.rb +8 -0
  21. data/lib/metar/data/weather_phenomenon.rb +86 -0
  22. data/lib/metar/data/wind.rb +82 -0
  23. data/lib/metar/data.rb +22 -636
  24. data/lib/metar/i18n.rb +6 -0
  25. data/lib/metar/parser.rb +165 -120
  26. data/lib/metar/report.rb +1 -1
  27. data/lib/metar/version.rb +2 -2
  28. data/lib/metar.rb +7 -6
  29. data/locales/de.yml +1 -0
  30. data/locales/en.yml +1 -0
  31. data/locales/it.yml +2 -1
  32. data/locales/pt-BR.yml +1 -0
  33. data/spec/data/density_altitude_spec.rb +12 -0
  34. data/spec/{distance_spec.rb → data/distance_spec.rb} +1 -1
  35. data/spec/data/lightning_spec.rb +49 -0
  36. data/spec/data/pressure_spec.rb +22 -0
  37. data/spec/data/remark_spec.rb +99 -0
  38. data/spec/data/runway_visible_range_spec.rb +92 -0
  39. data/spec/{sky_condition_spec.rb → data/sky_condition_spec.rb} +10 -6
  40. data/spec/data/speed_spec.rb +45 -0
  41. data/spec/data/temperature_spec.rb +36 -0
  42. data/spec/{variable_wind_spec.rb → data/variable_wind_spec.rb} +6 -6
  43. data/spec/{vertical_visibility_spec.rb → data/vertical_visibility_spec.rb} +2 -2
  44. data/spec/{data_spec.rb → data/visibility_remark_spec.rb} +1 -11
  45. data/spec/{visibility_spec.rb → data/visibility_spec.rb} +9 -7
  46. data/spec/{weather_phenomenon_spec.rb → data/weather_phenomenon_spec.rb} +7 -3
  47. data/spec/{wind_spec.rb → data/wind_spec.rb} +10 -7
  48. data/spec/parser_spec.rb +107 -13
  49. data/spec/report_spec.rb +12 -1
  50. data/spec/spec_helper.rb +1 -1
  51. data/spec/station_spec.rb +2 -1
  52. metadata +56 -31
  53. data/spec/pressure_spec.rb +0 -22
  54. data/spec/remark_spec.rb +0 -147
  55. data/spec/runway_visible_range_spec.rb +0 -81
  56. data/spec/speed_spec.rb +0 -45
  57. data/spec/temperature_spec.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b47782dda9aa78314cf6244e1d395acff88cd025
4
- data.tar.gz: 097ed5364bdf7ded700dd77b5722a863655ae2b9
3
+ metadata.gz: ae0329de719ceb6a7302bf28a0b2eb60224b6220
4
+ data.tar.gz: 2f480c17d9cd2f3df30ee882ebbf45605bdae90d
5
5
  SHA512:
6
- metadata.gz: ebf5217c8b8edbe61045f48f7293ed479864208021c16792b0d436292ac4513c74688fa0d09e05a23140cafb3b0dc93b97a6165b661b4e5f5d77e6ab42b4e4f5
7
- data.tar.gz: 84a588b82027c09eab3856a0dacafad06b2638a8ab34e1f75c5047f81af5018b7964e6a0319d8849a9918322060ea71bb4058f7affe7fb1f81d7892ce5ec016b
6
+ metadata.gz: a1c5d02054171c2b5d1a70d01caf6be3a2af8fa574e7a1b93595e0e5ce9e2b079bcab873bf2b2a82b9331c2dea2b7df01d493fe40f234d198c0f1c6b71712c32
7
+ data.tar.gz: ae061b3580ad88b8463af928ae8537bc58a99ae95c3553a9eb68a89320f4fa5712de1e6a84bfefe5cc0bdb7e877fbf7e619c59844e83a72a8e0f7d618ae244ee
@@ -0,0 +1,15 @@
1
+ class Metar::Data::Base
2
+ def self.parse(raw)
3
+ new(raw)
4
+ end
5
+
6
+ attr_reader :raw
7
+
8
+ def initialize(raw)
9
+ @raw = raw
10
+ end
11
+
12
+ def value
13
+ raw
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class Metar::Data::DensityAltitude < Metar::Data::Base
2
+ def self.parse(raw)
3
+ feet = raw[/^(\d+)(FT)/, 1]
4
+ height = Metar::Data::Distance.feet(feet)
5
+
6
+ new(raw, height: height)
7
+ end
8
+
9
+ attr_accessor :height
10
+
11
+ def initialize(raw, height:)
12
+ @raw = raw
13
+ @height = height
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ require "i18n"
2
+ require "m9t"
3
+
4
+ class Metar::Data::Direction < M9t::Direction
5
+ def initialize(direction)
6
+ direction = M9t::Direction::normalize(direction.to_f)
7
+ super(direction)
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ require "i18n"
2
+ require "m9t"
3
+
4
+ class Metar::Data::Distance < M9t::Distance
5
+ attr_accessor :units
6
+
7
+ # nil is taken to mean 'data unavailable'
8
+ def initialize(meters = nil)
9
+ @units = :meters
10
+ if meters
11
+ super
12
+ else
13
+ @value = nil
14
+ end
15
+ end
16
+
17
+ # Handles nil case differently to M9t::Distance
18
+ def to_s(options = {})
19
+ options = {
20
+ units: units,
21
+ precision: 0,
22
+ abbreviated: true,
23
+ }.merge(options)
24
+ return I18n.t("metar.distance.unknown") if @value.nil?
25
+ super(options)
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ class Metar::Data::Lightning < Metar::Data::Base
2
+ TYPE = {'' => :default}
3
+
4
+ def self.parse_chunks(chunks)
5
+ raw = chunks.shift
6
+ m = raw.match(/^LTG(|CG|IC|CC|CA)$/)
7
+ raise 'first chunk is not lightning' if m.nil?
8
+ type = TYPE[m[1]]
9
+
10
+ frequency = nil
11
+ distance = nil
12
+ directions = []
13
+
14
+ if chunks[0] == 'DSNT'
15
+ distance = Metar::Data::Distance.miles(10) # Should be >10SM, not 10SM
16
+ raw += " " + chunks.shift
17
+ end
18
+
19
+ loop do
20
+ if is_compass?(chunks[0])
21
+ direction = chunks.shift
22
+ raw += " " + direction
23
+ directions << direction
24
+ elsif chunks[0] == 'ALQDS'
25
+ directions += ['N', 'E', 'S', 'W']
26
+ raw += " " + chunks.shift
27
+ elsif chunks[0] =~ /^([NESW]{1,2})-([NESW]{1,2})$/
28
+ if is_compass?($1) and is_compass?($2)
29
+ directions += [$1, $2]
30
+ raw += " " + chunks.shift
31
+ else
32
+ break
33
+ end
34
+ elsif chunks[0] == 'AND'
35
+ raw += " " + chunks.shift
36
+ else
37
+ break
38
+ end
39
+ end
40
+
41
+ new(
42
+ raw,
43
+ frequency: frequency, type: type,
44
+ distance: distance, directions: directions
45
+ )
46
+ end
47
+
48
+ def self.is_compass?(s)
49
+ s =~ /^([NESW]|NE|SE|SW|NW)$/
50
+ end
51
+
52
+ attr_accessor :frequency
53
+ attr_accessor :type
54
+ attr_accessor :distance
55
+ attr_accessor :directions
56
+
57
+ def initialize(raw, frequency:, type:, distance:, directions:)
58
+ @raw = raw
59
+ @frequency, @type, @distance, @directions = frequency, type, distance, directions
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ class Metar::Data::Observer < Metar::Data::Base
2
+ def self.parse(raw)
3
+ case
4
+ when raw == 'AUTO' # WMO 15.4
5
+ new(raw, value: :auto)
6
+ when raw == 'COR' # WMO specified code word for correction
7
+ new(raw, value: :corrected)
8
+ when raw =~ /CC[A-Z]/ # Canadian correction
9
+ # Canada uses CCA for first correction, CCB for second, etc...
10
+ new(raw, value: :corrected)
11
+ when raw == 'RTD' # Delayed observation, no comments on observer
12
+ new(raw, value: :rtd)
13
+ else
14
+ new(nil, value: :real)
15
+ end
16
+ end
17
+
18
+ attr_reader :value
19
+
20
+ def initialize(raw, value:)
21
+ @raw = raw
22
+ @value = value
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ class Metar::Data::Pressure < Metar::Data::Base
2
+ def self.parse(raw)
3
+ case
4
+ when raw =~ /^Q(\d{4})$/
5
+ new(raw, pressure: M9t::Pressure.hectopascals($1.to_f))
6
+ when raw =~ /^A(\d{4})$/
7
+ new(raw, pressure: M9t::Pressure.inches_of_mercury($1.to_f / 100.0))
8
+ else
9
+ nil
10
+ end
11
+ end
12
+
13
+ attr_reader :pressure
14
+
15
+ def initialize(raw, pressure:)
16
+ @raw = raw
17
+ @pressure = pressure
18
+ end
19
+
20
+ def value
21
+ pressure.value
22
+ end
23
+ end
@@ -0,0 +1,98 @@
1
+ require "i18n"
2
+ require "m9t"
3
+
4
+ class Metar::Data::Remark
5
+ PRESSURE_CHANGE_CHARACTER = [
6
+ :increasing_then_decreasing, # 0
7
+ :increasing_then_steady, # 1
8
+ :increasing, # 2
9
+ :decreasing_or_steady_then_increasing, # 3
10
+ :steady, # 4
11
+ :decreasing_then_increasing, # 5
12
+ :decreasing_then_steady, # 6
13
+ :decreasing, # 7
14
+ :steady_then_decreasing, # 8
15
+ ]
16
+
17
+ INDICATOR_TYPE = {
18
+ 'TS' => :thunderstorm_information,
19
+ 'PWI' => :precipitation_identifier,
20
+ 'P' => :precipitation_amount,
21
+ }
22
+
23
+ COLOR_CODE = ['RED', 'AMB', 'YLO', 'GRN', 'WHT', 'BLU']
24
+
25
+ def self.parse(raw)
26
+ case raw
27
+ when /^([12])([01])(\d{3})$/
28
+ extreme = {'1' => :maximum, '2' => :minimum}[$1]
29
+ value = sign($2) * tenths($3)
30
+ Metar::Data::TemperatureExtreme.new(raw, extreme, value)
31
+ when /^4([01])(\d{3})([01])(\d{3})$/
32
+ [
33
+ Metar::Data::TemperatureExtreme.new(raw, :maximum, sign($1) * tenths($2)),
34
+ Metar::Data::TemperatureExtreme.new(raw, :minimum, sign($3) * tenths($4)),
35
+ ]
36
+ when /^5([0-8])(\d{3})$/
37
+ character = PRESSURE_CHANGE_CHARACTER[$1.to_i]
38
+ Metar::Data::PressureTendency.new(raw, character, tenths($2))
39
+ when /^6(\d{4})$/
40
+ Metar::Data::Precipitation.new(raw, 3, Metar::Data::Distance.new(inches_to_meters($1))) # actually 3 or 6 depending on reporting time
41
+ when /^7(\d{4})$/
42
+ Metar::Data::Precipitation.new(raw, 24, Metar::Data::Distance.new(inches_to_meters($1)))
43
+ when /^A[0O]([12])$/
44
+ type = [:with_precipitation_discriminator, :without_precipitation_discriminator][$1.to_i - 1]
45
+ Metar::Data::AutomatedStationType.new(raw, type)
46
+ when /^P(\d{4})$/
47
+ Metar::Data::Precipitation.new(raw, 1, Metar::Data::Distance.new(inches_to_meters($1)))
48
+ when /^T([01])(\d{3})([01])(\d{3})$/
49
+ temperature = Metar::Data::Temperature.new(sign($1) * tenths($2))
50
+ dew_point = Metar::Data::Temperature.new(sign($3) * tenths($4))
51
+ Metar::Data::HourlyTemperatureAndDewPoint.new(raw, temperature, dew_point)
52
+ when /^SLP(\d{3})$/
53
+ Metar::Data::SeaLevelPressure.new(raw, M9t::Pressure.hectopascals(tenths($1)))
54
+ when /^(#{INDICATOR_TYPE.keys.join('|')})NO$/
55
+ type = INDICATOR_TYPE[$1]
56
+ Metar::Data::SensorStatusIndicator.new(raw, :type, :not_available)
57
+ when /^(#{COLOR_CODE.join('|')})$/
58
+ Metar::Data::ColorCode.new(raw, $1)
59
+ when 'SKC'
60
+ Metar::Data::SkyCondition.new(raw)
61
+ when '$'
62
+ Metar::Data::MaintenanceNeeded.new(raw)
63
+ else
64
+ nil
65
+ end
66
+ end
67
+
68
+ def self.sign(digit)
69
+ case digit
70
+ when '0'
71
+ 1.0
72
+ when '1'
73
+ -1.0
74
+ else
75
+ raise "Unexpected sign: #{digit}"
76
+ end
77
+ end
78
+
79
+ def self.tenths(digits)
80
+ digits.to_f / 10.0
81
+ end
82
+
83
+ def self.inches_to_meters(digits)
84
+ digits.to_f * 0.000254
85
+ end
86
+ end
87
+
88
+ module Metar::Data
89
+ TemperatureExtreme = Struct.new(:raw, :extreme, :value)
90
+ PressureTendency = Struct.new(:raw, :character, :value)
91
+ Precipitation = Struct.new(:raw, :period, :amount)
92
+ AutomatedStationType = Struct.new(:raw, :type)
93
+ HourlyTemperatureAndDewPoint = Struct.new(:raw, :temperature, :dew_point)
94
+ SeaLevelPressure = Struct.new(:raw, :pressure)
95
+ SensorStatusIndicator = Struct.new(:raw, :type, :state)
96
+ ColorCode = Struct.new(:raw, :code)
97
+ MaintenanceNeeded = Struct.new(:raw)
98
+ end
@@ -0,0 +1,85 @@
1
+ class Metar::Data::RunwayVisibleRange < Metar::Data::Base
2
+ TENDENCY = {'' => nil, 'N' => :no_change, 'U' => :improving, 'D' => :worsening}
3
+ COMPARATOR = {'' => nil, 'P' => :more_than, 'M' => :less_than}
4
+ UNITS = {'' => :meters, 'FT' => :feet}
5
+
6
+ def self.parse(raw)
7
+ case
8
+ when raw =~ /^R(\d+[RLC]?)\/(P|M|)(\d{4})(FT|)\/?(N|U|D|)$/
9
+ designator = $1
10
+ comparator = COMPARATOR[$2]
11
+ count = $3.to_f
12
+ units = UNITS[$4]
13
+ tendency = TENDENCY[$5]
14
+ distance = Metar::Data::Distance.send(units, count)
15
+ visibility = Metar::Data::Visibility.new(
16
+ nil, distance: distance, comparator: comparator
17
+ )
18
+ new(
19
+ raw,
20
+ designator: designator, visibility1: visibility, tendency: tendency
21
+ )
22
+ when raw =~ /^R(\d+[RLC]?)\/(P|M|)(\d{4})V(P|M|)(\d{4})(FT|)\/?(N|U|D)?$/
23
+ designator = $1
24
+ comparator1 = COMPARATOR[$2]
25
+ count1 = $3.to_f
26
+ comparator2 = COMPARATOR[$4]
27
+ count2 = $5.to_f
28
+ units = UNITS[$6]
29
+ tendency = TENDENCY[$7]
30
+ distance1 = Metar::Data::Distance.send(units, count1)
31
+ distance2 = Metar::Data::Distance.send(units, count2)
32
+ visibility1 = Metar::Data::Visibility.new(
33
+ nil, distance: distance1, comparator: comparator1
34
+ )
35
+ visibility2 = Metar::Data::Visibility.new(
36
+ nil, distance: distance2, comparator: comparator2
37
+ )
38
+ new(
39
+ raw,
40
+ designator: designator,
41
+ visibility1: visibility1, visibility2: visibility2,
42
+ tendency: tendency, units: units
43
+ )
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ attr_reader :designator, :visibility1, :visibility2, :tendency
50
+
51
+ def initialize(
52
+ raw,
53
+ designator:, visibility1:, visibility2: nil, tendency: nil, units: :meters
54
+ )
55
+ @raw = raw
56
+ @designator, @visibility1, @visibility2, @tendency, @units = designator, visibility1, visibility2, tendency, units
57
+ end
58
+
59
+ def to_s
60
+ distance_options = {
61
+ abbreviated: true,
62
+ precision: 0,
63
+ units: @units,
64
+ }
65
+ s =
66
+ if @visibility2.nil?
67
+ I18n.t('metar.runway_visible_range.runway') +
68
+ ' ' + @designator +
69
+ ': ' + @visibility1.to_s(distance_options)
70
+ else
71
+ I18n.t('metar.runway_visible_range.runway') +
72
+ ' ' + @designator +
73
+ ': ' + I18n.t('metar.runway_visible_range.from') +
74
+ ' ' + @visibility1.to_s(distance_options) +
75
+ ' ' + I18n.t('metar.runway_visible_range.to') +
76
+ ' ' + @visibility2.to_s(distance_options)
77
+ end
78
+
79
+ if ! tendency.nil?
80
+ s += ' ' + I18n.t("tendency.#{tendency}")
81
+ end
82
+
83
+ s
84
+ end
85
+ end
@@ -0,0 +1,61 @@
1
+ class Metar::Data::SkyCondition < Metar::Data::Base
2
+ QUANTITY = {'BKN' => 'broken', 'FEW' => 'few', 'OVC' => 'overcast', 'SCT' => 'scattered'}
3
+ CONDITION = {
4
+ 'CB' => 'cumulonimbus',
5
+ 'TCU' => 'towering cumulus',
6
+ '///' => nil, # cloud type unknown as observed by automatic system (15.9.1.7)
7
+ '' => nil,
8
+ }
9
+ CLEAR_SKIES = [
10
+ 'NSC', # WMO
11
+ 'NCD', # WMO
12
+ 'CLR',
13
+ 'SKC',
14
+ ]
15
+
16
+ def self.parse(raw)
17
+ case
18
+ when CLEAR_SKIES.include?(raw)
19
+ new(raw)
20
+ when raw =~ /^(BKN|FEW|OVC|SCT)(\d+|\/{3})(CB|TCU|\/{3}|)?$/
21
+ quantity = QUANTITY[$1]
22
+ height =
23
+ if $2 == '///'
24
+ nil
25
+ else
26
+ Metar::Data::Distance.new($2.to_i * 30.48)
27
+ end
28
+ type = CONDITION[$3]
29
+ new(raw, quantity: quantity, height: height, type: type)
30
+ when raw =~ /^(CB|TCU)$/
31
+ type = CONDITION[$1]
32
+ new(raw, type: type)
33
+ else
34
+ nil
35
+ end
36
+ end
37
+
38
+ attr_reader :quantity, :height, :type
39
+
40
+ def initialize(raw, quantity: nil, height: nil, type: nil)
41
+ @raw = raw
42
+ @quantity, @height, @type = quantity, height, type
43
+ end
44
+
45
+ def to_s
46
+ if @height.nil?
47
+ to_summary
48
+ else
49
+ to_summary + ' ' + I18n.t('metar.altitude.at') + ' ' + height.to_s
50
+ end
51
+ end
52
+
53
+ def to_summary
54
+ if @quantity == nil and @height == nil and @type == nil
55
+ I18n.t('metar.sky_conditions.clear skies')
56
+ else
57
+ type = @type ? ' ' + @type : ''
58
+ I18n.t("metar.sky_conditions.#{@quantity}#{type}")
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ require "i18n"
2
+ require "m9t"
3
+
4
+ # Adds a parse method to the M9t base class
5
+ class Metar::Data::Speed < M9t::Speed
6
+ METAR_UNITS = {
7
+ "" => :kilometers_per_hour,
8
+ "KMH" => :kilometers_per_hour,
9
+ "MPS" => :meters_per_second,
10
+ "KT" => :knots,
11
+ }
12
+
13
+ def self.parse(raw)
14
+ case
15
+ when raw =~ /^(\d+)(|KT|MPS|KMH)$/
16
+ # Call the appropriate factory method for the supplied units
17
+ send(METAR_UNITS[$2], $1.to_i)
18
+ else
19
+ nil
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ class Metar::Data::StationCode < Metar::Data::Base
2
+ def self.parse(raw)
3
+ if raw =~ /^[A-Z][A-Z0-9]{3}$/
4
+ new(raw)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ require "i18n"
2
+ require "m9t"
3
+
4
+ # Adds a parse method to the M9t base class
5
+ class Metar::Data::Temperature < M9t::Temperature
6
+ def self.parse(raw)
7
+ if raw =~ /^(M?)(\d+)$/
8
+ sign = $1
9
+ value = $2.to_i
10
+ value *= -1 if sign == 'M'
11
+ new(value)
12
+ else
13
+ nil
14
+ end
15
+ end
16
+
17
+ def to_s(options = {})
18
+ options = {abbreviated: true, precision: 0}.merge(options)
19
+ super(options)
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ class Metar::Data::TemperatureAndDewPoint < Metar::Data::Base
2
+ def self.parse(raw)
3
+ if raw =~ /^(M?\d+|XX|\/\/)\/(M?\d+|XX|\/\/)?$/
4
+ temperature = Metar::Data::Temperature.parse($1)
5
+ dew_point = Metar::Data::Temperature.parse($2)
6
+ new(raw, temperature: temperature, dew_point: dew_point)
7
+ end
8
+ end
9
+
10
+ attr_reader :temperature
11
+ attr_reader :dew_point
12
+
13
+ def initialize(raw, temperature:, dew_point:)
14
+ @raw = raw
15
+ @temperature = temperature
16
+ @dew_point = dew_point
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ class Metar::Data::Time < Metar::Data::Base
2
+ def self.parse(raw, year: nil, month: nil, strict: true)
3
+ year ||= DateTime.now.year
4
+ month ||= DateTime.now.month
5
+
6
+ date_matcher =
7
+ if strict
8
+ /^(\d{2})(\d{2})(\d{2})Z$/
9
+ else
10
+ /^(\d{1,2})(\d{2})(\d{2})Z$/
11
+ end
12
+
13
+ if raw =~ date_matcher
14
+ day, hour, minute = $1.to_i, $2.to_i, $3.to_i
15
+ else
16
+ return nil if strict
17
+
18
+ if raw =~ /^(\d{1,2})(\d{2})Z$/
19
+ # The day is missing, use today's date
20
+ day = Time.now.day
21
+ hour, minute = $1.to_i, $2.to_i
22
+ else
23
+ return nil
24
+ end
25
+ end
26
+
27
+ new(
28
+ raw,
29
+ strict: strict,
30
+ year: year, month: month, day: day, hour: hour, minute: minute
31
+ )
32
+ end
33
+
34
+ attr_reader :strict
35
+ attr_reader :year
36
+ attr_reader :month
37
+ attr_reader :day
38
+ attr_reader :hour
39
+ attr_reader :minute
40
+
41
+ def initialize(raw, strict:, year:, month:, day:, hour:, minute:)
42
+ @raw = raw
43
+ @strict = strict
44
+ @year = year
45
+ @month = month
46
+ @day = day
47
+ @hour = hour
48
+ @minute = minute
49
+ end
50
+
51
+ def value
52
+ Time.gm(year, month, day, hour, minute)
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ class Metar::Data::VariableWind < Metar::Data::Base
2
+ def self.parse(raw)
3
+ if raw =~ /^(\d+)V(\d+)$/
4
+ new(
5
+ raw,
6
+ direction1: Metar::Data::Direction.new($1),
7
+ direction2: Metar::Data::Direction.new($2)
8
+ )
9
+ else
10
+ nil
11
+ end
12
+ end
13
+
14
+ attr_reader :direction1
15
+ attr_reader :direction2
16
+
17
+ def initialize(raw, direction1:, direction2:)
18
+ @raw = raw
19
+ @direction1, @direction2 = direction1, direction2
20
+ end
21
+
22
+ def to_s
23
+ "#{direction1.to_s(units: :compass)} - #{direction2.to_s(units: :compass)}"
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ require "i18n"
2
+ require "m9t"
3
+
4
+ class Metar::Data::VerticalVisibility < Metar::Data::Base
5
+ def self.parse(raw)
6
+ case
7
+ when raw =~ /^VV(\d{3})$/
8
+ new(raw, distance: Metar::Data::Distance.new($1.to_f * 30.48))
9
+ when raw == '///'
10
+ new(raw, distance: Metar::Data::Distance.new)
11
+ else
12
+ nil
13
+ end
14
+ end
15
+
16
+ attr_reader :distance
17
+
18
+ def initialize(raw, distance:)
19
+ @raw = raw
20
+ @distance = distance
21
+ end
22
+
23
+ def value
24
+ distance.value
25
+ end
26
+ end
@@ -0,0 +1,71 @@
1
+ class Metar::Data::Visibility < Metar::Data::Base
2
+ def self.parse(raw)
3
+ case
4
+ when raw == '9999'
5
+ new(raw, distance: Metar::Data::Distance.new(10000), comparator: :more_than)
6
+ when raw =~ /(\d{4})NDV/ # WMO
7
+ new(raw, distance: Metar::Data::Distance.new($1.to_f)) # Assuming meters
8
+ when (raw =~ /^((1|2)\s|)([1357])\/([248]|16)SM$/) # US
9
+ miles = $1.to_f + $3.to_f / $4.to_f
10
+ distance = Metar::Data::Distance.miles(miles)
11
+ distance.units = :miles
12
+ new(raw, distance: distance)
13
+ when raw =~ /^(\d+)SM$/ # US
14
+ distance = Metar::Data::Distance.miles($1.to_f)
15
+ distance.units = :miles
16
+ new(raw, distance: distance)
17
+ when raw == 'M1/4SM' # US
18
+ distance = Metar::Data::Distance.miles(0.25)
19
+ distance.units = :miles
20
+ new(raw, distance: distance, comparator: :less_than)
21
+ when raw =~ /^(\d+)KM$/
22
+ new(raw, distance: Metar::Data::Distance.kilometers($1))
23
+ when raw =~ /^(\d+)$/ # We assume meters
24
+ new(raw, distance: Metar::Data::Distance.new($1))
25
+ when raw =~ /^(\d+)(N|NE|E|SE|S|SW|W|NW)$/
26
+ new(
27
+ raw,
28
+ distance: Metar::Data::Distance.meters($1),
29
+ direction: M9t::Direction.compass($2)
30
+ )
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ attr_reader :distance, :direction, :comparator
37
+
38
+ def initialize(raw, distance:, direction: nil, comparator: nil)
39
+ @raw = raw
40
+ @distance, @direction, @comparator = distance, direction, comparator
41
+ end
42
+
43
+ def to_s(options = {})
44
+ distance_options = {
45
+ abbreviated: true,
46
+ precision: 0,
47
+ units: :kilometers,
48
+ }.merge(options)
49
+ direction_options = {units: :compass}
50
+ case
51
+ when (@direction.nil? and @comparator.nil?)
52
+ @distance.to_s(distance_options)
53
+ when @comparator.nil?
54
+ [
55
+ @distance.to_s(distance_options),
56
+ @direction.to_s(direction_options),
57
+ ].join(' ')
58
+ when @direction.nil?
59
+ [
60
+ I18n.t('comparison.' + @comparator.to_s),
61
+ @distance.to_s(distance_options),
62
+ ].join(' ')
63
+ else
64
+ [
65
+ I18n.t('comparison.' + @comparator.to_s),
66
+ @distance.to_s(distance_options),
67
+ @direction.to_s(direction_options),
68
+ ].join(' ')
69
+ end
70
+ end
71
+ end