davidjrice-hamweather 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +13 -0
- data/README +73 -0
- data/Rakefile +40 -0
- data/hamweather.gemspec +30 -0
- data/lib/hamweather.rb +44 -0
- data/lib/hamweather/forecast.rb +54 -0
- data/lib/hamweather/forecast/daily.rb +25 -0
- data/lib/hamweather/forecast/hourly.rb +27 -0
- data/lib/hamweather/location.rb +91 -0
- data/spec/daily_spec.rb +56 -0
- data/spec/forecast_spec.rb +52 -0
- data/spec/hamweather_spec.rb +37 -0
- data/spec/hourly_spec.rb +73 -0
- data/spec/location_spec.rb +77 -0
- data/spec/spec_helper.rb +11 -0
- metadata +86 -0
data/Manifest
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
lib/hamweather/forecast/daily.rb
|
2
|
+
lib/hamweather/forecast/hourly.rb
|
3
|
+
lib/hamweather/forecast.rb
|
4
|
+
lib/hamweather/location.rb
|
5
|
+
lib/hamweather.rb
|
6
|
+
Rakefile
|
7
|
+
sample_data.xml
|
8
|
+
spec/daily_spec.rb
|
9
|
+
spec/forecast_spec.rb
|
10
|
+
spec/hamweather_spec.rb
|
11
|
+
spec/hourly_spec.rb
|
12
|
+
spec/spec_helper.rb
|
13
|
+
Manifest
|
data/README
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
Hamweather
|
2
|
+
==========
|
3
|
+
|
4
|
+
This is a Ruby API to an XML webservice providing weather detail for global locations.
|
5
|
+
It can handle diverse locations by Zip Code, Canadian Post code or by any international address.
|
6
|
+
|
7
|
+
* It requires a Google Maps API key: http://code.google.com/apis/maps/signup.html
|
8
|
+
* It requires a Hamweather API key, contact Hamweather to discuss: http://www.hamweather.com/
|
9
|
+
|
10
|
+
Installation
|
11
|
+
------------
|
12
|
+
> sudo gem install hamweather
|
13
|
+
|
14
|
+
Usage
|
15
|
+
-----
|
16
|
+
require 'rubygems'
|
17
|
+
require 'hamweather'
|
18
|
+
|
19
|
+
*** Hamweather is a singleton class, it takes the two API keys as such:
|
20
|
+
Hamweather.google_maps_api_key = " ... "
|
21
|
+
Hamweather.api_key = " ... "
|
22
|
+
|
23
|
+
@location = Hamweather.locate('Belfast') # Assuming your location is unambiguous:
|
24
|
+
@forecast = Hamweather.forecast(@location) # Forecast is returned as a Hamweather::Forecast object
|
25
|
+
|
26
|
+
What 'address' to use?
|
27
|
+
----------------------
|
28
|
+
Zip Code, Canadian Postcode, International Address - it doesn't really matter.
|
29
|
+
This API uses the Google Maps API to geolocate any address which doesn't fit either of the former
|
30
|
+
precisely.
|
31
|
+
Which means the user could input "Lodnon" and get the right location as it's parsing is intelligent.
|
32
|
+
|
33
|
+
Using Hamweather::Location
|
34
|
+
--------------------------
|
35
|
+
If the 'address' you enter is ambiguous, the API may return a hash of Location objects which you can
|
36
|
+
use to choose. Check this by: @location.kind_of?(Hash)
|
37
|
+
Otherwise @location.zipcode @location.postcode? @location.geocode? will tell you what type of
|
38
|
+
Location you have.
|
39
|
+
|
40
|
+
Using Hamweather::Forecast
|
41
|
+
--------------------------
|
42
|
+
Assuming @location has been defined, @forecast = Hamweather.forecast(@location) will return a
|
43
|
+
Hamweather::Forecast object. A Forecast object has two available hashes: dailies{} and hourlies{}
|
44
|
+
which give all available Daily forecasts and Hourly forecasts respectively. Each Daily or Hourly
|
45
|
+
forecast is a Hash of Hamweather::Forecast::Daily or Hamweather::Forecast::Hourly objects.
|
46
|
+
|
47
|
+
The Daily object has a list of it's Hourly Forecasts available to you: 'hours'
|
48
|
+
Forecast variables available in a Daily forecast are:
|
49
|
+
:high_farenheit
|
50
|
+
:high_celsius
|
51
|
+
:low_farenheit
|
52
|
+
:low_celsius
|
53
|
+
:day
|
54
|
+
:date
|
55
|
+
:expected_weather
|
56
|
+
:detail
|
57
|
+
:probability_of_preciptiation
|
58
|
+
|
59
|
+
Forecast variables available in an Hourly forecast are:
|
60
|
+
:date
|
61
|
+
:time
|
62
|
+
:expected_weather
|
63
|
+
:temp_celsius
|
64
|
+
:temp_farenheit
|
65
|
+
:probability_of_precipitation
|
66
|
+
:precipitation_millimeters
|
67
|
+
:precipitation_inches
|
68
|
+
:dew_point_celsius
|
69
|
+
:dew_point_farenheit
|
70
|
+
:relative_humidity
|
71
|
+
:wind_speed_knots
|
72
|
+
:wind_speed_mph
|
73
|
+
:wind_direction
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require 'echoe'
|
3
|
+
|
4
|
+
Echoe.new('hamweather', '0.0.1') do |p|
|
5
|
+
p.rubyforge_name = 'hamweather'
|
6
|
+
p.summary = "Hamweather is a Ruby client library for interacting with http://hamweather.com weather data"
|
7
|
+
p.description = "Hamweather is a Ruby client library for interacting with http://hamweather.com weather data"
|
8
|
+
p.url = "http://github.com/davidjrice/hamweather"
|
9
|
+
p.author = ['David Rice']
|
10
|
+
p.email = "david@contrast.ie"
|
11
|
+
p.dependencies = ["json"]
|
12
|
+
end
|
13
|
+
|
14
|
+
rescue LoadError => e
|
15
|
+
puts "You are missing a dependency required for meta-operations on this gem."
|
16
|
+
puts "#{e.to_s.capitalize}."
|
17
|
+
end
|
18
|
+
|
19
|
+
# add spec tasks, if you have rspec installed
|
20
|
+
begin
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
|
23
|
+
Spec::Rake::SpecTask.new("spec") do |t|
|
24
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
25
|
+
t.spec_opts = ['--color']
|
26
|
+
end
|
27
|
+
|
28
|
+
task :test do
|
29
|
+
Rake::Task['spec'].invoke
|
30
|
+
end
|
31
|
+
|
32
|
+
Spec::Rake::SpecTask.new("coverage") do |t|
|
33
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
34
|
+
t.spec_opts = ['--color']
|
35
|
+
t.rcov = true
|
36
|
+
t.rcov_opts = ['--exclude', '^spec,/gems/']
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
end
|
data/hamweather.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "hamweather"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.date = "2008-10-13"
|
5
|
+
s.summary = " Ruby Client API for Hamweather Weather Service (http://www.hamweather.com)"
|
6
|
+
s.email = "david@contrast.ie"
|
7
|
+
s.homepage = "http://github.com/davidjrice/hamweather"
|
8
|
+
s.description = "Client API for Hamweather Weather Service (http://www.hamweather.com)"
|
9
|
+
s.has_rdoc = true
|
10
|
+
s.authors = ["David Rice", "David Lowry"]
|
11
|
+
s.files = ["Manifest",
|
12
|
+
"README",
|
13
|
+
"Rakefile",
|
14
|
+
"hamweather.gemspec",
|
15
|
+
"lib/hamweather/forecast/daily.rb",
|
16
|
+
"lib/hamweather/forecast/hourly.rb",
|
17
|
+
"lib/hamweather/forecast.rb",
|
18
|
+
"lib/hamweather/location.rb",
|
19
|
+
"lib/hamweather.rb"]
|
20
|
+
s.test_files = ["spec/daily_spec.rb",
|
21
|
+
"spec/forecast_spec.rb",
|
22
|
+
"spec/hamweather_spec.rb",
|
23
|
+
"spec/hourly_spec.rb",
|
24
|
+
"spec/location_spec.rb",
|
25
|
+
"spec/spec_helper.rb"]
|
26
|
+
s.rdoc_options = ["--main", "README"]
|
27
|
+
s.extra_rdoc_files = ["Manifest", "README"]
|
28
|
+
s.add_dependency("google-geo", ["> 0.0.0"])
|
29
|
+
s.add_dependency("hpricot", ["> 0.0.0"])
|
30
|
+
end
|
data/lib/hamweather.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
require 'hamweather/location'
|
3
|
+
require 'hamweather/forecast'
|
4
|
+
require 'hamweather/forecast/daily'
|
5
|
+
require 'hamweather/forecast/hourly'
|
6
|
+
|
7
|
+
require 'date'
|
8
|
+
require 'hpricot'
|
9
|
+
|
10
|
+
module Hamweather
|
11
|
+
|
12
|
+
class ApiKeyException < StandardError; end
|
13
|
+
class GoogleApiKeyException < StandardError; end
|
14
|
+
|
15
|
+
class UnknownAddressError < StandardError; end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
attr_accessor :api_key, :google_maps_api_key
|
20
|
+
# Hamweather.api_key = "..."
|
21
|
+
# Hamweather.google_maps_api_key = "..."
|
22
|
+
# locations = Hamweather.locate "Belfast"
|
23
|
+
# Hamweather.forecast(locations.first)
|
24
|
+
|
25
|
+
def locate(place)
|
26
|
+
check_api_key
|
27
|
+
@location = Hamweather::Location.parse(place)
|
28
|
+
end
|
29
|
+
|
30
|
+
def forecast(location)
|
31
|
+
check_api_key
|
32
|
+
Hamweather::Forecast.new(location)
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def check_api_key
|
38
|
+
raise ApiKeyException.new("you must provide a Hamweather api key.") if api_key.nil?
|
39
|
+
raise ApiKeyException.new("you must provide a Google Maps api key.") if google_maps_api_key.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Hamweather
|
5
|
+
class Forecast
|
6
|
+
|
7
|
+
# @forecast.each_day
|
8
|
+
# => day_proxy_object.high_farenheit
|
9
|
+
# @forecast.each_hour
|
10
|
+
# => hour_proxy_object.high_farenheit
|
11
|
+
|
12
|
+
attr_accessor :forecast, :dailies, :hourlies
|
13
|
+
|
14
|
+
def initialize(location)
|
15
|
+
# Construct url from api key, location uri
|
16
|
+
# Request url, get xml then parse
|
17
|
+
xml_data = self.class.fetch(location.to_uri).gsub!(/\n/,'')
|
18
|
+
data = Hpricot.parse(xml_data)
|
19
|
+
|
20
|
+
@dailies = {}
|
21
|
+
@hourlies = {}
|
22
|
+
|
23
|
+
data.at(:wxforecast).children.each do |day|
|
24
|
+
#TODO make ordered hash
|
25
|
+
@dailies[day[:date].to_s] = Daily.new(day)
|
26
|
+
end
|
27
|
+
|
28
|
+
data.at(:wxshortterm).children.each do |hour|
|
29
|
+
#TODO make ordered hash
|
30
|
+
@hourlies[hour[:time]] = Hourly.new(hour)
|
31
|
+
@dailies[hour[:date]].hours[hour[:time]] = Hourly.new(hour)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO stub out calls to this where possible.
|
36
|
+
# TODO / or raise an error.
|
37
|
+
def self.fetch(location_uri)
|
38
|
+
Net::HTTP.get URI.parse("http://hwlite.hamweather.net/#{Hamweather.api_key}/#{location_uri}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def each_day
|
42
|
+
@dailies.each_pair do |key, value|
|
43
|
+
yield value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def each_hour
|
48
|
+
@hourlies.each_pair do |key, value|
|
49
|
+
yield value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hamweather
|
2
|
+
class Forecast::Daily
|
3
|
+
#daily_forecast = Hpricot.parse('<FPeriod interval="1" Day="FRI" Date="2008-11-14" Wx="Chance T-storms" Icon="tstorm.gif" HiF="66" HiC="19" LoF="56" LoC="13" Pop="60" Detail="Occasional showers with a chance of thunderstorms. Areas of fog. Some thunderstorms May be severe after midnight. Lows in the mid 50s. South winds 5 to 10 mph shifting to the southwest 10 to 15 mph after midnight. Chance of rain near 100 percent."/>').root
|
4
|
+
attr_accessor :high_farenheit, :high_celsius, :low_farenheit, :low_celsius, :day, :date, :expected_weather, :detail, :probability_of_preciptiation
|
5
|
+
attr_accessor :hours
|
6
|
+
|
7
|
+
def initialize(daily_forecast)
|
8
|
+
@hours = {}
|
9
|
+
@high_farenheit = daily_forecast[:hif].to_i
|
10
|
+
@high_celsius = daily_forecast[:hic].to_i
|
11
|
+
@low_farenheit = daily_forecast[:lof].to_i
|
12
|
+
@low_celsius = daily_forecast[:loc].to_i
|
13
|
+
@date = Date.parse(daily_forecast[:date])
|
14
|
+
@expected_weather = daily_forecast[:wx]
|
15
|
+
@detail = daily_forecast[:detail]
|
16
|
+
@probability_of_preciptiation = daily_forecast[:pop].to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def each_hour
|
20
|
+
@hours.each_pair do |key, value|
|
21
|
+
yield value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Hamweather
|
2
|
+
class Forecast::Hourly
|
3
|
+
#hourly_forecast = Hpricot.parse('<STPeriod interval="1" Epoch="1228298400" Day="Wed" Date="2008-12-03" Time="10:00" Wx="Partly Cloudy" Icon="pcloudy.gif" TempC="0" TempF="32" Pop="10" QPFmm="" QPFin="" DewPointC="-5" DewPointF="23" RelativeHumidity="70" WindSpeedKnots="8" WindSpeedMPH="9" WindDirectionEng="SW" />').root
|
4
|
+
attr_accessor :date, :time, :expected_weather, :temp_celsius, :temp_farenheit, :probability_of_precipitation, :precipitation_millimeters, :precipitation_inches, :dew_point_celsius, :dew_point_farenheit, :relative_humidity, :wind_speed_knots, :wind_speed_mph, :wind_direction
|
5
|
+
|
6
|
+
# def initialize(xml_data)
|
7
|
+
# hourly_forecast = Hpricot.parse(xml_data).root
|
8
|
+
|
9
|
+
def initialize(hourly_forecast)
|
10
|
+
@date = Date.parse(hourly_forecast[:date])
|
11
|
+
@time = hourly_forecast[:time]
|
12
|
+
@expected_weather = hourly_forecast[:wx]
|
13
|
+
@temp_farenheit = hourly_forecast[:tempf].to_i
|
14
|
+
@temp_celsius = hourly_forecast[:tempc].to_i
|
15
|
+
@probability_of_precipitation = hourly_forecast[:pop].to_i
|
16
|
+
@precipitation_millimeters = hourly_forecast[:qpfmm]
|
17
|
+
@precipitation_inches = hourly_forecast[:qpfin]
|
18
|
+
@dew_point_celsius = hourly_forecast[:dewpointc].to_i
|
19
|
+
@dew_point_farenheit = hourly_forecast[:dewpointf].to_i
|
20
|
+
@relative_humidity = hourly_forecast[:relativehumidity].to_i
|
21
|
+
@wind_speed_knots = hourly_forecast[:windspeedknots].to_i
|
22
|
+
@wind_speed_mph = hourly_forecast[:windspeedmph].to_i
|
23
|
+
@wind_direction = hourly_forecast[:winddirectioneng]
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'google/geo'
|
3
|
+
|
4
|
+
module Hamweather
|
5
|
+
|
6
|
+
class Location
|
7
|
+
|
8
|
+
attr_reader :string, :address
|
9
|
+
|
10
|
+
def lat
|
11
|
+
@address.latitude
|
12
|
+
end
|
13
|
+
|
14
|
+
def lon
|
15
|
+
@address.longitude
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_uri
|
19
|
+
if zipcode? || postcode?
|
20
|
+
@uri ||= "wx/#{string}.xml"
|
21
|
+
elsif geocode?
|
22
|
+
@uri ||= "wx/nearby.xml?lat=#{lat}&lon=#{lon}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def zipcode?
|
27
|
+
@kind == :zipcode
|
28
|
+
end
|
29
|
+
|
30
|
+
def postcode?
|
31
|
+
@kind == :postcode
|
32
|
+
end
|
33
|
+
|
34
|
+
def geocode?
|
35
|
+
@kind == :geocode
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.parse(string)
|
39
|
+
if is_zipcode?(string) || is_canadian_postcode?(string)
|
40
|
+
return self.new(string)
|
41
|
+
else
|
42
|
+
geocode(string)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(object)
|
47
|
+
if self.class.is_zipcode?(object)
|
48
|
+
@string = object
|
49
|
+
@kind = :zipcode
|
50
|
+
elsif self.class.is_canadian_postcode?(object)
|
51
|
+
@string = object
|
52
|
+
@kind = :postcode
|
53
|
+
elsif object.kind_of?(Google::Geo::Address)
|
54
|
+
@address = object
|
55
|
+
@kind = :geocode
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.geocode(string)
|
60
|
+
locations = call_geocoder(string).map { |a| self.new(a) }
|
61
|
+
return locations.first if locations.size == 1
|
62
|
+
return locations
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.call_geocoder(string)
|
66
|
+
geo = Google::Geo.new Hamweather.google_maps_api_key
|
67
|
+
|
68
|
+
begin
|
69
|
+
return geo.locate(string)
|
70
|
+
|
71
|
+
rescue Google::Geo::UnknownAddressError, Google::Geo::UnknownError
|
72
|
+
raise Hamweather::UnknownAddressError
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.is_zipcode?(string)
|
77
|
+
#Address: ZIP code (US)
|
78
|
+
zip_codes_regex = /(^[0-9]{5}$)|(^[0-9]{5}-[0-9]{4}$)/
|
79
|
+
return string =~ zip_codes_regex
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.is_canadian_postcode?(string)
|
83
|
+
#Rules: no D, F, I, O, Q, or U anywhere
|
84
|
+
# Basic validation: ^[ABCEGHJ-NPRSTVXY]{1}[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[ ]?[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[0-9]{1}$
|
85
|
+
# Extended validation: ^(A(0[ABCEGHJ-NPR]|1[ABCEGHK-NSV-Y]|2[ABHNV]|5[A]|8[A])|B(0[CEHJ-NPRSTVW]|1[ABCEGHJ-NPRSTV-Y]|2[ABCEGHJNRSTV-Z]|3[ABEGHJ-NPRSTVZ]|4[ABCEGHNPRV]|5[A]|6[L]|9[A])|C(0[AB]|1[ABCEN])|E(1[ABCEGHJNVWX]|2[AEGHJ-NPRSV]|3[ABCELNVYZ]|4[ABCEGHJ-NPRSTV-Z]|5[ABCEGHJ-NPRSTV]|6[ABCEGHJKL]|7[ABCEGHJ-NP]|8[ABCEGJ-NPRST]|9[ABCEGH])|G(0[ACEGHJ-NPRSTV-Z]|1[ABCEGHJ-NPRSTV-Y]|2[ABCEGJ-N]|3[ABCEGHJ-NZ]|4[ARSTVWXZ]|5[ABCHJLMNRTVXYZ]|6[ABCEGHJKLPRSTVWXZ]|7[ABGHJKNPSTXYZ]|8[ABCEGHJ-NPTVWYZ]|9[ABCHNPRTX])|H(0[HM]|1[ABCEGHJ-NPRSTV-Z]|2[ABCEGHJ-NPRSTV-Z]|3[ABCEGHJ-NPRSTV-Z]|4[ABCEGHJ-NPRSTV-Z]|5[AB]|7[ABCEGHJ-NPRSTV-Y]|8[NPRSTYZ]|9[ABCEGHJKPRSWX])|J(0[ABCEGHJ-NPRSTV-Z]|1[ACEGHJ-NRSTXZ]|2[ABCEGHJ-NRSTWXY]|3[ABEGHLMNPRTVXYZ]|4[BGHJ-NPRSTV-Z]|5[ABCJ-MRTV-Z]|6[AEJKNRSTVWYXZ]|7[ABCEGHJ-NPRTV-Z]|8[ABCEGHLMNPRTVXYZ]|9[ABEHJLNTVXYZ])|K(0[ABCEGHJ-M]|1[ABCEGHJ-NPRSTV-Z]|2[ABCEGHJ-MPRSTVW]|4[ABCKMPR]|6[AHJKTV]|7[ACGHK-NPRSV]|8[ABHNPRV]|9[AHJKLV])|L(0[[ABCEGHJ-NPRS]]|1[ABCEGHJ-NPRSTV-Z]|2[AEGHJMNPRSTVW]|3[BCKMPRSTVXYZ]|4[ABCEGHJ-NPRSTV-Z]|5[ABCEGHJ-NPRSTVW]|6[ABCEGHJ-MPRSTV-Z]|7[ABCEGJ-NPRST]|8[EGHJ-NPRSTVW]|9[ABCGHK-NPRSTVWYZ])|M(1[BCEGHJ-NPRSTVWX]|2[HJ-NPR]|3[ABCHJ-N]|4[ABCEGHJ-NPRSTV-Y]|5[ABCEGHJ-NPRSTVWX]|6[ABCEGHJ-NPRS]|7[AY]|8[V-Z]|9[ABCLMNPRVW])|N(0[ABCEGHJ-NPR]|1[ACEGHKLMPRST]|2[ABCEGHJ-NPRTVZ]|3[ABCEHLPRSTVWY]|4[BGKLNSTVWXZ]|5[ACHLPRV-Z]|6[ABCEGHJ-NP]|7[AGLMSTVWX]|8[AHMNPRSTV-Y]|9[ABCEGHJKVY])|P(0[ABCEGHJ-NPRSTV-Y]|1[ABCHLP]|2[ABN]|3[ABCEGLNPY]|4[NPR]|5[AEN]|6[ABC]|7[ABCEGJKL]|8[NT]|9[AN])|R(0[ABCEGHJ-M]|1[ABN]|2[CEGHJ-NPRV-Y]|3[ABCEGHJ-NPRSTV-Y]|4[AHJKL]|5[AGH]|6[MW]|7[ABCN]|8[AN]|9[A])|S(0[ACEGHJ-NP]|2[V]|3[N]|4[AHLNPRSTV-Z]|6[HJKVWX]|7[HJ-NPRSTVW]|9[AHVX])|T(0[ABCEGHJ-MPV]|1[ABCGHJ-MPRSV-Y]|2[ABCEGHJ-NPRSTV-Z]|3[ABCEGHJ-NPRZ]|4[ABCEGHJLNPRSTVX]|5[ABCEGHJ-NPRSTV-Z]|6[ABCEGHJ-NPRSTVWX]|7[AENPSVXYZ]|8[ABCEGHLNRSVWX]|9[ACEGHJKMNSVWX])|V(0[ABCEGHJ-NPRSTVWX]|1[ABCEGHJ-NPRSTV-Z]|2[ABCEGHJ-NPRSTV-Z]|3[ABCEGHJ-NRSTV-Y]|4[ABCEGK-NPRSTVWXZ]|5[ABCEGHJ-NPRSTV-Z]|6[ABCEGHJ-NPRSTV-Z]|7[ABCEGHJ-NPRSTV-Y]|8[ABCGJ-NPRSTV-Z]|9[ABCEGHJ-NPRSTV-Z])|X(0[ABCGX]|1[A])|Y(0[AB]|1[A]))[ ]?[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[0-9]{1}$
|
86
|
+
ca_postcode_regex = /^[A-Z]{1}[\d]{1}[A-Z]{1}[ ]?[\d]{1}[A-Z]{1}[\d]{1}$/
|
87
|
+
return string =~ ca_postcode_regex
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
data/spec/daily_spec.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
#Hpricot.parse('<FPeriod interval="1" Day="FRI" Date="2008-11-14" Wx="Chance T-storms" Icon="tstorm.gif" HiF="66" HiC="19" LoF="56" LoC="13" Pop="60" Detail="Occasional showers with a chance of thunderstorms. Areas of fog. Some thunderstorms May be severe after midnight. Lows in the mid 50s. South winds 5 to 10 mph shifting to the southwest 10 to 15 mph after midnight. Chance of rain near 100 percent."/>').root
|
4
|
+
#=> {emptyelem <fperiod loc="13" wx="Chance T-storms" hic="19" lof="56" icon="tstorm.gif" date="2008-11-14" hif="66" pop="60" day="FRI" interval="1" detail="Occasional showers with a chance of thunderstorms. Areas of fog. Some thunderstorms May be severe after midnight. Lows in the mid 50s. South winds 5 to 10 mph shifting to the southwest 10 to 15 mph after midnight. Chance of rain near 100 percent.">}
|
5
|
+
|
6
|
+
describe Hamweather::Forecast::Daily do
|
7
|
+
|
8
|
+
def test_forecast_data
|
9
|
+
<<-XML
|
10
|
+
<FPeriod interval="1" Day="FRI" Date="2008-11-14" Wx="Chance T-storms" Icon="tstorm.gif" HiF="66" HiC="19" LoF="56" LoC="13" Pop="60" Detail="Occasional showers with a chance of thunderstorms. Areas of fog. Some thunderstorms May be severe after midnight. Lows in the mid 50s. South winds 5 to 10 mph shifting to the southwest 10 to 15 mph after midnight. Chance of rain near 100 percent."/>
|
11
|
+
XML
|
12
|
+
end
|
13
|
+
|
14
|
+
before(:each) do
|
15
|
+
@daily_forecast = Hamweather::Forecast::Daily.new(Hpricot.parse(test_forecast_data).root)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "@hours should contain only hours belonging to 'today'" do
|
19
|
+
@daily_forecast.each_hour do |hour|
|
20
|
+
hour.date.should_be @daily_forecast.date
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "high_farenheit should be 66 degrees" do
|
25
|
+
@daily_forecast.high_farenheit.should == 66
|
26
|
+
end
|
27
|
+
|
28
|
+
it "high_celsius should be 19 degrees" do
|
29
|
+
@daily_forecast.high_celsius.should == 19
|
30
|
+
end
|
31
|
+
|
32
|
+
it "low_farenheit should be 56 degrees" do
|
33
|
+
@daily_forecast.low_farenheit.should == 56
|
34
|
+
end
|
35
|
+
|
36
|
+
it "low_celsius should be 13 degrees" do
|
37
|
+
@daily_forecast.low_celsius.should == 13
|
38
|
+
end
|
39
|
+
|
40
|
+
it "date should be the 14th of November 2008" do
|
41
|
+
@daily_forecast.date.should == Date.parse("2008-11-14")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "expected_weather should be Chance T-storms" do
|
45
|
+
@daily_forecast.expected_weather.should == "Chance T-storms"
|
46
|
+
end
|
47
|
+
|
48
|
+
it "probability_of_preciptiation should be 60 percent" do
|
49
|
+
@daily_forecast.probability_of_preciptiation.should == 60
|
50
|
+
end
|
51
|
+
|
52
|
+
it "detail should be Occasional showers with a chance of thunderstorms. Areas of fog. Some thunderstorms May be severe after midnight. Lows in the mid 50s. South winds 5 to 10 mph shifting to the southwest 10 to 15 mph after midnight. Chance of rain near 100 percent." do
|
53
|
+
@daily_forecast.detail.should == "Occasional showers with a chance of thunderstorms. Areas of fog. Some thunderstorms May be severe after midnight. Lows in the mid 50s. South winds 5 to 10 mph shifting to the southwest 10 to 15 mph after midnight. Chance of rain near 100 percent."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Hamweather::Forecast::Daily do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
setup_hamweather_api_config
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_forecast_data
|
10
|
+
<<-XML
|
11
|
+
<WeatherFeed><Location ID="12834" name="Greenwich" state="NY" country="US" tz="-5" tzname="EST" ><Astro><AstroPeriod Day="WED" Date="2008-12-03" Sunrise="7:06 AM EST" Sunset="4:19 PM EST" Moonrise="11:19 AM EST" Moonset="9:45 PM EST" MoonIllum="44%" MoonAge="6" MoonPhase="Waxing Crescent Moon" MoonIcon="6" /><AstroPeriod Day="WED" Date="2008-12-03" Sunrise="7:07 AM EST" Sunset="4:19 PM EST" Moonrise="11:41 AM EST" Moonset="10:50 PM EST" MoonIllum="50%" MoonAge="7" MoonPhase="First Quarter Moon" MoonIcon="7" /></Astro><WxOb StationID="KGFL" TempC="-4" TempF="25" ApparentC="-7" ApparentF="19" DewPointC="-5" DewPointF="23" RelativeHumidity="93" WindSpeedKnots="4" WindSpeedMPH="5" WindDirection="200" WindDirectionEng="SSW" WindGustKnots="0" WindGustMPH="0" PressureMB="1022" PressureIN="30.15" Wx="Fog/mist" Icon="fog.gif" Visibility="3SM" VisibilityKM="4.83" VisibilityMI="3" ReportEpoch="1228302540" ReportDate="2008-12-03 11:09 UTC" /><WxShortTerm ID="NYZ084"><STPeriod interval="1" Epoch="1228298400" Day="Wed" Date="2008-12-03" Time="10:00" Wx="Partly Cloudy" Icon="pcloudy.gif" TempC="0" TempF="32" Pop="10" QPFmm="" QPFin="" DewPointC="-5" DewPointF="23" RelativeHumidity="70" WindSpeedKnots="8" WindSpeedMPH="9" WindDirectionEng="SW" /></WxShortTerm><WxForecast ID="NYZ084"><FPeriod interval="1" Day="FRI" Date="2008-11-14" Wx="Chance T-storms" Icon="tstorm.gif" HiF="66" HiC="19" LoF="56" LoC="13" Pop="60" Detail="Occasional showers with a chance of thunderstorms. Areas of fog. Some thunderstorms May be severe after midnight. Lows in the mid 50s. South winds 5 to 10 mph shifting to the southwest 10 to 15 mph after midnight. Chance of rain near 100 percent."/></WxForecast></Location></WeatherFeed>
|
12
|
+
XML
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_location_name
|
16
|
+
"Greenwich, NY, 12834, USA"
|
17
|
+
end
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
#@forecast = Hamweather::Forecast.new(test_forecast_data)
|
21
|
+
@forecast = Hamweather::Forecast.new(Hamweather::Location.parse(test_location_name))
|
22
|
+
end
|
23
|
+
|
24
|
+
it "dailies{} should not be empty" do
|
25
|
+
@forecast.dailies.empty?.should be_false
|
26
|
+
end
|
27
|
+
|
28
|
+
it "each_day should yield Daily objects" do
|
29
|
+
@forecast.each_day do |day|
|
30
|
+
day.class.should == Hamweather::Forecast::Daily
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "each_hour should give back a Hash of Hourly objects" do
|
35
|
+
@forecast.each_day do |day|
|
36
|
+
day.each_hour do |hour|
|
37
|
+
hour.class.should == Hamweather::Forecast::Hourly
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "hourlies{} should not be empty" do
|
43
|
+
@forecast.hourlies.empty?.should be_false
|
44
|
+
end
|
45
|
+
|
46
|
+
it "each_hour should yield Hourly objects" do
|
47
|
+
@forecast.each_hour do |hour|
|
48
|
+
hour.class.should == Hamweather::Forecast::Hourly
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Hamweather do
|
4
|
+
|
5
|
+
describe "with no configuration" do
|
6
|
+
|
7
|
+
it "should raise an ApiKeyException" do
|
8
|
+
lambda { Hamweather.locate('Belfast') }.should raise_error(Hamweather::ApiKeyException)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should raise an ApiKeyException" do
|
12
|
+
lambda { Hamweather.forecast(mock(Hamweather::Location)) }.should raise_error(Hamweather::ApiKeyException)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "with a normal configuration" do
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
setup_google_api_config
|
21
|
+
setup_hamweather_api_config
|
22
|
+
@location = Hamweather.locate('Belfast')
|
23
|
+
@forecast = Hamweather.forecast(@location)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should give a location" do
|
27
|
+
@location.kind_of?(Hamweather::Location).should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should give a forecast" do
|
31
|
+
@forecast.kind_of?(Hamweather::Forecast).should be_true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
|
data/spec/hourly_spec.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
#Hpricot.parse('<STPeriod interval="1" Epoch="1228298400" Day="Wed" Date="2008-12-03" Time="10:00" Wx="Partly Cloudy" Icon="pcloudy.gif" TempC="0" TempF="32" Pop="10" QPFmm="" QPFin="" DewPointC="-5" DewPointF="23" RelativeHumidity="70" WindSpeedKnots="8" WindSpeedMPH="9" WindDirectionEng="SW" />').root
|
4
|
+
#=> {emptyelem <stperiod qpfmm="" wx="Partly Cloudy" relativehumidity="70" dewpointc="-5" time="10:00" icon="pcloudy.gif" date="2008-12-03" tempc="0" qpfin="" windspeedknots="8" dewpointf="23" pop="10" day="Wed" winddirectioneng="SW" tempf="32" epoch="1228298400" interval="1" windspeedmph="9">}
|
5
|
+
|
6
|
+
describe Hamweather::Forecast::Hourly do
|
7
|
+
|
8
|
+
def test_forecast_data
|
9
|
+
<<-XML
|
10
|
+
<STPeriod interval="1" Epoch="1228298400" Day="Wed" Date="2008-12-03" Time="10:00" Wx="Partly Cloudy" Icon="pcloudy.gif" TempC="0" TempF="32" Pop="10" QPFmm="" QPFin="" DewPointC="-5" DewPointF="23" RelativeHumidity="70" WindSpeedKnots="8" WindSpeedMPH="9" WindDirectionEng="SW" />
|
11
|
+
XML
|
12
|
+
end
|
13
|
+
|
14
|
+
before(:each) do
|
15
|
+
@hourly_forecast = Hamweather::Forecast::Hourly.new(Hpricot.parse(test_forecast_data).root)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "date should be 3rd December 2008" do
|
19
|
+
@hourly_forecast.date.should == Date.parse("2008-12-03")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "time should be 10:00" do
|
23
|
+
@hourly_forecast.time.should == "10:00"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "expected_weather should be Partly Cloudy" do
|
27
|
+
@hourly_forecast.expected_weather.should == "Partly Cloudy"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "temp_farenheit should be 32" do
|
31
|
+
@hourly_forecast.temp_farenheit.should == 32
|
32
|
+
end
|
33
|
+
|
34
|
+
it "temp_celsius should be 0" do
|
35
|
+
@hourly_forecast.temp_celsius.should == 0
|
36
|
+
end
|
37
|
+
|
38
|
+
it "probability_of_precipitation should be 10" do
|
39
|
+
@hourly_forecast.probability_of_precipitation.should == 10
|
40
|
+
end
|
41
|
+
|
42
|
+
it "precipitation_millimeters should be " do
|
43
|
+
@hourly_forecast.precipitation_millimeters.should == ""
|
44
|
+
end
|
45
|
+
|
46
|
+
it "precipitation_inches should be " do
|
47
|
+
@hourly_forecast.precipitation_inches.should == ""
|
48
|
+
end
|
49
|
+
|
50
|
+
it "dew_point_celsius should be -5" do
|
51
|
+
@hourly_forecast.dew_point_celsius.should == -5
|
52
|
+
end
|
53
|
+
|
54
|
+
it "dew_point_farenheit should be 23" do
|
55
|
+
@hourly_forecast.dew_point_farenheit.should == 23
|
56
|
+
end
|
57
|
+
|
58
|
+
it "relative_humidity should be 70" do
|
59
|
+
@hourly_forecast.relative_humidity.should == 70
|
60
|
+
end
|
61
|
+
|
62
|
+
it "wind_speed_knots should be 8" do
|
63
|
+
@hourly_forecast.wind_speed_knots.should == 8
|
64
|
+
end
|
65
|
+
|
66
|
+
it "wind_speed_mph should be 9" do
|
67
|
+
@hourly_forecast.wind_speed_mph.should == 9
|
68
|
+
end
|
69
|
+
|
70
|
+
it "wind_direction should be SW" do
|
71
|
+
@hourly_forecast.wind_direction.should == "SW"
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Hamweather::Location do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
setup_google_api_config
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "with zipcode 12834" do
|
10
|
+
|
11
|
+
# should be a valid zipcode (Greenwich, NY, USA)
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
@location = Hamweather::Location.parse("12834")
|
15
|
+
end
|
16
|
+
|
17
|
+
it ".to_uri should be 'wx/12834.xml'" do
|
18
|
+
@location.to_uri.should == "wx/12834.xml"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "with postcode H0H0H0" do
|
24
|
+
|
25
|
+
# should be a valid postcode (Santa, Lapland, CA)
|
26
|
+
|
27
|
+
before(:each) do
|
28
|
+
@location = Hamweather::Location.parse("H0H0H0")
|
29
|
+
end
|
30
|
+
|
31
|
+
it ".to_uri should be 'wx/H0H0H0.xml'" do
|
32
|
+
@location.to_uri.should == "wx/H0H0H0.xml"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "with an unambiguous address, '575 Burton Road, Greenwich, NY, 12834'" do
|
38
|
+
#Returns one option
|
39
|
+
|
40
|
+
before(:each) do
|
41
|
+
@location = Hamweather::Location.parse('575 Burton Road, Greenwich, NY, 12834')
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return one location object" do
|
45
|
+
#Google::Geo.should_receive(:new).with('thisapikey').and_return(mock(Google::Geo))
|
46
|
+
#so if you are using should_receive i'd put it in the actual it block of the spec
|
47
|
+
#so you should create the mock object first, set an expectation that ".locate" is called on it... then set the expectation that it's instantiated too!
|
48
|
+
@location.kind_of?(Hamweather::Location).should be_true
|
49
|
+
end
|
50
|
+
|
51
|
+
it ".to_uri should be 'wx/nearby.xml?lat=-73.504265&lon=43.0676821'" do
|
52
|
+
@location.to_uri.should == "wx/nearby.xml?lat=43.0676821&lon=-73.504265"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "with an ambiguous address, 'Greenwich, USA" do
|
57
|
+
#Returns circa 10 options, nb, no 'NY' or 'CT' in search string
|
58
|
+
before(:each) do
|
59
|
+
@location = Hamweather::Location.parse('Greenwich, USA')
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should return an array of location objects" do
|
63
|
+
@location.kind_of?(Array).should be_true
|
64
|
+
@location.should_not be_empty
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "with an unlocatable address" do
|
69
|
+
|
70
|
+
it "should raise a Hamweather::UnknownAddressError" do
|
71
|
+
|
72
|
+
lambda { Hamweather::Location.parse('') }.should raise_error(Hamweather::UnknownAddressError)
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/hamweather'
|
3
|
+
|
4
|
+
|
5
|
+
def setup_google_api_config
|
6
|
+
Hamweather.google_maps_api_key = "ABQIAAAAuC9Wz6AZ_BvsKClq3zThQhT2yXp_ZAY8_ufC3CFXhHIE1NvwkxRAjq1Mt9DYkVx1c-jcuAsAreOT_w"
|
7
|
+
end
|
8
|
+
|
9
|
+
def setup_hamweather_api_config
|
10
|
+
Hamweather.api_key = "wxC1E2gT8AY7"
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: davidjrice-hamweather
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Rice
|
8
|
+
- David Lowry
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2008-10-13 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: google-geo
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hpricot
|
27
|
+
version_requirement:
|
28
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.0.0
|
33
|
+
version:
|
34
|
+
description: Client API for Hamweather Weather Service (http://www.hamweather.com)
|
35
|
+
email: david@contrast.ie
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files:
|
41
|
+
- Manifest
|
42
|
+
- README
|
43
|
+
files:
|
44
|
+
- Manifest
|
45
|
+
- README
|
46
|
+
- Rakefile
|
47
|
+
- hamweather.gemspec
|
48
|
+
- lib/hamweather/forecast/daily.rb
|
49
|
+
- lib/hamweather/forecast/hourly.rb
|
50
|
+
- lib/hamweather/forecast.rb
|
51
|
+
- lib/hamweather/location.rb
|
52
|
+
- lib/hamweather.rb
|
53
|
+
has_rdoc: true
|
54
|
+
homepage: http://github.com/davidjrice/hamweather
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options:
|
57
|
+
- --main
|
58
|
+
- README
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
requirements: []
|
74
|
+
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.2.0
|
77
|
+
signing_key:
|
78
|
+
specification_version: 2
|
79
|
+
summary: Ruby Client API for Hamweather Weather Service (http://www.hamweather.com)
|
80
|
+
test_files:
|
81
|
+
- spec/daily_spec.rb
|
82
|
+
- spec/forecast_spec.rb
|
83
|
+
- spec/hamweather_spec.rb
|
84
|
+
- spec/hourly_spec.rb
|
85
|
+
- spec/location_spec.rb
|
86
|
+
- spec/spec_helper.rb
|