metar-parser 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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 }