metar-parser 1.2.1 → 1.3.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.
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