summaryrefslogtreecommitdiff
path: root/rbot/utils.rb
diff options
context:
space:
mode:
Diffstat (limited to 'rbot/utils.rb')
-rw-r--r--rbot/utils.rb778
1 files changed, 778 insertions, 0 deletions
diff --git a/rbot/utils.rb b/rbot/utils.rb
new file mode 100644
index 00000000..516fb4dc
--- /dev/null
+++ b/rbot/utils.rb
@@ -0,0 +1,778 @@
+require 'net/http'
+require 'uri'
+
+module Irc
+
+ # miscellaneous useful functions
+ module Utils
+ # read a time in string format, turn it into "seconds from now".
+ # example formats handled are "5 minutes", "2 days", "five hours",
+ # "11:30", "15:45:11", "one day", etc.
+ #
+ # Throws:: RunTimeError "invalid time string" on parse failure
+ def Utils.timestr_offset(timestr)
+ case timestr
+ when (/^(\S+)\s+(\S+)$/)
+ mult = $1
+ unit = $2
+ if(mult =~ /^([\d.]+)$/)
+ num = $1.to_f
+ raise "invalid time string" unless num
+ else
+ case mult
+ when(/^(one|an|a)$/)
+ num = 1
+ when(/^two$/)
+ num = 2
+ when(/^three$/)
+ num = 3
+ when(/^four$/)
+ num = 4
+ when(/^five$/)
+ num = 5
+ when(/^six$/)
+ num = 6
+ when(/^seven$/)
+ num = 7
+ when(/^eight$/)
+ num = 8
+ when(/^nine$/)
+ num = 9
+ when(/^ten$/)
+ num = 10
+ when(/^fifteen$/)
+ num = 15
+ when(/^twenty$/)
+ num = 20
+ when(/^thirty$/)
+ num = 30
+ when(/^sixty$/)
+ num = 60
+ else
+ raise "invalid time string"
+ end
+ end
+ case unit
+ when (/^(s|sec(ond)?s?)$/)
+ return num
+ when (/^(m|min(ute)?s?)$/)
+ return num * 60
+ when (/^(h|h(ou)?rs?)$/)
+ return num * 60 * 60
+ when (/^(d|days?)$/)
+ return num * 60 * 60 * 24
+ else
+ raise "invalid time string"
+ end
+ when (/^(\d+):(\d+):(\d+)$/)
+ hour = $1.to_i
+ min = $2.to_i
+ sec = $3.to_i
+ now = Time.now
+ later = Time.mktime(now.year, now.month, now.day, hour, min, sec)
+ return later - now
+ when (/^(\d+):(\d+)$/)
+ hour = $1.to_i
+ min = $2.to_i
+ now = Time.now
+ later = Time.mktime(now.year, now.month, now.day, hour, min, now.sec)
+ return later - now
+ when (/^(\d+):(\d+)(am|pm)$/)
+ hour = $1.to_i
+ min = $2.to_i
+ ampm = $3
+ if ampm == "pm"
+ hour += 12
+ end
+ now = Time.now
+ later = Time.mktime(now.year, now.month, now.day, hour, min, now.sec)
+ return later - now
+ when (/^(\S+)$/)
+ num = 1
+ unit = $1
+ case unit
+ when (/^(s|sec(ond)?s?)$/)
+ return num
+ when (/^(m|min(ute)?s?)$/)
+ return num * 60
+ when (/^(h|h(ou)?rs?)$/)
+ return num * 60 * 60
+ when (/^(d|days?)$/)
+ return num * 60 * 60 * 24
+ else
+ raise "invalid time string"
+ end
+ else
+ raise "invalid time string"
+ end
+ end
+
+ # turn a number of seconds into a human readable string, e.g
+ # 2 days, 3 hours, 18 minutes, 10 seconds
+ def Utils.secs_to_string(secs)
+ ret = ""
+ days = (secs / (60 * 60 * 24)).to_i
+ secs = secs % (60 * 60 * 24)
+ hours = (secs / (60 * 60)).to_i
+ secs = (secs % (60 * 60))
+ mins = (secs / 60).to_i
+ secs = (secs % 60).to_i
+ ret += "#{days} days, " if days > 0
+ ret += "#{hours} hours, " if hours > 0 || days > 0
+ ret += "#{mins} minutes and " if mins > 0 || hours > 0 || days > 0
+ ret += "#{secs} seconds"
+ return ret
+ end
+
+ def Utils.safe_exec(command, *args)
+ IO.popen("-") {|p|
+ if(p)
+ return p.readlines.join("\n")
+ else
+ begin
+ $stderr = $stdout
+ exec(command, *args)
+ rescue Exception => e
+ puts "exec of #{command} led to exception: #{e}"
+ Kernel::exit! 0
+ end
+ puts "exec of #{command} failed"
+ Kernel::exit! 0
+ end
+ }
+ end
+
+ # returns a string containing the result of an HTTP GET on the uri
+ def Utils.http_get(uristr, readtimeout=8, opentimeout=4)
+
+ # ruby 1.7 or better needed for this (or 1.6 and debian unstable)
+ Net::HTTP.version_1_2
+ # (so we support the 1_1 api anyway, avoids problems)
+
+ uri = URI.parse uristr
+ query = uri.path
+ if uri.query
+ query += "?#{uri.query}"
+ end
+
+ proxy_host = nil
+ proxy_port = nil
+ if(ENV['http_proxy'] && proxy_uri = URI.parse(ENV['http_proxy']))
+ proxy_host = proxy_uri.host
+ proxy_port = proxy_uri.port
+ end
+
+ http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
+ http.open_timeout = opentimeout
+ http.read_timeout = readtimeout
+
+ http.start {|http|
+ begin
+ resp = http.get(query)
+ if resp.code == "200"
+ return resp.body
+ end
+ rescue => e
+ # cheesy for now
+ $stderr.puts "Utils.http_get exception: #{e}, while trying to get #{uristr}"
+ return nil
+ end
+ }
+ end
+
+ # This is nasty-ass. I hate writing parsers.
+ class Metar
+ attr_reader :decoded
+ attr_reader :input
+ attr_reader :date
+ attr_reader :nodata
+ def initialize(string)
+ str = nil
+ @nodata = false
+ string.each_line {|l|
+ if str == nil
+ # grab first line (date)
+ @date = l.chomp.strip
+ str = ""
+ else
+ if(str == "")
+ str = l.chomp.strip
+ else
+ str += " " + l.chomp.strip
+ end
+ end
+ }
+ if @date && @date =~ /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+)$/
+ # 2002/02/26 05:00
+ @date = Time.gm($1, $2, $3, $4, $5, 0)
+ else
+ @date = Time.now
+ end
+ @input = str.chomp
+ @cloud_layers = 0
+ @cloud_coverage = {
+ 'SKC' => '0',
+ 'CLR' => '0',
+ 'VV' => '8/8',
+ 'FEW' => '1/8 - 2/8',
+ 'SCT' => '3/8 - 4/8',
+ 'BKN' => '5/8 - 7/8',
+ 'OVC' => '8/8'
+ }
+ @wind_dir_texts = [
+ 'North',
+ 'North/Northeast',
+ 'Northeast',
+ 'East/Northeast',
+ 'East',
+ 'East/Southeast',
+ 'Southeast',
+ 'South/Southeast',
+ 'South',
+ 'South/Southwest',
+ 'Southwest',
+ 'West/Southwest',
+ 'West',
+ 'West/Northwest',
+ 'Northwest',
+ 'North/Northwest',
+ 'North'
+ ]
+ @wind_dir_texts_short = [
+ 'N',
+ 'N/NE',
+ 'NE',
+ 'E/NE',
+ 'E',
+ 'E/SE',
+ 'SE',
+ 'S/SE',
+ 'S',
+ 'S/SW',
+ 'SW',
+ 'W/SW',
+ 'W',
+ 'W/NW',
+ 'NW',
+ 'N/NW',
+ 'N'
+ ]
+ @weather_array = {
+ 'MI' => 'Mild ',
+ 'PR' => 'Partial ',
+ 'BC' => 'Patches ',
+ 'DR' => 'Low Drifting ',
+ 'BL' => 'Blowing ',
+ 'SH' => 'Shower(s) ',
+ 'TS' => 'Thunderstorm ',
+ 'FZ' => 'Freezing',
+ 'DZ' => 'Drizzle ',
+ 'RA' => 'Rain ',
+ 'SN' => 'Snow ',
+ 'SG' => 'Snow Grains ',
+ 'IC' => 'Ice Crystals ',
+ 'PE' => 'Ice Pellets ',
+ 'GR' => 'Hail ',
+ 'GS' => 'Small Hail and/or Snow Pellets ',
+ 'UP' => 'Unknown ',
+ 'BR' => 'Mist ',
+ 'FG' => 'Fog ',
+ 'FU' => 'Smoke ',
+ 'VA' => 'Volcanic Ash ',
+ 'DU' => 'Widespread Dust ',
+ 'SA' => 'Sand ',
+ 'HZ' => 'Haze ',
+ 'PY' => 'Spray',
+ 'PO' => 'Well-Developed Dust/Sand Whirls ',
+ 'SQ' => 'Squalls ',
+ 'FC' => 'Funnel Cloud Tornado Waterspout ',
+ 'SS' => 'Sandstorm/Duststorm '
+ }
+ @cloud_condition_array = {
+ 'SKC' => 'clear',
+ 'CLR' => 'clear',
+ 'VV' => 'vertical visibility',
+ 'FEW' => 'a few',
+ 'SCT' => 'scattered',
+ 'BKN' => 'broken',
+ 'OVC' => 'overcast'
+ }
+ @strings = {
+ 'mm_inches' => '%s mm (%s inches)',
+ 'precip_a_trace' => 'a trace',
+ 'precip_there_was' => 'There was %s of precipitation ',
+ 'sky_str_format1' => 'There were %s at a height of %s meters (%s feet)',
+ 'sky_str_clear' => 'The sky was clear',
+ 'sky_str_format2' => ', %s at a height of %s meter (%s feet) and %s at a height of %s meters (%s feet)',
+ 'sky_str_format3' => ' and %s at a height of %s meters (%s feet)',
+ 'clouds' => ' clouds',
+ 'clouds_cb' => ' cumulonimbus clouds',
+ 'clouds_tcu' => ' towering cumulus clouds',
+ 'visibility_format' => 'The visibility was %s kilometers (%s miles).',
+ 'wind_str_format1' => 'blowing at a speed of %s meters per second (%s miles per hour)',
+ 'wind_str_format2' => ', with gusts to %s meters per second (%s miles per hour),',
+ 'wind_str_format3' => ' from the %s',
+ 'wind_str_calm' => 'calm',
+ 'precip_last_hour' => 'in the last hour. ',
+ 'precip_last_6_hours' => 'in the last 3 to 6 hours. ',
+ 'precip_last_24_hours' => 'in the last 24 hours. ',
+ 'precip_snow' => 'There is %s mm (%s inches) of snow on the ground. ',
+ '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).',
+ 'temp_max_6_hours' => 'The maximum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
+ 'temp_min_6_hours' => 'The minimum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
+ '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). ',
+ 'light' => 'Light ',
+ 'moderate' => 'Moderate ',
+ 'heavy' => 'Heavy ',
+ 'mild' => 'Mild ',
+ 'nearby' => 'Nearby ',
+ 'current_weather' => 'Current weather is %s. ',
+ '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'
+ }
+
+ parse
+ end
+
+ def store_speed(value, windunit, meterspersec, knots, milesperhour)
+ # Helper function to convert and store speed based on unit.
+ # &$meterspersec, &$knots and &$milesperhour are passed on
+ # reference
+ if (windunit == 'KT')
+ # The windspeed measured in knots:
+ @decoded[knots] = sprintf("%.2f", value)
+ # The windspeed measured in meters per second, rounded to one decimal place:
+ @decoded[meterspersec] = sprintf("%.2f", value.to_f * 0.51444)
+ # The windspeed measured in miles per hour, rounded to one decimal place: */
+ @decoded[milesperhour] = sprintf("%.2f", value.to_f * 1.1507695060844667)
+ elsif (windunit == 'MPS')
+ # The windspeed measured in meters per second:
+ @decoded[meterspersec] = sprintf("%.2f", value)
+ # The windspeed measured in knots, rounded to one decimal place:
+ @decoded[knots] = sprintf("%.2f", value.to_f / 0.51444)
+ #The windspeed measured in miles per hour, rounded to one decimal place:
+ @decoded[milesperhour] = sprintf("%.1f", value.to_f / 0.51444 * 1.1507695060844667)
+ elsif (windunit == 'KMH')
+ # The windspeed measured in kilometers per hour:
+ @decoded[meterspersec] = sprintf("%.1f", value.to_f * 1000 / 3600)
+ @decoded[knots] = sprintf("%.1f", value.to_f * 1000 / 3600 / 0.51444)
+ # The windspeed measured in miles per hour, rounded to one decimal place:
+ @decoded[milesperhour] = sprintf("%.1f", knots.to_f * 1.1507695060844667)
+ end
+ end
+
+ def parse
+ @decoded = Hash.new
+ puts @input
+ @input.split(" ").each {|part|
+ if (part == 'METAR')
+ # Type of Report: METAR
+ @decoded['type'] = 'METAR'
+ elsif (part == 'SPECI')
+ # Type of Report: SPECI
+ @decoded['type'] = 'SPECI'
+ elsif (part == 'AUTO')
+ # Report Modifier: AUTO
+ @decoded['report_mod'] = 'AUTO'
+ elsif (part == 'NIL')
+ @nodata = true
+ elsif (part =~ /^\S{4}$/ && ! (@decoded.has_key?('station')))
+ # Station Identifier
+ @decoded['station'] = part
+ elsif (part =~ /([0-9]{2})([0-9]{2})([0-9]{2})Z/)
+ # ignore this bit, it's useless without month/year. some of these
+ # things are hideously out of date.
+ # now = Time.new
+ # time = Time.gm(now.year, now.month, $1, $2, $3, 0)
+ # Date and Time of Report
+ # @decoded['time'] = time
+ elsif (part == 'COR')
+ # Report Modifier: COR
+ @decoded['report_mod'] = 'COR'
+ elsif (part =~ /([0-9]{3}|VRB)([0-9]{2,3}).*(KT|MPS|KMH)/)
+ # Wind Group
+ windunit = $3
+ # now do ereg to get the actual values
+ part =~ /([0-9]{3}|VRB)([0-9]{2,3})((G[0-9]{2,3})?#{windunit})/
+ if ($1 == 'VRB')
+ @decoded['wind_deg'] = 'variable directions'
+ @decoded['wind_dir_text'] = 'variable directions'
+ @decoded['wind_dir_text_short'] = 'VAR'
+ else
+ @decoded['wind_deg'] = $1
+ @decoded['wind_dir_text'] = @wind_dir_texts[($1.to_i/22.5).round]
+ @decoded['wind_dir_text_short'] = @wind_dir_texts_short[($1.to_i/22.5).round]
+ end
+ store_speed($2, windunit,
+ 'wind_meters_per_second',
+ 'wind_knots',
+ 'wind_miles_per_hour')
+
+ if ($4 != nil)
+ # We have a report with information about the gust.
+ # First we have the gust measured in knots
+ if ($4 =~ /G([0-9]{2,3})/)
+ store_speed($1,windunit,
+ 'wind_gust_meters_per_second',
+ 'wind_gust_knots',
+ 'wind_gust_miles_per_hour')
+ end
+ end
+ elsif (part =~ /([0-9]{3})V([0-9]{3})/)
+ # Variable wind-direction
+ @decoded['wind_var_beg'] = $1
+ @decoded['wind_var_end'] = $2
+ elsif (part == "9999")
+ # A strange value. When you look at other pages you see it
+ # interpreted like this (where I use > to signify 'Greater
+ # than'):
+ @decoded['visibility_miles'] = '>7';
+ @decoded['visibility_km'] = '>11.3';
+ elsif (part =~ /^([0-9]{4})$/)
+ # Visibility in meters (4 digits only)
+ # The visibility measured in kilometers, rounded to one decimal place.
+ @decoded['visibility_km'] = sprintf("%.1f", $1.to_i / 1000)
+ # The visibility measured in miles, rounded to one decimal place.
+ @decoded['visibility_miles'] = sprintf("%.1f", $1.to_i / 1000 / 1.609344)
+ elsif (part =~ /^[0-9]$/)
+ # Temp Visibility Group, single digit followed by space
+ @decoded['temp_visibility_miles'] = part
+ elsif (@decoded['temp_visibility_miles'] && (@decoded['temp_visibility_miles']+' '+part) =~ /^M?(([0-9]?)[ ]?([0-9])(\/?)([0-9]*))SM$/)
+ # Visibility Group
+ if ($4 == '/')
+ vis_miles = $2.to_i + $3.to_i/$5.to_i
+ else
+ vis_miles = $1.to_i;
+ end
+ if (@decoded['temp_visibility_miles'][0] == 'M')
+ # The visibility measured in miles, prefixed with < to indicate 'Less than'
+ @decoded['visibility_miles'] = '<' + sprintf("%.1f", vis_miles)
+ # The visibility measured in kilometers. The value is rounded
+ # to one decimal place, prefixed with < to indicate 'Less than' */
+ @decoded['visibility_km'] = '<' . sprintf("%.1f", vis_miles * 1.609344)
+ else
+ # The visibility measured in mile.s */
+ @decoded['visibility_miles'] = sprintf("%.1f", vis_miles)
+ # The visibility measured in kilometers, rounded to one decimal place.
+ @decoded['visibility_km'] = sprintf("%.1f", vis_miles * 1.609344)
+ end
+ 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)+$/)
+ # Current weather-group
+ @decoded['weather'] = '' unless @decoded.has_key?('weather')
+ if (part[0].chr == '-')
+ # A light phenomenon
+ @decoded['weather'] += @strings['light']
+ part = part[1,part.length]
+ elsif (part[0].chr == '+')
+ # A heavy phenomenon
+ @decoded['weather'] += @strings['heavy']
+ part = part[1,part.length]
+ elsif (part[0,2] == 'VC')
+ # Proximity Qualifier
+ @decoded['weather'] += @strings['nearby']
+ part = part[2,part.length]
+ elsif (part[0,2] == 'MI')
+ @decoded['weather'] += @strings['mild']
+ part = part[2,part.length]
+ else
+ # no intensity code => moderate phenomenon
+ @decoded['weather'] += @strings['moderate']
+ end
+
+ while (part && bite = part[0,2]) do
+ # Now we take the first two letters and determine what they
+ # mean. We append this to the variable so that we gradually
+ # build up a phrase.
+
+ @decoded['weather'] += @weather_array[bite]
+ # Here we chop off the two first letters, so that we can take
+ # a new bite at top of the while-loop.
+ part = part[2,-1]
+ end
+ elsif (part =~ /(SKC|CLR)/)
+ # Cloud-layer-group.
+ # There can be up to three of these groups, so we store them as
+ # cloud_layer1, cloud_layer2 and cloud_layer3.
+
+ @cloud_layers += 1;
+ # Again we have to translate the code-characters to a
+ # meaningful string.
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = @cloud_condition_array[$1]
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
+ elsif (part =~ /^(VV|FEW|SCT|BKN|OVC)([0-9]{3})(CB|TCU)?$/)
+ # We have found (another) a cloud-layer-group. There can be up
+ # to three of these groups, so we store them as cloud_layer1,
+ # cloud_layer2 and cloud_layer3.
+ @cloud_layers += 1;
+ # Again we have to translate the code-characters to a meaningful string.
+ if ($3 == 'CB')
+ # cumulonimbus (CB) clouds were observed. */
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
+ @cloud_condition_array[$1] + @strings['clouds_cb']
+ elsif ($3 == 'TCU')
+ # towering cumulus (TCU) clouds were observed.
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
+ @cloud_condition_array[$1] + @strings['clouds_tcu']
+ else
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
+ @cloud_condition_array[$1] + @strings['clouds']
+ end
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_ft'] = $2.to_i * 100
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_m'] = ($2.to_f * 30.48).round
+ elsif (part =~ /^T([0-9]{4})$/)
+ store_temp($1,'temp_c','temp_f')
+ elsif (part =~ /^T?(M?[0-9]{2})\/(M?[0-9\/]{1,2})?$/)
+ # Temperature/Dew Point Group
+ # The temperature and dew-point measured in Celsius.
+ @decoded['temp_c'] = sprintf("%d", $1.tr('M', '-'))
+ if $2 == "//" || !$2
+ @decoded['dew_c'] = 0
+ else
+ @decoded['dew_c'] = sprintf("%.1f", $2.tr('M', '-'))
+ end
+ # The temperature and dew-point measured in Fahrenheit, rounded to
+ # the nearest degree.
+ @decoded['temp_f'] = ((@decoded['temp_c'].to_f * 9 / 5) + 32).round
+ @decoded['dew_f'] = ((@decoded['dew_c'].to_f * 9 / 5) + 32).round
+ elsif(part =~ /A([0-9]{4})/)
+ # Altimeter
+ # The pressure measured in inHg
+ @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_i/100)
+ # The pressure measured in mmHg, hPa and atm
+ @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.254)
+ @decoded['altimeter_hpa'] = sprintf("%d", ($1.to_f * 0.33863881578947).to_i)
+ @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 3.3421052631579e-4)
+ elsif(part =~ /Q([0-9]{4})/)
+ # Altimeter
+ # This is strange, the specification doesnt say anything about
+ # the Qxxxx-form, but it's in the METARs.
+ # The pressure measured in hPa
+ @decoded['altimeter_hpa'] = sprintf("%d", $1.to_i)
+ # The pressure measured in mmHg, inHg and atm
+ @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.7500616827)
+ @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_f * 0.0295299875)
+ @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 9.869232667e-4)
+ elsif (part =~ /^T([0-9]{4})([0-9]{4})/)
+ # Temperature/Dew Point Group, coded to tenth of degree.
+ # The temperature and dew-point measured in Celsius.
+ store_temp($1,'temp_c','temp_f')
+ store_temp($2,'dew_c','dew_f')
+ elsif (part =~ /^1([0-9]{4}$)/)
+ # 6 hour maximum temperature Celsius, coded to tenth of degree
+ store_temp($1,'temp_max6h_c','temp_max6h_f')
+ elsif (part =~ /^2([0-9]{4}$)/)
+ # 6 hour minimum temperature Celsius, coded to tenth of degree
+ store_temp($1,'temp_min6h_c','temp_min6h_f')
+ elsif (part =~ /^4([0-9]{4})([0-9]{4})$/)
+ # 24 hour maximum and minimum temperature Celsius, coded to
+ # tenth of degree
+ store_temp($1,'temp_max24h_c','temp_max24h_f')
+ store_temp($2,'temp_min24h_c','temp_min24h_f')
+ elsif (part =~ /^P([0-9]{4})/)
+ # Precipitation during last hour in hundredths of an inch
+ # (store as inches)
+ @decoded['precip_in'] = sprintf("%.2f", $1.to_f/100)
+ @decoded['precip_mm'] = sprintf("%.2f", $1.to_f * 0.254)
+ elsif (part =~ /^6([0-9]{4})/)
+ # Precipitation during last 3 or 6 hours in hundredths of an
+ # inch (store as inches)
+ @decoded['precip_6h_in'] = sprintf("%.2f", $1.to_f/100)
+ @decoded['precip_6h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
+ elsif (part =~ /^7([0-9]{4})/)
+ # Precipitation during last 24 hours in hundredths of an inch
+ # (store as inches)
+ @decoded['precip_24h_in'] = sprintf("%.2f", $1.to_f/100)
+ @decoded['precip_24h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
+ elsif(part =~ /^4\/([0-9]{3})/)
+ # Snow depth in inches
+ @decoded['snow_in'] = sprintf("%.2f", $1);
+ @decoded['snow_mm'] = sprintf("%.2f", $1.to_f * 25.4)
+ else
+ # If we couldn't match the group, we assume that it was a
+ # remark.
+ @decoded['remarks'] = '' unless @decoded.has_key?("remarks")
+ @decoded['remarks'] += ' ' + part;
+ end
+ }
+
+ # Relative humidity
+ # p @decoded['dew_c'] # 11.0
+ # p @decoded['temp_c'] # 21.0
+ # => 56.1
+ @decoded['rel_humidity'] = sprintf("%.1f",100 *
+ (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')
+ end
+
+ def store_temp(temp,temp_cname,temp_fname)
+ # Given a numerical temperature temp in Celsius, coded to tenth of
+ # degree, store in @decoded[temp_cname], convert to Fahrenheit
+ # and store in @decoded[temp_fname]
+ # Note: temp is converted to negative if temp > 100.0 (See
+ # Federal Meteorological Handbook for groups T, 1, 2 and 4)
+
+ # Temperature measured in Celsius, coded to tenth of degree
+ temp = temp.to_f/10
+ if (temp >100.0)
+ # first digit = 1 means minus temperature
+ temp = -(temp - 100.0)
+ end
+ @decoded[temp_cname] = sprintf("%.1f", temp)
+ # The temperature in Fahrenheit.
+ @decoded[temp_fname] = sprintf("%.1f", (temp * 9 / 5) + 32)
+ end
+
+ def pretty_print_precip(precip_mm, precip_in)
+ # Returns amount if $precip_mm > 0, otherwise "trace" (see Federal
+ # Meteorological Handbook No. 1 for code groups P, 6 and 7) used in
+ # several places, so standardized in one function.
+ if (precip_mm.to_i > 0)
+ amount = sprintf(@strings['mm_inches'], precip_mm, precip_in)
+ else
+ amount = @strings['a_trace']
+ end
+ return sprintf(@strings['precip_there_was'], amount)
+ end
+
+ def pretty_print
+ if @nodata
+ return "The weather stored for #{@decoded['station']} consists of the string 'NIL' :("
+ end
+
+ ["temp_c", "altimeter_hpa"].each {|key|
+ if !@decoded.has_key?(key)
+ return "The weather stored for #{@decoded['station']} could not be parsed (#{@input})"
+ end
+ }
+
+ mins_old = ((Time.now - @date.to_i).to_f/60).round
+ if (mins_old <= 60)
+ weather_age = mins_old.to_s + " minutes ago,"
+ elsif (mins_old <= 60 * 25)
+ weather_age = (mins_old / 60).to_s + " hours, "
+ weather_age += (mins_old % 60).to_s + " minutes ago,"
+ else
+ # return "The weather stored for #{@decoded['station']} is hideously out of date :( (Last update #{@date})"
+ weather_age = "The weather stored for #{@decoded['station']} is hideously out of date :( here it is anyway:"
+ end
+
+ if(@decoded.has_key?("cloud_layer1_altitude_ft"))
+ sky_str = sprintf(@strings['sky_str_format1'],
+ @decoded["cloud_layer1_condition"],
+ @decoded["cloud_layer1_altitude_m"],
+ @decoded["cloud_layer1_altitude_ft"])
+ else
+ sky_str = @strings['sky_str_clear']
+ end
+
+ if(@decoded.has_key?("cloud_layer2_altitude_ft"))
+ if(@decoded.has_key?("cloud_layer3_altitude_ft"))
+ sky_str += sprintf(@strings['sky_str_format2'],
+ @decoded["cloud_layer2_condition"],
+ @decoded["cloud_layer2_altitude_m"],
+ @decoded["cloud_layer2_altitude_ft"],
+ @decoded["cloud_layer3_condition"],
+ @decoded["cloud_layer3_altitude_m"],
+ @decoded["cloud_layer3_altitude_ft"])
+ else
+ sky_str += sprintf(@strings['sky_str_format3'],
+ @decoded["cloud_layer2_condition"],
+ @decoded["cloud_layer2_altitude_m"],
+ @decoded["cloud_layer2_altitude_ft"])
+ end
+ end
+ sky_str += "."
+
+ if(@decoded.has_key?("visibility_miles"))
+ visibility = sprintf(@strings['visibility_format'],
+ @decoded["visibility_km"],
+ @decoded["visibility_miles"])
+ else
+ visibility = ""
+ end
+
+ if (@decoded.has_key?("wind_meters_per_second") && @decoded["wind_meters_per_second"].to_i > 0)
+ wind_str = sprintf(@strings['wind_str_format1'],
+ @decoded["wind_meters_per_second"],
+ @decoded["wind_miles_per_hour"])
+ if (@decoded.has_key?("wind_gust_meters_per_second") && @decoded["wind_gust_meters_per_second"].to_i > 0)
+ wind_str += sprintf(@strings['wind_str_format2'],
+ @decoded["wind_gust_meters_per_second"],
+ @decoded["wind_gust_miles_per_hour"])
+ end
+ wind_str += sprintf(@strings['wind_str_format3'],
+ @decoded["wind_dir_text"])
+ else
+ wind_str = @strings['wind_str_calm']
+ end
+
+ prec_str = ""
+ if (@decoded.has_key?("precip_in"))
+ prec_str += pretty_print_precip(@decoded["precip_mm"], @decoded["precip_in"]) + @strings['precip_last_hour']
+ end
+ if (@decoded.has_key?("precip_6h_in"))
+ prec_str += pretty_print_precip(@decoded["precip_6h_mm"], @decoded["precip_6h_in"]) + @strings['precip_last_6_hours']
+ end
+ if (@decoded.has_key?("precip_24h_in"))
+ prec_str += pretty_print_precip(@decoded["precip_24h_mm"], @decoded["precip_24h_in"]) + @strings['precip_last_24_hours']
+ end
+ if (@decoded.has_key?("snow_in"))
+ prec_str += sprintf(@strings['precip_snow'], @decoded["snow_mm"], @decoded["snow_in"])
+ end
+
+ temp_str = ""
+ if (@decoded.has_key?("temp_max6h_c") && @decoded.has_key?("temp_min6h_c"))
+ temp_str += sprintf(@strings['temp_min_max_6_hours'],
+ @decoded["temp_max6h_c"],
+ @decoded["temp_min6h_c"],
+ @decoded["temp_max6h_f"],
+ @decoded["temp_min6h_f"])
+ else
+ if (@decoded.has_key?("temp_max6h_c"))
+ temp_str += sprintf(@strings['temp_max_6_hours'],
+ @decoded["temp_max6h_c"],
+ @decoded["temp_max6h_f"])
+ end
+ if (@decoded.has_key?("temp_min6h_c"))
+ temp_str += sprintf(@strings['temp_max_6_hours'],
+ @decoded["temp_min6h_c"],
+ @decoded["temp_min6h_f"])
+ end
+ end
+ if (@decoded.has_key?("temp_max24h_c"))
+ temp_str += sprintf(@strings['temp_min_max_24_hours'],
+ @decoded["temp_max24h_c"],
+ @decoded["temp_min24h_c"],
+ @decoded["temp_max24h_f"],
+ @decoded["temp_min24h_f"])
+ end
+
+ if (@decoded.has_key?("weather"))
+ weather_str = sprintf(@strings['current_weather'], @decoded["weather"])
+ else
+ weather_str = ''
+ end
+
+ return sprintf(@strings['pretty_print_metar'],
+ weather_age,
+ @date,
+ wind_str, @decoded["station"], @decoded["temp_c"],
+ @decoded["temp_f"], @decoded["altimeter_hpa"],
+ @decoded["altimeter_inhg"],
+ @decoded["rel_humidity"], sky_str,
+ visibility, weather_str, prec_str, temp_str).strip
+ end
+
+ def to_s
+ @input
+ end
+ end
+
+ def Utils.get_metar(station)
+ station.upcase!
+
+ result = Utils.http_get("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT")
+ return nil unless result
+ return Metar.new(result)
+ end
+ end
+end