metar-parser 1.1.0 → 1.1.1

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.
data/README.md CHANGED
@@ -67,6 +67,18 @@ raw = Metar::Raw::Data.new( metar_string )
67
67
  parser = Metar::Parser.new( raw )
68
68
  ```
69
69
 
70
+ Compliance
71
+ ==========
72
+
73
+ By default, the parser runs in 'loose' compliance mode. That means that it tries to
74
+ accept non-standard input.
75
+
76
+ If you only want to accept standards-compliant METAR strings, do this:
77
+
78
+ ```ruby
79
+ Metar::Parse.compliance = :strict
80
+ ```
81
+
70
82
  Changelog
71
83
  =========
72
84
 
data/lib/metar/data.rb CHANGED
@@ -467,5 +467,264 @@ module Metar
467
467
 
468
468
  end
469
469
 
470
+ class Remark
471
+
472
+ PRESSURE_CHANGE_CHARACTER = [
473
+ :increasing_then_decreasing, # 0
474
+ :increasing_then_steady, # 1
475
+ :increasing, # 2
476
+ :decreasing_or_steady_then_increasing, # 3
477
+ :steady, # 4
478
+ :decreasing_then_increasing, # 5
479
+ :decreasing_then_steady, # 6
480
+ :decreasing, # 7
481
+ :steady_then_decreasing, # 8
482
+ ]
483
+
484
+ INDICATOR_TYPE = {
485
+ 'TS' => :thunderstorm_information,
486
+ 'PWI' => :precipitation_identifier,
487
+ 'P' => :precipitation_amount,
488
+ }
489
+
490
+ COLOR_CODE = ['RED', 'AMB', 'YLO', 'GRN', 'WHT', 'BLU']
491
+
492
+ def self.parse(s)
493
+ case s
494
+ when /^([12])([01])(\d{3})$/
495
+ extreme = {'1' => :maximum, '2' => :minimum}[$1]
496
+ value = sign($2) * tenths($3)
497
+ TemperatureExtreme.new(extreme, value)
498
+ when /^4([01])(\d{3})([01])(\d{3})$/
499
+ [
500
+ TemperatureExtreme.new(:maximum, sign($1) * tenths($2)),
501
+ TemperatureExtreme.new(:minimum, sign($3) * tenths($4))
502
+ ]
503
+ when /^5([0-8])(\d{3})$/
504
+ character = PRESSURE_CHANGE_CHARACTER[$1.to_i]
505
+ PressureTendency.new(character, tenths($2))
506
+ when /^6(\d{4})$/
507
+ Precipitation.new(3, Distance.new(inches_to_meters($1))) # actually 3 or 6 depending on reporting time
508
+ when /^7(\d{4})$/
509
+ Precipitation.new(24, Distance.new(inches_to_meters($1)))
510
+ when /^A[0O]([12])$/
511
+ type = [:with_precipitation_discriminator, :without_precipitation_discriminator][$1.to_i - 1]
512
+ AutomatedStationType.new(type)
513
+ when /^P(\d{4})$/
514
+ Precipitation.new(1, Distance.new(inches_to_meters($1)))
515
+ when /^T([01])(\d{3})([01])(\d{3})$/
516
+ temperature = Temperature.new(sign($1) * tenths($2))
517
+ dew_point = Temperature.new(sign($3) * tenths($4))
518
+ HourlyTemperaturAndDewPoint.new(temperature, dew_point)
519
+ when /^SLP(\d{3})$/
520
+ SeaLevelPressure.new(Pressure.hectopascals(tenths($1)))
521
+ when /^(#{INDICATOR_TYPE.keys.join('|')})NO$/
522
+ type = INDICATOR_TYPE[$1]
523
+ SensorStatusIndicator.new(:type, :not_available)
524
+ when /^(#{COLOR_CODE.join('|')})$/
525
+ ColorCode.new($1)
526
+ when 'SKC'
527
+ SkyCondition.new
528
+ when '$'
529
+ MaintenanceNeeded.new
530
+ else
531
+ nil
532
+ end
533
+ end
534
+
535
+ def self.sign(digit)
536
+ case digit
537
+ when '0'
538
+ 1.0
539
+ when '1'
540
+ -1.0
541
+ else
542
+ raise "Unexpected sign: #{digit}"
543
+ end
544
+ end
545
+
546
+ def self.tenths(digits)
547
+ digits.to_f / 10.0
548
+ end
549
+
550
+ def self.inches_to_meters(digits)
551
+ digits.to_f * 0.000254
552
+ end
553
+
554
+ end
555
+
556
+ class TemperatureExtreme
557
+
558
+ attr_accessor :value
559
+ attr_accessor :extreme
560
+
561
+ def initialize(extreme, value)
562
+ @extreme, @value = extreme, value
563
+ end
564
+
565
+ end
566
+
567
+ class PressureTendency
568
+
569
+ attr_accessor :value
570
+ attr_accessor :character
571
+
572
+ def initialize(character, value)
573
+ @character, @value = character, value
574
+ end
575
+
576
+ end
577
+
578
+ class Precipitation
579
+
580
+ attr_accessor :period
581
+ attr_accessor :amount
582
+
583
+ def initialize(period, amount)
584
+ @period, @amount = period, amount
585
+ end
586
+
587
+ end
588
+
589
+ class AutomatedStationType
590
+
591
+ attr_accessor :type
592
+
593
+ def initialize(type)
594
+ @type = type
595
+ end
596
+
597
+ end
598
+
599
+ class HourlyTemperaturAndDewPoint
600
+
601
+ attr_accessor :temperature
602
+ attr_accessor :dew_point
603
+
604
+ def initialize(temperature, dew_point)
605
+ @temperature, @dew_point = temperature, dew_point
606
+ end
607
+
608
+ end
609
+
610
+ class SeaLevelPressure
611
+
612
+ attr_accessor :pressure
613
+
614
+ def initialize(pressure)
615
+ @pressure = pressure
616
+ end
617
+
618
+ end
619
+
620
+ class SensorStatusIndicator
621
+
622
+ attr_accessor :type
623
+ attr_accessor :state
624
+
625
+ def initialize(type, state)
626
+ @type, @state = type, state
627
+ end
628
+
629
+ end
630
+
631
+ class MaintenanceNeeded
632
+ end
633
+
634
+ class Lightning
635
+
636
+ TYPE = {'' => :default}
637
+
638
+ def self.parse_chunks(chunks)
639
+ ltg = chunks.shift
640
+ m = ltg.match(/^LTG(|CG|IC|CC|CA)$/)
641
+ raise 'first chunk is not lightning' if m.nil?
642
+ type = TYPE[m[1]]
643
+
644
+ frequency = nil
645
+ distance = nil
646
+ directions = []
647
+
648
+ if chunks[0] == 'DSNT'
649
+ distance = Distance.miles(10) # Should be >10SM, not 10SM
650
+ chunks.shift
651
+ end
652
+
653
+ loop do
654
+ if is_compass?(chunks[0])
655
+ directions << chunks.shift
656
+ elsif chunks[0] == 'ALQDS'
657
+ directions += ['N', 'E', 'S', 'W']
658
+ chunks.shift
659
+ elsif chunks[0] =~ /^([NESW]{1,2})-([NESW]{1,2})$/
660
+ if is_compass?($1) and is_compass?($2)
661
+ directions += [$1, $2]
662
+ chunks.shift
663
+ else
664
+ break
665
+ end
666
+ elsif chunks[0] == 'AND'
667
+ chunks.shift
668
+ else
669
+ break
670
+ end
671
+ end
672
+
673
+ new(frequency, type, distance, directions)
674
+ end
675
+
676
+ def self.is_compass?(s)
677
+ s =~ /^([NESW]|NE|SE|SW|NW)$/
678
+ end
679
+
680
+ attr_accessor :frequency
681
+ attr_accessor :type
682
+ attr_accessor :distance
683
+ attr_accessor :directions
684
+
685
+ def initialize(frequency, type, distance, directions)
686
+ @frequency, @type, @distance, @directions = frequency, type, distance, directions
687
+ end
688
+
689
+ end
690
+
691
+ class VisibilityRemark < Visibility
692
+
693
+ def self.parse(chunk)
694
+ chunk =~ /^(\d{4})([NESW]?)$/
695
+ distance = Distance.new($1)
696
+
697
+ new(distance, $2, :more_than)
698
+ end
699
+
700
+ end
701
+
702
+ class DensityAltitude
703
+
704
+ def self.parse(chunk)
705
+ chunk =~ /^(\d+)(FT)$/
706
+ height = Distance.feet($1)
707
+
708
+ new(height)
709
+ end
710
+
711
+ attr_accessor :height
712
+
713
+ def initialize(height)
714
+ @height = height
715
+ end
716
+
717
+ end
718
+
719
+ class ColorCode
720
+
721
+ attr_accessor :code
722
+
723
+ def initialize(code)
724
+ @code = code
725
+ end
726
+
727
+ end
728
+
470
729
  end
471
730
 
data/lib/metar/parser.rb CHANGED
@@ -9,11 +9,26 @@ module Metar
9
9
  new(raw)
10
10
  end
11
11
 
12
+ COMPLIANCE = [:strict, :loose]
13
+
14
+ def self.thread_attributes
15
+ Thread.current[:metar_parser] ||= {}
16
+ end
17
+
18
+ def self.compliance
19
+ thread_attributes[:compliance] ||= :loose
20
+ end
21
+
22
+ def self.compliance=(compliance)
23
+ raise 'Unknown compliance' unless COMPLIANCE.find(compliance)
24
+ thread_attributes[:compliance] = compliance
25
+ end
26
+
12
27
  attr_reader :raw, :metar
13
28
  attr_reader :station_code, :observer, :wind, :variable_wind, :visibility,
14
29
  :minimum_visibility, :runway_visible_range, :present_weather, :sky_conditions,
15
30
  :vertical_visibility, :temperature, :dew_point, :sea_level_pressure,
16
- :recent_weather, :remarks
31
+ :recent_weather, :unparsed, :remarks
17
32
 
18
33
  def initialize(raw)
19
34
  @raw = raw
@@ -44,6 +59,7 @@ module Metar
44
59
  @dew_point = nil
45
60
  @sea_level_pressure = nil
46
61
  @recent_weather = []
62
+ @unparsed = []
47
63
  @remarks = []
48
64
 
49
65
  seek_location
@@ -63,6 +79,7 @@ module Metar
63
79
  seek_temperature_dew_point
64
80
  seek_sea_level_pressure
65
81
  seek_recent_weather
82
+ seek_to_remarks
66
83
  seek_remarks
67
84
  end
68
85
 
@@ -75,12 +92,30 @@ module Metar
75
92
  end
76
93
 
77
94
  def seek_datetime
78
- case
79
- when @chunks[0] =~ /^(\d{2})(\d{2})(\d{2})Z$/
80
- @chunks.shift
95
+ found = false
96
+ date_matcher =
97
+ if strict?
98
+ /^(\d{2})(\d{2})(\d{2})Z$/
99
+ else
100
+ /^(\d{1,2})(\d{2})(\d{2})Z$/
101
+ end
102
+ if @chunks[0] =~ date_matcher
81
103
  @day, @hour, @minute = $1.to_i, $2.to_i, $3.to_i
104
+ found = true
105
+ else
106
+ if not strict?
107
+ if @chunks[0] =~ /^(\d{1,2})(\d{2})Z$/
108
+ # The day is missing, use today's date
109
+ @day = Time.now.day
110
+ @hour, @minute = $1.to_i, $2.to_i, $3.to_i
111
+ found = true
112
+ end
113
+ end
114
+ end
115
+ if found
116
+ @chunks.shift
82
117
  else
83
- raise ParseError.new("Expecting datetime, found '#{ @chunks[0] }' in #{@metar}")
118
+ raise ParseError.new("Expecting datetime, found '#{@chunks[0]}' in #{@metar}")
84
119
  end
85
120
  end
86
121
 
@@ -238,19 +273,60 @@ module Metar
238
273
  m = /^RE/.match(@chunks[0])
239
274
  break if m.nil?
240
275
  recent_weather = Metar::WeatherPhenomenon.parse(m.post_match)
241
- if recent_weather
242
- @chunks.shift
243
- @recent_weather << recent_weather
276
+ break if recent_weather.nil?
277
+ @chunks.shift
278
+ @recent_weather << recent_weather
279
+ end
280
+ end
281
+
282
+ def seek_to_remarks
283
+ if strict?
284
+ if @chunks.size > 0 and @chunks[0] != 'RMK'
285
+ raise ParseError.new("Unparsable text found: '#{@chunks.join(' ')}'")
286
+ end
287
+ else
288
+ while @chunks.size > 0 and @chunks[0] != 'RMK' do
289
+ @unparsed << @chunks.shift
244
290
  end
245
291
  end
246
292
  end
247
293
 
248
294
  def seek_remarks
249
- if @chunks[0] == 'RMK'
250
- @chunks.shift
295
+ return if @chunks.size == 0
296
+ raise 'seek_remarks calls without remark' if @chunks[0] != 'RMK'
297
+
298
+ @chunks.shift # Drop 'RMK'
299
+ @remarks = []
300
+ loop do
301
+ break if @chunks.size == 0
302
+ r = Metar::Remark.parse(@chunks[0])
303
+ if r
304
+ @remarks += [*r]
305
+ @chunks.shift
306
+ next
307
+ end
308
+ if @chunks[0] == 'VIS' and @chunks.size >= 3 and @chunks[1] == 'MIN'
309
+ @chunks.shift(3)
310
+ r = Metar::VisibilityRemark.parse(@chunks[2])
311
+ @remarks << r
312
+ end
313
+ if @chunks[0] == 'DENSITY' and @chunks.size >= 3 and @chunks[1] == 'ALT'
314
+ @chunks.shift(3)
315
+ r = Metar::DensityAltitude.parse(@chunks[2])
316
+ @remarks << r
317
+ end
318
+ case
319
+ when @chunks[0] =~ /^LTG(|CG|IC|CC|CA)$/
320
+ r = Metar::Lightning.parse_chunks(@chunks)
321
+ @remarks << r
322
+ else
323
+ @remarks << @chunks.shift
324
+ end
251
325
  end
252
- @remarks += @chunks.clone
253
- @chunks = []
326
+ end
327
+
328
+ def strict?
329
+ self.class.compliance == :strict
254
330
  end
255
331
 
256
332
  end
data/lib/metar/station.rb CHANGED
@@ -14,22 +14,6 @@ module Metar
14
14
 
15
15
  @nsd_cccc = nil # Contains the text of the station list
16
16
 
17
- def download_local
18
- nsd_cccc = Metar::Station.download_stations
19
- File.open(Metar::Station.local_nsd_path, 'w') do |fil|
20
- fil.write nsd_cccc
21
- end
22
- end
23
-
24
- # Load local copy of the station list
25
- # and download it first if missing
26
- def load_local
27
- download_local if not File.exist?(Metar::Station.local_nsd_path)
28
- @nsd_cccc = File.open(Metar::Station.local_nsd_path) do |fil|
29
- fil.read
30
- end
31
- end
32
-
33
17
  def countries
34
18
  all_structures.reduce( Set.new ) { |a, s| a.add( s[ :country ] ) }.to_a.sort
35
19
  end
@@ -96,11 +80,6 @@ module Metar
96
80
  open(NOAA_STATION_LIST_URL) { |fil| fil.read }
97
81
  end
98
82
 
99
- # Path for saving a local copy of the nsc_cccc station list
100
- def local_nsd_path
101
- File.join(File.expand_path(File.dirname(__FILE__) + '/../../'), 'data', 'nsd_cccc.txt')
102
- end
103
-
104
83
  def all_structures
105
84
  return @structures if @structures
106
85
 
data/lib/metar/version.rb CHANGED
@@ -3,7 +3,7 @@ module Metar
3
3
  module VERSION #:nodoc:
4
4
  MAJOR = 1
5
5
  MINOR = 1
6
- TINY = 0
6
+ TINY = 1
7
7
 
8
8
  STRING = [ MAJOR, MINOR, TINY ].join( '.' )
9
9
  end
data/locales/en.yml CHANGED
@@ -138,6 +138,7 @@ en:
138
138
  light dust storm: light dust storm
139
139
  nearby dust storm: nearby dust storm
140
140
  thunderstorm: thunderstorm
141
+ thunderstorm and rain: thunderstorm and rain
141
142
  nearby thunderstorm: nearby thunderstorm
142
143
  heavy thunderstorm and hail: heavy thunderstorm and hail
143
144
  light thunderstorm and hail: light thunderstorm and hail
data/locales/it.yml CHANGED
@@ -138,6 +138,7 @@ it:
138
138
  light dust storm: tempesta di polvere leggera
139
139
  nearby dust storm: tempesta di polvere vicina
140
140
  thunderstorm: temporale
141
+ thunderstorm and rain: temporale con pioggia
141
142
  nearby thunderstorm: temporale vicino
142
143
  heavy thunderstorm and hail: temporale con grandine intenso
143
144
  light thunderstorm and hail: temporale con grandine leggero
data/spec/spec_helper.rb CHANGED
@@ -12,3 +12,17 @@ end
12
12
 
13
13
  require File.expand_path( File.dirname(__FILE__) + '/../lib/metar' )
14
14
 
15
+ RSpec::Matchers.define :be_temperature_extreme do |extreme, value|
16
+ match do |remark|
17
+ if not remark.is_a?(Metar::TemperatureExtreme)
18
+ false
19
+ elsif remark.extreme != extreme
20
+ false
21
+ elsif remark.value != value
22
+ false
23
+ else
24
+ true
25
+ end
26
+ end
27
+ end
28
+
@@ -3,6 +3,10 @@ load File.expand_path( '../spec_helper.rb', File.dirname(__FILE__) )
3
3
 
4
4
  describe Metar::Parser do
5
5
 
6
+ after :each do
7
+ Metar::Parser.compliance = :loose
8
+ end
9
+
6
10
  context '.for_cccc' do
7
11
 
8
12
  it 'returns a loaded parser' do
@@ -33,16 +37,50 @@ describe Metar::Parser do
33
37
  end. to raise_error( Metar::ParseError, /Expecting location/ )
34
38
  end
35
39
 
36
- it '.time missing' do
37
- expect do
38
- setup_parser("PAIL 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 RMK AO2 P0000")
39
- end. to raise_error( Metar::ParseError, /Expecting datetime/ )
40
- end
40
+ context 'datetime' do
41
41
 
42
- it 'time' do
43
- parser = setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 RMK AO2 P0000")
42
+ it 'is parsed' do
43
+ parser = setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 RMK AO2 P0000")
44
+
45
+ parser.time. should == Time.gm(2011, 05, 06, 16, 10)
46
+ end
47
+
48
+ it 'throws an error is missing' do
49
+ expect do
50
+ setup_parser("PAIL 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 RMK AO2 P0000")
51
+ end. to raise_error( Metar::ParseError, /Expecting datetime/ )
52
+ end
53
+
54
+ context 'in strict mode' do
55
+
56
+ before :each do
57
+ Metar::Parser.compliance = :strict
58
+ end
59
+
60
+ it 'less than 6 numerals fails' do
61
+ expect do
62
+ parser = setup_parser('MMCE 21645Z 12010KT 8SM SKC 29/26 A2992 RMK')
63
+ end. to raise_error(Metar::ParseError, /Expecting datetime/)
64
+ end
65
+
66
+ end
67
+
68
+ context 'in loose mode' do
69
+
70
+ it '5 numerals parses' do
71
+ parser = setup_parser('MMCE 21645Z 12010KT 8SM SKC 29/26 A2992 RMK')
72
+
73
+ parser.time. should == Time.gm(2011, 05, 02, 16, 45)
74
+ end
75
+
76
+ it "with 4 numerals parses, takes today's day" do
77
+ parser = setup_parser('HKML 1600Z 19010KT 9999 FEW022 25/22 Q1015')
78
+
79
+ parser.time. should == Time.gm(2011, 05, 06, 16, 00)
80
+ end
81
+
82
+ end
44
83
 
45
- parser.time. should == Time.gm(2011, 05, 06, 16, 10)
46
84
  end
47
85
 
48
86
  context '.observer' do
@@ -249,20 +287,54 @@ describe Metar::Parser do
249
287
  should == 'thunderstorm'
250
288
  end
251
289
 
252
- it 'remarks' do
253
- parser = setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 RMK AO2 P0000")
290
+ context 'remarks' do
254
291
 
255
- parser.remarks. should be_a Array
256
- parser.remarks.length. should == 2
257
- parser.remarks[0]. should == 'AO2'
258
- parser.remarks[1]. should == 'P0000'
259
- end
292
+ it 'are collected' do
293
+ parser = setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 RMK AO2 P0000")
294
+
295
+ parser.remarks. should be_a Array
296
+ parser.remarks.size. should == 2
297
+ end
260
298
 
261
- it 'remarks_defaults_to_empty_array' do
262
- parser = setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910")
299
+ it 'remarks defaults to empty array' do
300
+ parser = setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910")
301
+
302
+ parser.remarks. should be_a Array
303
+ parser.remarks.length. should == 0
304
+ end
305
+
306
+ it 'parses known remarks' do
307
+ parser = setup_parser('CYZT 052200Z 31010KT 20SM SKC 17/12 A3005 RMK SLP174 20046')
308
+
309
+ parser.remarks[0]. should be_a(Metar::SeaLevelPressure)
310
+ parser.remarks[1]. should be_temperature_extreme(:minimum, 4.6)
311
+ end
312
+
313
+ context 'in strict mode' do
314
+
315
+ before :each do
316
+ Metar::Parser.compliance = :strict
317
+ end
318
+
319
+ it 'unparsed data causes an error' do
320
+ expect do
321
+ setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 FOO RMK AO2 P0000")
322
+ end. to raise_error(Metar::ParseError, /Unparsable text found/)
323
+ end
324
+
325
+ end
326
+
327
+ context 'in loose mode' do
328
+
329
+ it 'unparsed data is collected' do
330
+ parser = setup_parser("PAIL 061610Z 24006KT 1 3/4SM -SN BKN016 OVC030 M17/M20 A2910 FOO RMK AO2 P0000")
331
+
332
+ parser.unparsed. should == ['FOO']
333
+ parser.remarks.size. should == 2
334
+ end
335
+
336
+ end
263
337
 
264
- parser.remarks. should be_a Array
265
- parser.remarks.length. should == 0
266
338
  end
267
339
 
268
340
  def setup_parser(metar)
@@ -0,0 +1,173 @@
1
+ # encoding: utf-8
2
+ load File.expand_path( '../spec_helper.rb', File.dirname(__FILE__) )
3
+
4
+ describe Metar::Remark do
5
+
6
+ context '.parse' do
7
+
8
+ it 'should delegate to subclasses' do
9
+ Metar::Remark.parse('21012'). should be_a(Metar::TemperatureExtreme)
10
+ end
11
+
12
+ it 'should return nil for unrecognised' do
13
+ Metar::Remark.parse('FOO'). should be_nil
14
+ end
15
+
16
+ context '6-hour maximum or minimum' do
17
+
18
+ [
19
+ ['positive maximum', '10046', [:maximum, 4.6]],
20
+ ['negative maximum', '11012', [:maximum, -1.2]],
21
+ ['positive minimum', '20046', [:minimum, 4.6]],
22
+ ['negative minimum', '21012', [:minimum, -1.2]],
23
+ ].each do |docstring, raw, expected|
24
+ example docstring do
25
+ Metar::Remark.parse(raw).
26
+ should be_temperature_extreme(*expected)
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ context '24-hour maximum and minimum' do
33
+
34
+ it 'returns minimum and maximum' do
35
+ max, min = Metar::Remark.parse('400461006')
36
+
37
+ max. should be_temperature_extreme(:maximum, 4.6)
38
+ min. should be_temperature_extreme(:minimum, -0.6)
39
+ end
40
+
41
+ end
42
+
43
+ context 'pressure tendency' do
44
+
45
+ it 'steady_then_decreasing' do
46
+ pt = Metar::Remark.parse('58033')
47
+
48
+ pt. should be_a(Metar::PressureTendency)
49
+ pt.character. should == :steady_then_decreasing
50
+ pt.value. should == 3.3
51
+ end
52
+
53
+ end
54
+
55
+ context '3-hour and 6-hour precipitation' do
56
+
57
+ it '60009' do
58
+ pr = Metar::Remark.parse('60009')
59
+
60
+ pr. should be_a(Metar::Precipitation)
61
+ pr.period. should == 3
62
+ pr.amount.value. should == 0.002286
63
+ end
64
+
65
+ end
66
+
67
+ context '24-hour precipitation' do
68
+
69
+ it '70015' do
70
+ pr = Metar::Remark.parse('70015')
71
+
72
+ pr. should be_a(Metar::Precipitation)
73
+ pr.period. should == 24
74
+ pr.amount.value. should == 0.003810
75
+ end
76
+
77
+ end
78
+
79
+ context 'automated station' do
80
+
81
+ [
82
+ ['with precipitation dicriminator', 'AO1', [Metar::AutomatedStationType, :with_precipitation_discriminator]],
83
+ ['without precipitation dicriminator', 'AO2', [Metar::AutomatedStationType, :without_precipitation_discriminator]],
84
+ ].each do |docstring, raw, expected|
85
+ example docstring do
86
+ aut = Metar::Remark.parse(raw)
87
+
88
+ aut. should be_a(expected[0])
89
+ aut.type. should == expected[1]
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ context 'sea-level pressure' do
96
+
97
+ it 'SLP125' do
98
+ slp = Metar::Remark.parse('SLP125')
99
+
100
+ slp. should be_a(Metar::SeaLevelPressure)
101
+ slp.pressure.value. should == 0.0125
102
+ end
103
+
104
+ end
105
+
106
+ context 'hourly temperature and dew point' do
107
+
108
+ it 'T00640036' do
109
+ htm = Metar::Remark.parse('T00641036')
110
+
111
+ htm. should be_a(Metar::HourlyTemperaturAndDewPoint)
112
+ htm.temperature.value. should == 6.4
113
+ htm.dew_point.value. should == -3.6
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+
122
+ describe Metar::Lightning do
123
+
124
+ context '.parse_chunks' do
125
+
126
+ [
127
+ ['direction', 'LTG SE', [:default, nil, ['SE']]],
128
+ ['distance direction', 'LTG DSNT SE', [:default, 16093.44, ['SE']]],
129
+ ['distance direction and direction', 'LTG DSNT NE AND W', [:default, 16093.44, ['NE', 'W']]],
130
+ ['distance direction-direction', 'LTG DSNT SE-SW', [:default, 16093.44, ['SE', 'SW']]],
131
+ ['distance all quandrants', 'LTG DSNT ALQDS', [:default, 16093.44, ['N', 'E', 'S', 'W']]],
132
+ ].each do |docstring, section, expected|
133
+ example docstring do
134
+ chunks = section.split(' ')
135
+ r = Metar::Lightning.parse_chunks(chunks)
136
+
137
+ r. should be_a(Metar::Lightning)
138
+ r.type. should == expected[0]
139
+ if expected[1]
140
+ r.distance.value. should == expected[1]
141
+ else
142
+ r.distance. should be_nil
143
+ end
144
+ r.directions. should == expected[2]
145
+ end
146
+ end
147
+
148
+ it 'should remove parsed chunks' do
149
+ chunks = ['LTG', 'DSNT', 'SE', 'FOO']
150
+
151
+ r = Metar::Lightning.parse_chunks(chunks)
152
+
153
+ chunks. should == ['FOO']
154
+ end
155
+
156
+ it 'should fail if the first chunk is not LTGnnn' do
157
+ expect do
158
+ Metar::Lightning.parse_chunks(['FOO'])
159
+ end. to raise_error(RuntimeError, /not lightning/)
160
+ end
161
+
162
+ it 'should not fail if all chunks are parsed' do
163
+ chunks = ['LTG', 'DSNT', 'SE']
164
+
165
+ r = Metar::Lightning.parse_chunks(chunks)
166
+
167
+ chunks. should == []
168
+ end
169
+
170
+ end
171
+
172
+ end
173
+
@@ -21,74 +21,10 @@ describe Metar::Station do
21
21
  @file = stub( 'file' )
22
22
  end
23
23
 
24
- context '.download_local' do
25
-
26
- it 'downloads the station list' do
27
- File.stub!( :open => @file )
28
-
29
- Metar::Station. should_receive( :open ).
30
- with( /^http/ )
31
-
32
- Metar::Station.download_local
33
- end
34
-
35
- it 'saves the list to disk' do
36
- Metar::Station.stub!( :open ).and_return( 'aaa' )
37
-
38
- File. should_receive( :open ) do | path, mode, &block |
39
- path. should =~ %r(data/nsd_cccc.txt$)
40
- mode. should == 'w'
41
- block.call @file
42
- end
43
- @file. should_receive( :write ).
44
- with( 'aaa' )
45
-
46
- Metar::Station.download_local
47
- end
48
-
49
- end
50
-
51
- context '.load_local' do
52
-
53
- it 'downloads the station list, if missing, then loads it' do
54
- File. should_receive( :exist? ).
55
- and_return( false )
56
- # open-uri call
57
- Metar::Station. should_receive( :open ).
58
- and_return( 'aaa' )
59
-
60
- outfile = stub( 'outfile' )
61
- File. should_receive( :open ).once.with( %r(nsd_cccc.txt), 'w' ) do | *args, &block |
62
- block.call outfile
63
- end
64
- outfile. should_receive( :write ).
65
- with( 'aaa' )
66
-
67
- infile = stub( 'infile' )
68
- File. should_receive( :open ).once.with( %r(nsd_cccc.txt) ) do | *args, &block |
69
- block.call infile
70
- end
71
- infile. should_receive( :read ).
72
- and_return( 'bbb' )
73
-
74
- Metar::Station.load_local. should == 'bbb'
75
- end
76
-
77
- it 'loads the file, if already present' do
78
- File. should_receive( :exist? ).
79
- and_return( true )
80
-
81
- File. should_receive( :open ).once.with( %r(nsd_cccc.txt) )
82
-
83
- Metar::Station.load_local
84
- end
85
-
86
- end
87
-
88
24
  context 'using structures' do
89
25
 
90
26
  before :each do
91
- preload_data
27
+ Metar::Station.stub!(:open).with(Metar::Station::NOAA_STATION_LIST_URL).and_return(nsd_file)
92
28
  end
93
29
 
94
30
  context '.countries' do
@@ -143,12 +79,6 @@ EOT
143
79
  StringIO.new( nsd_text )
144
80
  end
145
81
 
146
- def preload_data
147
- File.stub!( :exist? ).and_return( true )
148
- File.stub!( :open ).with( %r(nsd_cccc.txt) ).and_return( nsd_file )
149
- Metar::Station.load_local
150
- end
151
-
152
82
  end
153
83
 
154
84
  context '.to_longitude' do
@@ -20,12 +20,11 @@ RSpec::Matchers.define :be_weather_phenomenon do | modifier, descriptor, phenome
20
20
  end
21
21
 
22
22
  describe Metar::WeatherPhenomenon do
23
-
24
23
  context '.parse' do
25
-
26
24
  [
27
25
  [ 'simple phenomenon', 'BR', [ nil, nil, 'mist' ] ],
28
26
  [ 'descriptor + phenomenon', 'BCFG', [ nil, 'patches of', 'fog' ] ],
27
+ [ 'thunderstorm and rain', 'TSRA', [ nil, 'thunderstorm and', 'rain' ] ],
29
28
  [ 'intensity + phenomenon', '+RA', [ 'heavy', nil, 'rain' ] ],
30
29
  [ 'intensity + proximity + phenomenon', '-VCTSRA', [ 'nearby light', 'thunderstorm and', 'rain' ] ],
31
30
  [ '2 phenomena: SN RA', 'SNRA', [ nil, nil, 'snow and rain' ] ],
@@ -37,11 +36,9 @@ describe Metar::WeatherPhenomenon do
37
36
  Metar::WeatherPhenomenon.parse( raw ).should be_weather_phenomenon( *expected )
38
37
  end
39
38
  end
40
-
41
39
  end
42
40
 
43
41
  context '#to_s' do
44
-
45
42
  before :all do
46
43
  @locale = I18n.locale
47
44
  I18n.locale = :it
@@ -52,10 +49,11 @@ describe Metar::WeatherPhenomenon do
52
49
  end
53
50
 
54
51
  [
55
- [ 'simple phenomenon', :en, [ nil, nil, 'mist' ], 'mist' ],
56
- [ 'simple phenomenon', :it, [ nil, nil, 'mist' ], 'foschia' ],
57
- [ 'descriptor + phenomenon', :en, [ nil, 'patches of', 'fog' ], 'patches of fog' ],
58
- [ 'modifier + phenomenon', :en, ['heavy', nil, 'drizzle' ], 'heavy drizzle' ],
52
+ [ 'simple phenomenon', :en, [ nil, nil, 'mist' ], 'mist' ],
53
+ [ 'simple phenomenon', :it, [ nil, nil, 'mist' ], 'foschia' ],
54
+ [ 'descriptor + phenomenon', :en, [ nil, 'patches of', 'fog' ], 'patches of fog' ],
55
+ [ 'thunderstorm and rain', :en, [ nil, 'thunderstorm and', 'rain' ], 'thunderstorm and rain' ],
56
+ [ 'modifier + phenomenon', :en, ['heavy', nil, 'drizzle' ], 'heavy drizzle' ],
59
57
  [ 'modifier + descriptor + phenomenon', :en, ['heavy', 'freezing', 'drizzle' ], 'heavy freezing drizzle' ],
60
58
  ].each do | docstring, locale, ( modifier, descriptor, phenomenon ), expected |
61
59
  example docstring + " (#{locale})" do
@@ -64,8 +62,6 @@ describe Metar::WeatherPhenomenon do
64
62
  should == expected
65
63
  end
66
64
  end
67
-
68
65
  end
69
-
70
66
  end
71
67
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metar-parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-02 00:00:00.000000000 Z
12
+ date: 2012-10-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -82,7 +82,7 @@ dependencies:
82
82
  requirements:
83
83
  - - ! '>='
84
84
  - !ruby/object:Gem::Version
85
- version: 2.3.0
85
+ version: 2.11.0
86
86
  type: :development
87
87
  prerelease: false
88
88
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  requirements:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
- version: 2.3.0
93
+ version: 2.11.0
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: simplecov
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -151,33 +151,33 @@ files:
151
151
  - README.md
152
152
  - COPYING
153
153
  - Rakefile
154
- - bin/download_raw.rb
155
154
  - bin/parse_raw.rb
156
- - lib/metar.rb
157
- - lib/metar/data.rb
158
- - lib/metar/station.rb
159
155
  - lib/metar/parser.rb
160
156
  - lib/metar/report.rb
161
- - lib/metar/version.rb
162
157
  - lib/metar/raw.rb
163
- - spec/unit/runway_visible_range_spec.rb
164
- - spec/unit/vertical_visibility_spec.rb
158
+ - lib/metar/station.rb
159
+ - lib/metar/data.rb
160
+ - lib/metar/version.rb
161
+ - lib/metar.rb
162
+ - spec/spec_helper.rb
165
163
  - spec/unit/report_spec.rb
166
- - spec/unit/temperature_spec.rb
167
- - spec/unit/sky_condition_spec.rb
164
+ - spec/unit/pressure_spec.rb
165
+ - spec/unit/station_spec.rb
168
166
  - spec/unit/visibility_spec.rb
167
+ - spec/unit/sky_condition_spec.rb
168
+ - spec/unit/speed_spec.rb
169
+ - spec/unit/wind_spec.rb
170
+ - spec/unit/distance_spec.rb
171
+ - spec/unit/temperature_spec.rb
172
+ - spec/unit/vertical_visibility_spec.rb
173
+ - spec/unit/remark_spec.rb
174
+ - spec/unit/runway_visible_range_spec.rb
169
175
  - spec/unit/weather_phenomenon_spec.rb
170
- - spec/unit/station_spec.rb
176
+ - spec/unit/raw_spec.rb
171
177
  - spec/unit/parser_spec.rb
172
- - spec/unit/pressure_spec.rb
173
178
  - spec/unit/variable_wind_spec.rb
174
- - spec/unit/wind_spec.rb
175
- - spec/unit/raw_spec.rb
176
- - spec/unit/speed_spec.rb
177
- - spec/unit/distance_spec.rb
178
- - spec/spec_helper.rb
179
- - locales/it.yml
180
179
  - locales/en.yml
180
+ - locales/it.yml
181
181
  homepage: http://github.com/joeyates/metar-parser
182
182
  licenses: []
183
183
  post_install_message:
@@ -192,7 +192,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
192
192
  version: '0'
193
193
  segments:
194
194
  - 0
195
- hash: -116852050728515412
195
+ hash: -171469053592145990
196
196
  required_rubygems_version: !ruby/object:Gem::Requirement
197
197
  none: false
198
198
  requirements:
@@ -201,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
201
  version: '0'
202
202
  segments:
203
203
  - 0
204
- hash: -116852050728515412
204
+ hash: -171469053592145990
205
205
  requirements: []
206
206
  rubyforge_project: nowarning
207
207
  rubygems_version: 1.8.23
@@ -209,19 +209,20 @@ signing_key:
209
209
  specification_version: 3
210
210
  summary: A Ruby gem for worldwide weather reports
211
211
  test_files:
212
- - spec/unit/runway_visible_range_spec.rb
213
- - spec/unit/vertical_visibility_spec.rb
214
212
  - spec/unit/report_spec.rb
215
- - spec/unit/temperature_spec.rb
216
- - spec/unit/sky_condition_spec.rb
213
+ - spec/unit/pressure_spec.rb
214
+ - spec/unit/station_spec.rb
217
215
  - spec/unit/visibility_spec.rb
216
+ - spec/unit/sky_condition_spec.rb
217
+ - spec/unit/speed_spec.rb
218
+ - spec/unit/wind_spec.rb
219
+ - spec/unit/distance_spec.rb
220
+ - spec/unit/temperature_spec.rb
221
+ - spec/unit/vertical_visibility_spec.rb
222
+ - spec/unit/remark_spec.rb
223
+ - spec/unit/runway_visible_range_spec.rb
218
224
  - spec/unit/weather_phenomenon_spec.rb
219
- - spec/unit/station_spec.rb
225
+ - spec/unit/raw_spec.rb
220
226
  - spec/unit/parser_spec.rb
221
- - spec/unit/pressure_spec.rb
222
227
  - spec/unit/variable_wind_spec.rb
223
- - spec/unit/wind_spec.rb
224
- - spec/unit/raw_spec.rb
225
- - spec/unit/speed_spec.rb
226
- - spec/unit/distance_spec.rb
227
228
  has_rdoc:
data/bin/download_raw.rb DELETED
@@ -1,55 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- =begin
4
-
5
- This script downloads the current weather report for each station.
6
-
7
- =end
8
-
9
- require 'rubygems' if RUBY_VERSION < '1.9'
10
- require 'yaml'
11
- require File.join(File.expand_path(File.dirname(__FILE__) + '/../lib'), 'metar')
12
-
13
- Metar::Station.load_local
14
-
15
- ('A'..'Z').each do |initial|
16
-
17
- stations = {}
18
-
19
- Metar::Station.all.each do |station|
20
-
21
- next if station.cccc[0, 1] < initial
22
- break if station.cccc[0, 1] > initial
23
-
24
- print station.cccc
25
- raw = nil
26
- begin
27
- raw = Metar::Raw.new(station.cccc)
28
- rescue Net::FTPPermError => e
29
- puts ": Not available - #{ e }"
30
- next
31
- rescue
32
- puts ": Other error - #{ e }"
33
- next
34
- end
35
-
36
- stations[station.cccc] = raw.raw.clone
37
- puts ': OK'
38
- end
39
-
40
- filename = File.join(File.expand_path(File.dirname(__FILE__) + '/../data'), "stations.#{ initial }.yml")
41
- File.open(filename, 'w') { |fil| fil.write stations.to_yaml }
42
-
43
- end
44
-
45
- # Merge into one file
46
- stations = {}
47
- ('A'..'Z').each do |initial|
48
- filename = File.join(File.expand_path(File.dirname(__FILE__) + '/../data'), "stations.#{ initial }.yml")
49
- next if not File.exist?(filename)
50
- h = YAML.load_file(filename)
51
- stations.merge!(h)
52
- end
53
-
54
- filename = File.join(File.expand_path(File.dirname(__FILE__) + '/../data'), "stations.yml")
55
- File.open(filename, 'w') { |fil| fil.write stations.to_yaml }