1 # This is nasty-ass. I hate writing parsers.
12 # grab first line (date)
19 str += " " + l.chomp.strip
23 if @date && @date =~ /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+)$/
25 @date = Time.gm($1, $2, $3, $4, $5, 0)
59 @wind_dir_texts_short = [
82 'DR' => 'Low Drifting ',
85 'TS' => 'Thunderstorm ',
90 'SG' => 'Snow Grains ',
91 'IC' => 'Ice Crystals ',
92 'PE' => 'Ice Pellets ',
94 'GS' => 'Small Hail and/or Snow Pellets ',
99 'VA' => 'Volcanic Ash ',
100 'DU' => 'Widespread Dust ',
104 'PO' => 'Well-Developed Dust/Sand Whirls ',
106 'FC' => 'Funnel Cloud Tornado Waterspout ',
107 'SS' => 'Sandstorm/Duststorm '
109 @cloud_condition_array = {
112 'VV' => 'vertical visibility',
114 'SCT' => 'scattered',
119 'mm_inches' => '%s mm (%s inches)',
120 'precip_a_trace' => 'a trace',
121 'precip_there_was' => 'There was %s of precipitation ',
122 'sky_str_format1' => 'There were %s at a height of %s meters (%s feet)',
123 'sky_str_clear' => 'The sky was clear',
124 'sky_str_format2' => ', %s at a height of %s meter (%s feet) and %s at a height of %s meters (%s feet)',
125 'sky_str_format3' => ' and %s at a height of %s meters (%s feet)',
126 'clouds' => ' clouds',
127 'clouds_cb' => ' cumulonimbus clouds',
128 'clouds_tcu' => ' towering cumulus clouds',
129 'visibility_format' => 'The visibility was %s kilometers (%s miles).',
130 'wind_str_format1' => 'blowing at a speed of %s meters per second (%s miles per hour)',
131 'wind_str_format2' => ', with gusts to %s meters per second (%s miles per hour),',
132 'wind_str_format3' => ' from the %s',
133 'wind_str_calm' => 'calm',
134 'precip_last_hour' => 'in the last hour. ',
135 'precip_last_6_hours' => 'in the last 3 to 6 hours. ',
136 'precip_last_24_hours' => 'in the last 24 hours. ',
137 'precip_snow' => 'There is %s mm (%s inches) of snow on the ground. ',
138 '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).',
139 'temp_max_6_hours' => 'The maximum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
140 'temp_min_6_hours' => 'The minimum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
141 '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). ',
143 'moderate' => 'Moderate ',
146 'nearby' => 'Nearby ',
147 'current_weather' => 'Current weather is %s. ',
148 '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'
154 def store_speed(value, windunit, meterspersec, knots, milesperhour)
155 # Helper function to convert and store speed based on unit.
156 # &$meterspersec, &$knots and &$milesperhour are passed on
158 if (windunit == 'KT')
159 # The windspeed measured in knots:
160 @decoded[knots] = sprintf("%.2f", value)
161 # The windspeed measured in meters per second, rounded to one decimal place:
162 @decoded[meterspersec] = sprintf("%.2f", value.to_f * 0.51444)
163 # The windspeed measured in miles per hour, rounded to one decimal place: */
164 @decoded[milesperhour] = sprintf("%.2f", value.to_f * 1.1507695060844667)
165 elsif (windunit == 'MPS')
166 # The windspeed measured in meters per second:
167 @decoded[meterspersec] = sprintf("%.2f", value)
168 # The windspeed measured in knots, rounded to one decimal place:
169 @decoded[knots] = sprintf("%.2f", value.to_f / 0.51444)
170 #The windspeed measured in miles per hour, rounded to one decimal place:
171 @decoded[milesperhour] = sprintf("%.1f", value.to_f / 0.51444 * 1.1507695060844667)
172 elsif (windunit == 'KMH')
173 # The windspeed measured in kilometers per hour:
174 @decoded[meterspersec] = sprintf("%.1f", value.to_f * 1000 / 3600)
175 @decoded[knots] = sprintf("%.1f", value.to_f * 1000 / 3600 / 0.51444)
176 # The windspeed measured in miles per hour, rounded to one decimal place:
177 @decoded[milesperhour] = sprintf("%.1f", knots.to_f * 1.1507695060844667)
184 @input.split(" ").each {|part|
186 # Type of Report: METAR
187 @decoded['type'] = 'METAR'
188 elsif (part == 'SPECI')
189 # Type of Report: SPECI
190 @decoded['type'] = 'SPECI'
191 elsif (part == 'AUTO')
192 # Report Modifier: AUTO
193 @decoded['report_mod'] = 'AUTO'
194 elsif (part == 'NIL')
196 elsif (part =~ /^\S{4}$/ && ! (@decoded.has_key?('station')))
198 @decoded['station'] = part
199 elsif (part =~ /([0-9]{2})([0-9]{2})([0-9]{2})Z/)
200 # ignore this bit, it's useless without month/year. some of these
201 # things are hideously out of date.
203 # time = Time.gm(now.year, now.month, $1, $2, $3, 0)
204 # Date and Time of Report
205 # @decoded['time'] = time
206 elsif (part == 'COR')
207 # Report Modifier: COR
208 @decoded['report_mod'] = 'COR'
209 elsif (part =~ /([0-9]{3}|VRB)([0-9]{2,3}).*(KT|MPS|KMH)/)
212 # now do ereg to get the actual values
213 part =~ /([0-9]{3}|VRB)([0-9]{2,3})((G[0-9]{2,3})?#{windunit})/
215 @decoded['wind_deg'] = 'variable directions'
216 @decoded['wind_dir_text'] = 'variable directions'
217 @decoded['wind_dir_text_short'] = 'VAR'
219 @decoded['wind_deg'] = $1
220 @decoded['wind_dir_text'] = @wind_dir_texts[($1.to_i/22.5).round]
221 @decoded['wind_dir_text_short'] = @wind_dir_texts_short[($1.to_i/22.5).round]
223 store_speed($2, windunit,
224 'wind_meters_per_second',
226 'wind_miles_per_hour')
229 # We have a report with information about the gust.
230 # First we have the gust measured in knots
231 if ($4 =~ /G([0-9]{2,3})/)
232 store_speed($1,windunit,
233 'wind_gust_meters_per_second',
235 'wind_gust_miles_per_hour')
238 elsif (part =~ /([0-9]{3})V([0-9]{3})/)
239 # Variable wind-direction
240 @decoded['wind_var_beg'] = $1
241 @decoded['wind_var_end'] = $2
242 elsif (part == "9999")
243 # A strange value. When you look at other pages you see it
244 # interpreted like this (where I use > to signify 'Greater
246 @decoded['visibility_miles'] = '>7';
247 @decoded['visibility_km'] = '>11.3';
248 elsif (part =~ /^([0-9]{4})$/)
249 # Visibility in meters (4 digits only)
250 # The visibility measured in kilometers, rounded to one decimal place.
251 @decoded['visibility_km'] = sprintf("%.1f", $1.to_i / 1000)
252 # The visibility measured in miles, rounded to one decimal place.
253 @decoded['visibility_miles'] = sprintf("%.1f", $1.to_i / 1000 / 1.609344)
254 elsif (part =~ /^[0-9]$/)
255 # Temp Visibility Group, single digit followed by space
256 @decoded['temp_visibility_miles'] = part
257 elsif (@decoded['temp_visibility_miles'] && (@decoded['temp_visibility_miles']+' '+part) =~ /^M?(([0-9]?)[ ]?([0-9])(\/?)([0-9]*))SM$/)
260 vis_miles = $2.to_i + $3.to_i/$5.to_i
264 if (@decoded['temp_visibility_miles'][0] == 'M')
265 # The visibility measured in miles, prefixed with < to indicate 'Less than'
266 @decoded['visibility_miles'] = '<' + sprintf("%.1f", vis_miles)
267 # The visibility measured in kilometers. The value is rounded
268 # to one decimal place, prefixed with < to indicate 'Less than' */
269 @decoded['visibility_km'] = '<' . sprintf("%.1f", vis_miles * 1.609344)
271 # The visibility measured in mile.s */
272 @decoded['visibility_miles'] = sprintf("%.1f", vis_miles)
273 # The visibility measured in kilometers, rounded to one decimal place.
274 @decoded['visibility_km'] = sprintf("%.1f", vis_miles * 1.609344)
276 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)+$/)
277 # Current weather-group
278 @decoded['weather'] = '' unless @decoded.has_key?('weather')
279 if (part[0].chr == '-')
281 @decoded['weather'] += @strings['light']
282 part = part[1,part.length]
283 elsif (part[0].chr == '+')
285 @decoded['weather'] += @strings['heavy']
286 part = part[1,part.length]
287 elsif (part[0,2] == 'VC')
288 # Proximity Qualifier
289 @decoded['weather'] += @strings['nearby']
290 part = part[2,part.length]
291 elsif (part[0,2] == 'MI')
292 @decoded['weather'] += @strings['mild']
293 part = part[2,part.length]
295 # no intensity code => moderate phenomenon
296 @decoded['weather'] += @strings['moderate']
299 while (part && bite = part[0,2]) do
300 # Now we take the first two letters and determine what they
301 # mean. We append this to the variable so that we gradually
304 @decoded['weather'] += @weather_array[bite]
305 # Here we chop off the two first letters, so that we can take
306 # a new bite at top of the while-loop.
309 elsif (part =~ /(SKC|CLR)/)
311 # There can be up to three of these groups, so we store them as
312 # cloud_layer1, cloud_layer2 and cloud_layer3.
315 # Again we have to translate the code-characters to a
317 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = @cloud_condition_array[$1]
318 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
319 elsif (part =~ /^(VV|FEW|SCT|BKN|OVC)([0-9]{3})(CB|TCU)?$/)
320 # We have found (another) a cloud-layer-group. There can be up
321 # to three of these groups, so we store them as cloud_layer1,
322 # cloud_layer2 and cloud_layer3.
324 # Again we have to translate the code-characters to a meaningful string.
326 # cumulonimbus (CB) clouds were observed. */
327 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
328 @cloud_condition_array[$1] + @strings['clouds_cb']
330 # towering cumulus (TCU) clouds were observed.
331 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
332 @cloud_condition_array[$1] + @strings['clouds_tcu']
334 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
335 @cloud_condition_array[$1] + @strings['clouds']
337 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
338 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_ft'] = $2.to_i * 100
339 @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_m'] = ($2.to_f * 30.48).round
340 elsif (part =~ /^T([0-9]{4})$/)
341 store_temp($1,'temp_c','temp_f')
342 elsif (part =~ /^T?(M?[0-9]{2})\/(M?[0-9\/]{1,2})?$/)
343 # Temperature/Dew Point Group
344 # The temperature and dew-point measured in Celsius.
345 @decoded['temp_c'] = sprintf("%d", $1.tr('M', '-'))
347 @decoded['dew_c'] = 0
349 @decoded['dew_c'] = sprintf("%.1f", $2.tr('M', '-'))
351 # The temperature and dew-point measured in Fahrenheit, rounded to
352 # the nearest degree.
353 @decoded['temp_f'] = ((@decoded['temp_c'].to_f * 9 / 5) + 32).round
354 @decoded['dew_f'] = ((@decoded['dew_c'].to_f * 9 / 5) + 32).round
355 elsif(part =~ /A([0-9]{4})/)
357 # The pressure measured in inHg
358 @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_i/100)
359 # The pressure measured in mmHg, hPa and atm
360 @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.254)
361 @decoded['altimeter_hpa'] = sprintf("%d", ($1.to_f * 0.33863881578947).to_i)
362 @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 3.3421052631579e-4)
363 elsif(part =~ /Q([0-9]{4})/)
365 # This is strange, the specification doesnt say anything about
366 # the Qxxxx-form, but it's in the METARs.
367 # The pressure measured in hPa
368 @decoded['altimeter_hpa'] = sprintf("%d", $1.to_i)
369 # The pressure measured in mmHg, inHg and atm
370 @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.7500616827)
371 @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_f * 0.0295299875)
372 @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 9.869232667e-4)
373 elsif (part =~ /^T([0-9]{4})([0-9]{4})/)
374 # Temperature/Dew Point Group, coded to tenth of degree.
375 # The temperature and dew-point measured in Celsius.
376 store_temp($1,'temp_c','temp_f')
377 store_temp($2,'dew_c','dew_f')
378 elsif (part =~ /^1([0-9]{4}$)/)
379 # 6 hour maximum temperature Celsius, coded to tenth of degree
380 store_temp($1,'temp_max6h_c','temp_max6h_f')
381 elsif (part =~ /^2([0-9]{4}$)/)
382 # 6 hour minimum temperature Celsius, coded to tenth of degree
383 store_temp($1,'temp_min6h_c','temp_min6h_f')
384 elsif (part =~ /^4([0-9]{4})([0-9]{4})$/)
385 # 24 hour maximum and minimum temperature Celsius, coded to
387 store_temp($1,'temp_max24h_c','temp_max24h_f')
388 store_temp($2,'temp_min24h_c','temp_min24h_f')
389 elsif (part =~ /^P([0-9]{4})/)
390 # Precipitation during last hour in hundredths of an inch
392 @decoded['precip_in'] = sprintf("%.2f", $1.to_f/100)
393 @decoded['precip_mm'] = sprintf("%.2f", $1.to_f * 0.254)
394 elsif (part =~ /^6([0-9]{4})/)
395 # Precipitation during last 3 or 6 hours in hundredths of an
396 # inch (store as inches)
397 @decoded['precip_6h_in'] = sprintf("%.2f", $1.to_f/100)
398 @decoded['precip_6h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
399 elsif (part =~ /^7([0-9]{4})/)
400 # Precipitation during last 24 hours in hundredths of an inch
402 @decoded['precip_24h_in'] = sprintf("%.2f", $1.to_f/100)
403 @decoded['precip_24h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
404 elsif(part =~ /^4\/([0-9]{3})/)
405 # Snow depth in inches
406 @decoded['snow_in'] = sprintf("%.2f", $1);
407 @decoded['snow_mm'] = sprintf("%.2f", $1.to_f * 25.4)
409 # If we couldn't match the group, we assume that it was a
411 @decoded['remarks'] = '' unless @decoded.has_key?("remarks")
412 @decoded['remarks'] += ' ' + part;
417 # p @decoded['dew_c'] # 11.0
418 # p @decoded['temp_c'] # 21.0
420 @decoded['rel_humidity'] = sprintf("%.1f",100 *
421 (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')
424 def store_temp(temp,temp_cname,temp_fname)
425 # Given a numerical temperature temp in Celsius, coded to tenth of
426 # degree, store in @decoded[temp_cname], convert to Fahrenheit
427 # and store in @decoded[temp_fname]
428 # Note: temp is converted to negative if temp > 100.0 (See
429 # Federal Meteorological Handbook for groups T, 1, 2 and 4)
431 # Temperature measured in Celsius, coded to tenth of degree
434 # first digit = 1 means minus temperature
435 temp = -(temp - 100.0)
437 @decoded[temp_cname] = sprintf("%.1f", temp)
438 # The temperature in Fahrenheit.
439 @decoded[temp_fname] = sprintf("%.1f", (temp * 9 / 5) + 32)
442 def pretty_print_precip(precip_mm, precip_in)
443 # Returns amount if $precip_mm > 0, otherwise "trace" (see Federal
444 # Meteorological Handbook No. 1 for code groups P, 6 and 7) used in
445 # several places, so standardized in one function.
446 if (precip_mm.to_i > 0)
447 amount = sprintf(@strings['mm_inches'], precip_mm, precip_in)
449 amount = @strings['a_trace']
451 return sprintf(@strings['precip_there_was'], amount)
456 return "The weather stored for #{@decoded['station']} consists of the string 'NIL' :("
459 ["temp_c", "altimeter_hpa"].each {|key|
460 if !@decoded.has_key?(key)
461 return "The weather stored for #{@decoded['station']} could not be parsed (#{@input})"
465 mins_old = ((Time.now - @date.to_i).to_f/60).round
467 weather_age = mins_old.to_s + " minutes ago,"
468 elsif (mins_old <= 60 * 25)
469 weather_age = (mins_old / 60).to_s + " hours, "
470 weather_age += (mins_old % 60).to_s + " minutes ago,"
472 # return "The weather stored for #{@decoded['station']} is hideously out of date :( (Last update #{@date})"
473 weather_age = "The weather stored for #{@decoded['station']} is hideously out of date :( here it is anyway:"
476 if(@decoded.has_key?("cloud_layer1_altitude_ft"))
477 sky_str = sprintf(@strings['sky_str_format1'],
478 @decoded["cloud_layer1_condition"],
479 @decoded["cloud_layer1_altitude_m"],
480 @decoded["cloud_layer1_altitude_ft"])
482 sky_str = @strings['sky_str_clear']
485 if(@decoded.has_key?("cloud_layer2_altitude_ft"))
486 if(@decoded.has_key?("cloud_layer3_altitude_ft"))
487 sky_str += sprintf(@strings['sky_str_format2'],
488 @decoded["cloud_layer2_condition"],
489 @decoded["cloud_layer2_altitude_m"],
490 @decoded["cloud_layer2_altitude_ft"],
491 @decoded["cloud_layer3_condition"],
492 @decoded["cloud_layer3_altitude_m"],
493 @decoded["cloud_layer3_altitude_ft"])
495 sky_str += sprintf(@strings['sky_str_format3'],
496 @decoded["cloud_layer2_condition"],
497 @decoded["cloud_layer2_altitude_m"],
498 @decoded["cloud_layer2_altitude_ft"])
503 if(@decoded.has_key?("visibility_miles"))
504 visibility = sprintf(@strings['visibility_format'],
505 @decoded["visibility_km"],
506 @decoded["visibility_miles"])
511 if (@decoded.has_key?("wind_meters_per_second") && @decoded["wind_meters_per_second"].to_i > 0)
512 wind_str = sprintf(@strings['wind_str_format1'],
513 @decoded["wind_meters_per_second"],
514 @decoded["wind_miles_per_hour"])
515 if (@decoded.has_key?("wind_gust_meters_per_second") && @decoded["wind_gust_meters_per_second"].to_i > 0)
516 wind_str += sprintf(@strings['wind_str_format2'],
517 @decoded["wind_gust_meters_per_second"],
518 @decoded["wind_gust_miles_per_hour"])
520 wind_str += sprintf(@strings['wind_str_format3'],
521 @decoded["wind_dir_text"])
523 wind_str = @strings['wind_str_calm']
527 if (@decoded.has_key?("precip_in"))
528 prec_str += pretty_print_precip(@decoded["precip_mm"], @decoded["precip_in"]) + @strings['precip_last_hour']
530 if (@decoded.has_key?("precip_6h_in"))
531 prec_str += pretty_print_precip(@decoded["precip_6h_mm"], @decoded["precip_6h_in"]) + @strings['precip_last_6_hours']
533 if (@decoded.has_key?("precip_24h_in"))
534 prec_str += pretty_print_precip(@decoded["precip_24h_mm"], @decoded["precip_24h_in"]) + @strings['precip_last_24_hours']
536 if (@decoded.has_key?("snow_in"))
537 prec_str += sprintf(@strings['precip_snow'], @decoded["snow_mm"], @decoded["snow_in"])
541 if (@decoded.has_key?("temp_max6h_c") && @decoded.has_key?("temp_min6h_c"))
542 temp_str += sprintf(@strings['temp_min_max_6_hours'],
543 @decoded["temp_max6h_c"],
544 @decoded["temp_min6h_c"],
545 @decoded["temp_max6h_f"],
546 @decoded["temp_min6h_f"])
548 if (@decoded.has_key?("temp_max6h_c"))
549 temp_str += sprintf(@strings['temp_max_6_hours'],
550 @decoded["temp_max6h_c"],
551 @decoded["temp_max6h_f"])
553 if (@decoded.has_key?("temp_min6h_c"))
554 temp_str += sprintf(@strings['temp_max_6_hours'],
555 @decoded["temp_min6h_c"],
556 @decoded["temp_min6h_f"])
559 if (@decoded.has_key?("temp_max24h_c"))
560 temp_str += sprintf(@strings['temp_min_max_24_hours'],
561 @decoded["temp_max24h_c"],
562 @decoded["temp_min24h_c"],
563 @decoded["temp_max24h_f"],
564 @decoded["temp_min24h_f"])
567 if (@decoded.has_key?("weather"))
568 weather_str = sprintf(@strings['current_weather'], @decoded["weather"])
573 return sprintf(@strings['pretty_print_metar'],
576 wind_str, @decoded["station"], @decoded["temp_c"],
577 @decoded["temp_f"], @decoded["altimeter_hpa"],
578 @decoded["altimeter_inhg"],
579 @decoded["rel_humidity"], sky_str,
580 visibility, weather_str, prec_str, temp_str).strip
589 class WeatherPlugin < Plugin
591 def help(plugin, topic="")
592 "weather <ICAO> => display the current weather at the location specified by the ICAO code [Lookup your ICAO code at http://www.nws.noaa.gov/tg/siteloc.shtml - this will also store the ICAO against your nick, so you can later just say \"weather\", weather => display the current weather at the location you last asked for"
595 def get_metar(station)
598 result = @bot.httputil.get(URI.parse("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT"))
599 return nil unless result
600 return Metar.new(result)
606 # this plugin only wants to store strings
615 @metar_cache = Hash.new
618 def describe(m, where)
619 if @metar_cache.has_key?(where) &&
620 Time.now - @metar_cache[where].date < 3600
621 met = @metar_cache[where]
623 met = get_metar(where)
627 m.reply met.pretty_print
628 @metar_cache[where] = met
630 m.reply "couldn't find weather data for #{where}"
634 def weather(m, params)
636 @registry[m.sourcenick] = params[:where]
637 describe(m,params[:where])
639 if @registry.has_key?(m.sourcenick)
640 where = @registry[m.sourcenick]
643 m.reply "I don't know where you are yet! Lookup your code at http://www.nws.noaa.gov/tg/siteloc.shtml and tell me 'weather <code>', then I'll know."
648 plugin = WeatherPlugin.new
649 plugin.map 'weather :where', :defaults => {:where => false}