]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - rbot/utils.rb
initial import of rbot
[user/henk/code/ruby/rbot.git] / rbot / utils.rb
1 require 'net/http'
2 require 'uri'
3
4 module Irc
5
6   # miscellaneous useful functions
7   module Utils
8     # read a time in string format, turn it into "seconds from now".
9     # example formats handled are "5 minutes", "2 days", "five hours",
10     # "11:30", "15:45:11", "one day", etc.
11     #
12     # Throws:: RunTimeError "invalid time string" on parse failure
13     def Utils.timestr_offset(timestr)
14       case timestr
15         when (/^(\S+)\s+(\S+)$/)
16           mult = $1
17           unit = $2
18           if(mult =~ /^([\d.]+)$/)
19             num = $1.to_f
20             raise "invalid time string" unless num
21           else
22             case mult
23               when(/^(one|an|a)$/)
24                 num = 1
25               when(/^two$/)
26                 num = 2
27               when(/^three$/)
28                 num = 3
29               when(/^four$/)
30                 num = 4
31               when(/^five$/)
32                 num = 5
33               when(/^six$/)
34                 num = 6
35               when(/^seven$/)
36                 num = 7
37               when(/^eight$/)
38                 num = 8
39               when(/^nine$/)
40                 num = 9
41               when(/^ten$/)
42                 num = 10
43               when(/^fifteen$/)
44                 num = 15
45               when(/^twenty$/)
46                 num = 20
47               when(/^thirty$/)
48                 num = 30
49               when(/^sixty$/)
50                 num = 60
51               else
52                 raise "invalid time string"
53             end
54           end
55           case unit
56             when (/^(s|sec(ond)?s?)$/)
57               return num
58             when (/^(m|min(ute)?s?)$/)
59               return num * 60
60             when (/^(h|h(ou)?rs?)$/)
61               return num * 60 * 60
62             when (/^(d|days?)$/)
63               return num * 60 * 60 * 24
64             else
65               raise "invalid time string"
66           end
67         when (/^(\d+):(\d+):(\d+)$/)
68           hour = $1.to_i
69           min = $2.to_i
70           sec = $3.to_i
71           now = Time.now
72           later = Time.mktime(now.year, now.month, now.day, hour, min, sec)
73           return later - now
74         when (/^(\d+):(\d+)$/)
75           hour = $1.to_i
76           min = $2.to_i
77           now = Time.now
78           later = Time.mktime(now.year, now.month, now.day, hour, min, now.sec)
79           return later - now
80         when (/^(\d+):(\d+)(am|pm)$/)
81           hour = $1.to_i
82           min = $2.to_i
83           ampm = $3
84           if ampm == "pm"
85             hour += 12
86           end
87           now = Time.now
88           later = Time.mktime(now.year, now.month, now.day, hour, min, now.sec)
89           return later - now
90         when (/^(\S+)$/)
91           num = 1
92           unit = $1
93           case unit
94             when (/^(s|sec(ond)?s?)$/)
95               return num
96             when (/^(m|min(ute)?s?)$/)
97               return num * 60
98             when (/^(h|h(ou)?rs?)$/)
99               return num * 60 * 60
100             when (/^(d|days?)$/)
101               return num * 60 * 60 * 24
102             else
103               raise "invalid time string"
104           end
105         else
106           raise "invalid time string"
107       end
108     end
109
110     # turn a number of seconds into a human readable string, e.g
111     # 2 days, 3 hours, 18 minutes, 10 seconds
112     def Utils.secs_to_string(secs)
113       ret = ""
114       days = (secs / (60 * 60 * 24)).to_i
115       secs = secs % (60 * 60 * 24)
116       hours = (secs / (60 * 60)).to_i
117       secs = (secs % (60 * 60))
118       mins = (secs / 60).to_i
119       secs = (secs % 60).to_i
120       ret += "#{days} days, " if days > 0
121       ret += "#{hours} hours, " if hours > 0 || days > 0
122       ret += "#{mins} minutes and " if mins > 0 || hours > 0 || days > 0
123       ret += "#{secs} seconds"
124       return ret
125     end
126
127     def Utils.safe_exec(command, *args)
128       IO.popen("-") {|p|
129         if(p)
130           return p.readlines.join("\n")
131         else
132           begin
133             $stderr = $stdout
134             exec(command, *args)
135           rescue Exception => e
136             puts "exec of #{command} led to exception: #{e}"
137             Kernel::exit! 0
138           end
139           puts "exec of #{command} failed"
140           Kernel::exit! 0
141         end
142       }
143     end
144
145     # returns a string containing the result of an HTTP GET on the uri
146     def Utils.http_get(uristr, readtimeout=8, opentimeout=4)
147
148       # ruby 1.7 or better needed for this (or 1.6 and debian unstable)
149       Net::HTTP.version_1_2
150       # (so we support the 1_1 api anyway, avoids problems)
151
152       uri = URI.parse uristr
153       query = uri.path
154       if uri.query
155         query += "?#{uri.query}"
156       end
157
158       proxy_host = nil
159       proxy_port = nil
160       if(ENV['http_proxy'] && proxy_uri = URI.parse(ENV['http_proxy']))
161         proxy_host = proxy_uri.host
162         proxy_port = proxy_uri.port
163       end
164
165       http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
166       http.open_timeout = opentimeout
167       http.read_timeout = readtimeout
168
169       http.start {|http|
170         begin
171           resp = http.get(query)
172           if resp.code == "200"
173             return resp.body
174           end
175         rescue => e
176           # cheesy for now
177           $stderr.puts "Utils.http_get exception: #{e}, while trying to get #{uristr}"
178           return nil
179         end
180       }
181     end
182
183     # This is nasty-ass. I hate writing parsers.
184     class Metar
185       attr_reader :decoded
186       attr_reader :input
187       attr_reader :date
188       attr_reader :nodata
189       def initialize(string)
190         str = nil
191         @nodata = false
192         string.each_line {|l|
193           if str == nil
194             # grab first line (date)
195             @date = l.chomp.strip
196             str = ""
197           else
198             if(str == "")
199               str = l.chomp.strip
200             else
201               str += " " + l.chomp.strip
202             end
203           end
204         }
205         if @date && @date =~ /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+)$/
206           # 2002/02/26 05:00
207           @date = Time.gm($1, $2, $3, $4, $5, 0)
208         else
209           @date = Time.now
210         end
211         @input = str.chomp
212         @cloud_layers = 0
213         @cloud_coverage = {
214           'SKC' => '0',
215           'CLR' => '0',
216           'VV'  => '8/8',
217           'FEW' => '1/8 - 2/8',
218           'SCT' => '3/8 - 4/8',
219           'BKN' => '5/8 - 7/8',
220           'OVC' => '8/8'
221         }
222         @wind_dir_texts = [
223           'North',
224           'North/Northeast',
225           'Northeast',
226           'East/Northeast',
227           'East',
228           'East/Southeast',
229           'Southeast',
230           'South/Southeast',
231           'South',
232           'South/Southwest',
233           'Southwest',
234           'West/Southwest',
235           'West',
236           'West/Northwest',
237           'Northwest',
238           'North/Northwest',
239           'North'
240         ]
241         @wind_dir_texts_short = [
242           'N',
243           'N/NE',
244           'NE',
245           'E/NE',
246           'E',
247           'E/SE',
248           'SE',
249           'S/SE',
250           'S',
251           'S/SW',
252           'SW',
253           'W/SW',
254           'W',
255           'W/NW',
256           'NW',
257           'N/NW',
258           'N'
259         ]
260         @weather_array = {
261           'MI' => 'Mild ',
262           'PR' => 'Partial ',
263           'BC' => 'Patches ',
264           'DR' => 'Low Drifting ',
265           'BL' => 'Blowing ',
266           'SH' => 'Shower(s) ',
267           'TS' => 'Thunderstorm ',
268           'FZ' => 'Freezing',
269           'DZ' => 'Drizzle ',
270           'RA' => 'Rain ',
271           'SN' => 'Snow ',
272           'SG' => 'Snow Grains ',
273           'IC' => 'Ice Crystals ',
274           'PE' => 'Ice Pellets ',
275           'GR' => 'Hail ',
276           'GS' => 'Small Hail and/or Snow Pellets ',
277           'UP' => 'Unknown ',
278           'BR' => 'Mist ',
279           'FG' => 'Fog ',
280           'FU' => 'Smoke ',
281           'VA' => 'Volcanic Ash ',
282           'DU' => 'Widespread Dust ',
283           'SA' => 'Sand ',
284           'HZ' => 'Haze ',
285           'PY' => 'Spray',
286           'PO' => 'Well-Developed Dust/Sand Whirls ',
287           'SQ' => 'Squalls ',
288           'FC' => 'Funnel Cloud Tornado Waterspout ',
289           'SS' => 'Sandstorm/Duststorm '
290         }
291         @cloud_condition_array = {
292           'SKC' => 'clear',
293           'CLR' => 'clear',
294           'VV'  => 'vertical visibility',
295           'FEW' => 'a few',
296           'SCT' => 'scattered',
297           'BKN' => 'broken',
298           'OVC' => 'overcast'
299         }
300         @strings = {
301           'mm_inches'             => '%s mm (%s inches)',
302           'precip_a_trace'        => 'a trace',
303           'precip_there_was'      => 'There was %s of precipitation ',
304           'sky_str_format1'       => 'There were %s at a height of %s meters (%s feet)',
305           'sky_str_clear'         => 'The sky was clear',
306           'sky_str_format2'       => ', %s at a height of %s meter (%s feet) and %s at a height of %s meters (%s feet)',
307           'sky_str_format3'       => ' and %s at a height of %s meters (%s feet)',
308           'clouds'                => ' clouds',
309           'clouds_cb'             => ' cumulonimbus clouds',
310           'clouds_tcu'            => ' towering cumulus clouds',
311           'visibility_format'     => 'The visibility was %s kilometers (%s miles).',
312           'wind_str_format1'      => 'blowing at a speed of %s meters per second (%s miles per hour)',
313           'wind_str_format2'      => ', with gusts to %s meters per second (%s miles per hour),',
314           'wind_str_format3'      => ' from the %s',
315           'wind_str_calm'         => 'calm',
316           'precip_last_hour'      => 'in the last hour. ',
317           'precip_last_6_hours'   => 'in the last 3 to 6 hours. ',
318           'precip_last_24_hours'  => 'in the last 24 hours. ',
319           'precip_snow'           => 'There is %s mm (%s inches) of snow on the ground. ',
320           'temp_min_max_6_hours'  => 'The maximum and minimum temperatures over the last 6 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit).',
321           'temp_max_6_hours'      => 'The maximum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
322           'temp_min_6_hours'      => 'The minimum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
323           'temp_min_max_24_hours' => 'The maximum and minimum temperatures over the last 24 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit). ',
324           'light'                 => 'Light ',
325           'moderate'              => 'Moderate ',
326           'heavy'                 => 'Heavy ',
327           'mild'                  => 'Mild ',
328           'nearby'                => 'Nearby ',
329           'current_weather'       => 'Current weather is %s. ',
330           'pretty_print_metar'    => '%s on %s, the wind was %s at %s. The temperature was %s degrees Celsius (%s degrees Fahrenheit), and the pressure was %s hPa (%s inHg). The relative humidity was %s%%. %s %s %s %s %s'
331         }
332
333         parse
334       end
335
336       def store_speed(value, windunit, meterspersec, knots, milesperhour)
337         # Helper function to convert and store speed based on unit.
338         # &$meterspersec, &$knots and &$milesperhour are passed on
339         # reference
340         if (windunit == 'KT')
341           # The windspeed measured in knots:
342           @decoded[knots] = sprintf("%.2f", value)
343           # The windspeed measured in meters per second, rounded to one decimal place:
344           @decoded[meterspersec] = sprintf("%.2f", value.to_f * 0.51444)
345           # The windspeed measured in miles per hour, rounded to one decimal place: */
346           @decoded[milesperhour] = sprintf("%.2f", value.to_f * 1.1507695060844667)
347         elsif (windunit == 'MPS')
348           # The windspeed measured in meters per second:
349           @decoded[meterspersec] = sprintf("%.2f", value)
350           # The windspeed measured in knots, rounded to one decimal place:
351           @decoded[knots] = sprintf("%.2f", value.to_f / 0.51444)
352           #The windspeed measured in miles per hour, rounded to one decimal place:
353           @decoded[milesperhour] = sprintf("%.1f", value.to_f / 0.51444 * 1.1507695060844667)
354         elsif (windunit == 'KMH')
355           # The windspeed measured in kilometers per hour:
356           @decoded[meterspersec] = sprintf("%.1f", value.to_f * 1000 / 3600)
357           @decoded[knots] = sprintf("%.1f", value.to_f * 1000 / 3600 / 0.51444)
358           # The windspeed measured in miles per hour, rounded to one decimal place:
359           @decoded[milesperhour] = sprintf("%.1f", knots.to_f * 1.1507695060844667)
360         end
361       end
362       
363       def parse
364         @decoded = Hash.new
365         puts @input
366         @input.split(" ").each {|part|
367           if (part == 'METAR')
368             # Type of Report: METAR
369             @decoded['type'] = 'METAR'
370           elsif (part == 'SPECI')
371             # Type of Report: SPECI
372             @decoded['type'] = 'SPECI'
373           elsif (part == 'AUTO')
374             # Report Modifier: AUTO
375             @decoded['report_mod'] = 'AUTO'
376           elsif (part == 'NIL')
377             @nodata = true
378           elsif (part =~ /^\S{4}$/ && ! (@decoded.has_key?('station')))
379             # Station Identifier
380             @decoded['station'] = part
381           elsif (part =~ /([0-9]{2})([0-9]{2})([0-9]{2})Z/)
382             # ignore this bit, it's useless without month/year. some of these
383             # things are hideously out of date.
384             # now = Time.new
385             # time = Time.gm(now.year, now.month, $1, $2, $3, 0)
386             # Date and Time of Report
387             # @decoded['time'] = time
388           elsif (part == 'COR')
389             # Report Modifier: COR
390             @decoded['report_mod'] = 'COR'
391           elsif (part =~ /([0-9]{3}|VRB)([0-9]{2,3}).*(KT|MPS|KMH)/)
392             # Wind Group
393             windunit = $3
394             # now do ereg to get the actual values
395             part =~ /([0-9]{3}|VRB)([0-9]{2,3})((G[0-9]{2,3})?#{windunit})/
396             if ($1 == 'VRB')
397               @decoded['wind_deg'] = 'variable directions'
398               @decoded['wind_dir_text'] = 'variable directions'
399               @decoded['wind_dir_text_short'] = 'VAR'
400             else
401               @decoded['wind_deg'] = $1
402               @decoded['wind_dir_text'] = @wind_dir_texts[($1.to_i/22.5).round]
403               @decoded['wind_dir_text_short'] = @wind_dir_texts_short[($1.to_i/22.5).round]
404             end
405             store_speed($2, windunit,
406                         'wind_meters_per_second',
407                         'wind_knots',
408                         'wind_miles_per_hour')
409
410             if ($4 != nil)
411               # We have a report with information about the gust.
412               # First we have the gust measured in knots
413                           if ($4 =~ /G([0-9]{2,3})/)
414               store_speed($1,windunit,
415                           'wind_gust_meters_per_second',
416                           'wind_gust_knots',
417                           'wind_gust_miles_per_hour')
418                                 end
419             end
420           elsif (part =~ /([0-9]{3})V([0-9]{3})/)
421             #  Variable wind-direction
422             @decoded['wind_var_beg'] = $1
423             @decoded['wind_var_end'] = $2
424           elsif (part == "9999")
425             # A strange value. When you look at other pages you see it
426             # interpreted like this (where I use > to signify 'Greater
427             # than'):
428             @decoded['visibility_miles'] = '>7';
429             @decoded['visibility_km']    = '>11.3';
430           elsif (part =~ /^([0-9]{4})$/)
431             # Visibility in meters (4 digits only)
432             # The visibility measured in kilometers, rounded to one decimal place.
433             @decoded['visibility_km'] = sprintf("%.1f", $1.to_i / 1000)
434             # The visibility measured in miles, rounded to one decimal place.
435             @decoded['visibility_miles'] = sprintf("%.1f", $1.to_i / 1000 / 1.609344)
436           elsif (part =~ /^[0-9]$/)
437             # Temp Visibility Group, single digit followed by space
438             @decoded['temp_visibility_miles'] = part
439           elsif (@decoded['temp_visibility_miles'] && (@decoded['temp_visibility_miles']+' '+part) =~ /^M?(([0-9]?)[ ]?([0-9])(\/?)([0-9]*))SM$/)
440             # Visibility Group
441             if ($4 == '/')
442               vis_miles = $2.to_i + $3.to_i/$5.to_i
443             else
444               vis_miles = $1.to_i;
445             end
446             if (@decoded['temp_visibility_miles'][0] == 'M')
447               # The visibility measured in miles, prefixed with < to indicate 'Less than'
448               @decoded['visibility_miles'] = '<' + sprintf("%.1f", vis_miles)
449               # The visibility measured in kilometers. The value is rounded
450               # to one decimal place, prefixed with < to indicate 'Less than' */
451               @decoded['visibility_km']    = '<' . sprintf("%.1f", vis_miles * 1.609344)
452             else
453               # The visibility measured in mile.s */
454               @decoded['visibility_miles'] = sprintf("%.1f", vis_miles)
455               # The visibility measured in kilometers, rounded to one decimal place.
456               @decoded['visibility_km']    = sprintf("%.1f", vis_miles * 1.609344)
457             end
458           elsif (part =~ /^(-|\+|VC|MI)?(TS|SH|FZ|BL|DR|BC|PR|RA|DZ|SN|SG|GR|GS|PE|IC|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)+$/)
459             # Current weather-group
460             @decoded['weather'] = '' unless @decoded.has_key?('weather')
461             if (part[0].chr == '-')
462               # A light phenomenon
463               @decoded['weather'] += @strings['light']
464               part = part[1,part.length]
465             elsif (part[0].chr == '+')
466               # A heavy phenomenon
467               @decoded['weather'] += @strings['heavy']
468               part = part[1,part.length]
469             elsif (part[0,2] == 'VC')
470               # Proximity Qualifier
471               @decoded['weather'] += @strings['nearby']
472               part = part[2,part.length]
473             elsif (part[0,2] == 'MI')
474               @decoded['weather'] += @strings['mild']
475               part = part[2,part.length]
476             else
477               # no intensity code => moderate phenomenon
478               @decoded['weather'] += @strings['moderate']
479             end
480             
481             while (part && bite = part[0,2]) do
482               # Now we take the first two letters and determine what they
483               # mean. We append this to the variable so that we gradually
484               # build up a phrase.
485
486               @decoded['weather'] += @weather_array[bite]
487               # Here we chop off the two first letters, so that we can take
488               # a new bite at top of the while-loop.
489               part = part[2,-1]
490             end
491           elsif (part =~ /(SKC|CLR)/)
492             # Cloud-layer-group.
493             # There can be up to three of these groups, so we store them as
494             # cloud_layer1, cloud_layer2 and cloud_layer3.
495             
496             @cloud_layers += 1;
497             # Again we have to translate the code-characters to a
498             # meaningful string.
499             @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition']  = @cloud_condition_array[$1]
500             @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
501           elsif (part =~ /^(VV|FEW|SCT|BKN|OVC)([0-9]{3})(CB|TCU)?$/)
502             # We have found (another) a cloud-layer-group. There can be up
503             # to three of these groups, so we store them as cloud_layer1,
504             # cloud_layer2 and cloud_layer3.
505             @cloud_layers += 1;
506             # Again we have to translate the code-characters to a meaningful string.
507             if ($3 == 'CB')
508               # cumulonimbus (CB) clouds were observed. */
509               @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
510                           @cloud_condition_array[$1] + @strings['clouds_cb']
511             elsif ($3 == 'TCU')
512               # towering cumulus (TCU) clouds were observed.
513               @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
514                           @cloud_condition_array[$1] + @strings['clouds_tcu']
515             else
516               @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
517                           @cloud_condition_array[$1] + @strings['clouds']
518             end
519             @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
520             @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_ft'] = $2.to_i * 100
521             @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_m']  = ($2.to_f * 30.48).round
522           elsif (part =~ /^T([0-9]{4})$/)
523             store_temp($1,'temp_c','temp_f')
524           elsif (part =~ /^T?(M?[0-9]{2})\/(M?[0-9\/]{1,2})?$/)
525             # Temperature/Dew Point Group
526             # The temperature and dew-point measured in Celsius.
527             @decoded['temp_c'] = sprintf("%d", $1.tr('M', '-'))
528             if $2 == "//" || !$2
529               @decoded['dew_c'] = 0
530             else
531               @decoded['dew_c'] = sprintf("%.1f", $2.tr('M', '-'))
532             end
533             # The temperature and dew-point measured in Fahrenheit, rounded to
534             # the nearest degree.
535             @decoded['temp_f'] = ((@decoded['temp_c'].to_f * 9 / 5) + 32).round
536             @decoded['dew_f']  = ((@decoded['dew_c'].to_f * 9 / 5) + 32).round
537           elsif(part =~ /A([0-9]{4})/)
538             # Altimeter
539             # The pressure measured in inHg
540             @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_i/100)
541             # The pressure measured in mmHg, hPa and atm
542             @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.254)
543             @decoded['altimeter_hpa']  = sprintf("%d", ($1.to_f * 0.33863881578947).to_i)
544             @decoded['altimeter_atm']  = sprintf("%.3f", $1.to_f * 3.3421052631579e-4)
545           elsif(part =~ /Q([0-9]{4})/)
546             # Altimeter
547             # This is strange, the specification doesnt say anything about
548             # the Qxxxx-form, but it's in the METARs.
549             # The pressure measured in hPa
550             @decoded['altimeter_hpa']  = sprintf("%d", $1.to_i)
551             # The pressure measured in mmHg, inHg and atm
552             @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.7500616827)
553             @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_f * 0.0295299875)
554             @decoded['altimeter_atm']  = sprintf("%.3f", $1.to_f * 9.869232667e-4)
555           elsif (part =~ /^T([0-9]{4})([0-9]{4})/)
556             # Temperature/Dew Point Group, coded to tenth of degree.
557             # The temperature and dew-point measured in Celsius.
558             store_temp($1,'temp_c','temp_f')
559             store_temp($2,'dew_c','dew_f')
560           elsif (part =~ /^1([0-9]{4}$)/)
561             # 6 hour maximum temperature Celsius, coded to tenth of degree
562             store_temp($1,'temp_max6h_c','temp_max6h_f')
563           elsif (part =~ /^2([0-9]{4}$)/)
564             # 6 hour minimum temperature Celsius, coded to tenth of degree
565             store_temp($1,'temp_min6h_c','temp_min6h_f')
566           elsif (part =~ /^4([0-9]{4})([0-9]{4})$/)
567             # 24 hour maximum and minimum temperature Celsius, coded to
568             # tenth of degree
569             store_temp($1,'temp_max24h_c','temp_max24h_f')
570             store_temp($2,'temp_min24h_c','temp_min24h_f')
571           elsif (part =~ /^P([0-9]{4})/)
572             # Precipitation during last hour in hundredths of an inch
573             # (store as inches)
574             @decoded['precip_in'] = sprintf("%.2f", $1.to_f/100)
575             @decoded['precip_mm'] = sprintf("%.2f", $1.to_f * 0.254)
576           elsif (part =~ /^6([0-9]{4})/)
577             # Precipitation during last 3 or 6 hours in hundredths of an
578             # inch  (store as inches)
579             @decoded['precip_6h_in'] = sprintf("%.2f", $1.to_f/100)
580             @decoded['precip_6h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
581           elsif (part =~ /^7([0-9]{4})/)
582             # Precipitation during last 24 hours in hundredths of an inch
583             # (store as inches)
584             @decoded['precip_24h_in'] = sprintf("%.2f", $1.to_f/100)
585             @decoded['precip_24h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
586           elsif(part =~ /^4\/([0-9]{3})/)
587             # Snow depth in inches
588             @decoded['snow_in'] = sprintf("%.2f", $1);
589             @decoded['snow_mm'] = sprintf("%.2f", $1.to_f * 25.4)
590           else
591             # If we couldn't match the group, we assume that it was a
592             # remark.
593             @decoded['remarks'] = '' unless @decoded.has_key?("remarks")
594             @decoded['remarks'] += ' ' + part;
595           end
596         }
597         
598         # Relative humidity
599         # p @decoded['dew_c'] # 11.0
600         # p @decoded['temp_c'] # 21.0
601         # => 56.1
602         @decoded['rel_humidity'] = sprintf("%.1f",100 * 
603           (6.11 * (10.0**(7.5 * @decoded['dew_c'].to_f / (237.7 + @decoded['dew_c'].to_f)))) / (6.11 * (10.0 ** (7.5 * @decoded['temp_c'].to_f / (237.7 + @decoded['temp_c'].to_f))))) if @decoded.has_key?('dew_c')
604       end
605
606       def store_temp(temp,temp_cname,temp_fname)
607         # Given a numerical temperature temp in Celsius, coded to tenth of
608         # degree, store in @decoded[temp_cname], convert to Fahrenheit
609         # and store in @decoded[temp_fname]
610         # Note: temp is converted to negative if temp > 100.0 (See
611         # Federal Meteorological Handbook for groups T, 1, 2 and 4)
612
613         # Temperature measured in Celsius, coded to tenth of degree
614         temp = temp.to_f/10
615         if (temp >100.0) 
616            # first digit = 1 means minus temperature
617            temp = -(temp - 100.0)
618         end
619         @decoded[temp_cname] = sprintf("%.1f", temp)
620         # The temperature in Fahrenheit.
621         @decoded[temp_fname] = sprintf("%.1f", (temp * 9 / 5) + 32)        
622       end
623
624        def pretty_print_precip(precip_mm, precip_in)
625          # Returns amount if $precip_mm > 0, otherwise "trace" (see Federal
626          # Meteorological Handbook No. 1 for code groups P, 6 and 7) used in
627          # several places, so standardized in one function.
628          if (precip_mm.to_i > 0)
629            amount = sprintf(@strings['mm_inches'], precip_mm, precip_in)
630          else
631            amount = @strings['a_trace']
632          end
633          return sprintf(@strings['precip_there_was'], amount)
634       end
635
636       def pretty_print
637        if @nodata
638          return "The weather stored for #{@decoded['station']} consists of the string 'NIL' :("
639        end
640
641        ["temp_c", "altimeter_hpa"].each {|key|
642          if !@decoded.has_key?(key)
643            return "The weather stored for #{@decoded['station']} could not be parsed (#{@input})"
644          end
645        }
646        
647        mins_old = ((Time.now - @date.to_i).to_f/60).round
648        if (mins_old <= 60)
649          weather_age = mins_old.to_s + " minutes ago,"
650        elsif (mins_old <= 60 * 25)
651          weather_age = (mins_old / 60).to_s + " hours, "
652          weather_age += (mins_old % 60).to_s + " minutes ago,"
653        else
654          # return "The weather stored for #{@decoded['station']} is hideously out of date :( (Last update #{@date})"
655          weather_age = "The weather stored for #{@decoded['station']} is hideously out of date :( here it is anyway:"
656        end
657         
658        if(@decoded.has_key?("cloud_layer1_altitude_ft"))
659          sky_str = sprintf(@strings['sky_str_format1'],
660                            @decoded["cloud_layer1_condition"],
661                            @decoded["cloud_layer1_altitude_m"],
662                            @decoded["cloud_layer1_altitude_ft"])
663        else
664          sky_str = @strings['sky_str_clear']
665        end
666
667        if(@decoded.has_key?("cloud_layer2_altitude_ft"))
668          if(@decoded.has_key?("cloud_layer3_altitude_ft"))
669            sky_str += sprintf(@strings['sky_str_format2'],
670                               @decoded["cloud_layer2_condition"],
671                               @decoded["cloud_layer2_altitude_m"],
672                               @decoded["cloud_layer2_altitude_ft"],
673                               @decoded["cloud_layer3_condition"],
674                               @decoded["cloud_layer3_altitude_m"],
675                               @decoded["cloud_layer3_altitude_ft"])
676          else
677            sky_str += sprintf(@strings['sky_str_format3'],
678                               @decoded["cloud_layer2_condition"],
679                               @decoded["cloud_layer2_altitude_m"],
680                               @decoded["cloud_layer2_altitude_ft"])
681          end
682        end
683        sky_str += "."
684
685        if(@decoded.has_key?("visibility_miles"))
686          visibility = sprintf(@strings['visibility_format'],
687                               @decoded["visibility_km"],
688                               @decoded["visibility_miles"])
689        else
690          visibility = ""
691        end
692
693        if (@decoded.has_key?("wind_meters_per_second") && @decoded["wind_meters_per_second"].to_i > 0)
694          wind_str = sprintf(@strings['wind_str_format1'],
695                             @decoded["wind_meters_per_second"],
696                             @decoded["wind_miles_per_hour"])
697          if (@decoded.has_key?("wind_gust_meters_per_second") && @decoded["wind_gust_meters_per_second"].to_i > 0)
698            wind_str += sprintf(@strings['wind_str_format2'],
699                                @decoded["wind_gust_meters_per_second"],
700                                @decoded["wind_gust_miles_per_hour"])
701          end
702          wind_str += sprintf(@strings['wind_str_format3'],
703                              @decoded["wind_dir_text"])
704        else
705          wind_str = @strings['wind_str_calm']
706        end
707
708        prec_str = ""
709        if (@decoded.has_key?("precip_in"))
710          prec_str += pretty_print_precip(@decoded["precip_mm"], @decoded["precip_in"]) + @strings['precip_last_hour']
711        end
712        if (@decoded.has_key?("precip_6h_in"))
713          prec_str += pretty_print_precip(@decoded["precip_6h_mm"], @decoded["precip_6h_in"]) + @strings['precip_last_6_hours']
714        end
715        if (@decoded.has_key?("precip_24h_in"))
716          prec_str += pretty_print_precip(@decoded["precip_24h_mm"], @decoded["precip_24h_in"]) + @strings['precip_last_24_hours']
717        end
718        if (@decoded.has_key?("snow_in"))
719          prec_str += sprintf(@strings['precip_snow'], @decoded["snow_mm"], @decoded["snow_in"])
720        end
721
722        temp_str = ""
723        if (@decoded.has_key?("temp_max6h_c") && @decoded.has_key?("temp_min6h_c"))
724          temp_str += sprintf(@strings['temp_min_max_6_hours'],
725                              @decoded["temp_max6h_c"],
726                              @decoded["temp_min6h_c"],
727                              @decoded["temp_max6h_f"],
728                              @decoded["temp_min6h_f"])
729        else
730          if (@decoded.has_key?("temp_max6h_c"))
731            temp_str += sprintf(@strings['temp_max_6_hours'],
732                                @decoded["temp_max6h_c"],
733                                @decoded["temp_max6h_f"])
734          end
735          if (@decoded.has_key?("temp_min6h_c"))
736            temp_str += sprintf(@strings['temp_max_6_hours'],
737                                @decoded["temp_min6h_c"],
738                                @decoded["temp_min6h_f"])
739          end
740        end
741        if (@decoded.has_key?("temp_max24h_c"))
742          temp_str += sprintf(@strings['temp_min_max_24_hours'],
743                              @decoded["temp_max24h_c"],
744                              @decoded["temp_min24h_c"],
745                              @decoded["temp_max24h_f"],
746                              @decoded["temp_min24h_f"])
747        end
748
749        if (@decoded.has_key?("weather"))
750          weather_str = sprintf(@strings['current_weather'], @decoded["weather"])
751        else
752          weather_str = ''
753        end
754
755        return sprintf(@strings['pretty_print_metar'],
756                       weather_age,
757                       @date,
758                       wind_str, @decoded["station"], @decoded["temp_c"],
759                       @decoded["temp_f"], @decoded["altimeter_hpa"],
760                       @decoded["altimeter_inhg"],
761                       @decoded["rel_humidity"], sky_str,
762                       visibility, weather_str, prec_str, temp_str).strip
763       end
764   
765       def to_s
766         @input
767       end
768     end
769
770     def Utils.get_metar(station)
771       station.upcase!
772       
773       result = Utils.http_get("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT")
774       return nil unless result
775       return Metar.new(result)
776     end
777   end
778 end