barometer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +266 -0
  3. data/VERSION.yml +4 -0
  4. data/bin/barometer +63 -0
  5. data/lib/barometer.rb +52 -0
  6. data/lib/barometer/base.rb +52 -0
  7. data/lib/barometer/data.rb +15 -0
  8. data/lib/barometer/data/current.rb +93 -0
  9. data/lib/barometer/data/distance.rb +131 -0
  10. data/lib/barometer/data/forecast.rb +66 -0
  11. data/lib/barometer/data/geo.rb +98 -0
  12. data/lib/barometer/data/location.rb +20 -0
  13. data/lib/barometer/data/measurement.rb +161 -0
  14. data/lib/barometer/data/pressure.rb +133 -0
  15. data/lib/barometer/data/speed.rb +147 -0
  16. data/lib/barometer/data/sun.rb +35 -0
  17. data/lib/barometer/data/temperature.rb +164 -0
  18. data/lib/barometer/data/units.rb +55 -0
  19. data/lib/barometer/data/zone.rb +124 -0
  20. data/lib/barometer/extensions/graticule.rb +50 -0
  21. data/lib/barometer/extensions/httparty.rb +21 -0
  22. data/lib/barometer/query.rb +228 -0
  23. data/lib/barometer/services.rb +6 -0
  24. data/lib/barometer/services/google.rb +146 -0
  25. data/lib/barometer/services/noaa.rb +6 -0
  26. data/lib/barometer/services/service.rb +324 -0
  27. data/lib/barometer/services/weather_bug.rb +6 -0
  28. data/lib/barometer/services/weather_dot_com.rb +6 -0
  29. data/lib/barometer/services/wunderground.rb +285 -0
  30. data/lib/barometer/services/yahoo.rb +274 -0
  31. data/lib/barometer/weather.rb +187 -0
  32. data/spec/barometer_spec.rb +162 -0
  33. data/spec/data_current_spec.rb +225 -0
  34. data/spec/data_distance_spec.rb +336 -0
  35. data/spec/data_forecast_spec.rb +150 -0
  36. data/spec/data_geo_spec.rb +90 -0
  37. data/spec/data_location_spec.rb +59 -0
  38. data/spec/data_measurement_spec.rb +411 -0
  39. data/spec/data_pressure_spec.rb +336 -0
  40. data/spec/data_speed_spec.rb +374 -0
  41. data/spec/data_sun_spec.rb +76 -0
  42. data/spec/data_temperature_spec.rb +396 -0
  43. data/spec/data_zone_spec.rb +133 -0
  44. data/spec/fixtures/current_calgary_ab.xml +1 -0
  45. data/spec/fixtures/forecast_calgary_ab.xml +1 -0
  46. data/spec/fixtures/geocode_40_73.xml +1 -0
  47. data/spec/fixtures/geocode_90210.xml +1 -0
  48. data/spec/fixtures/geocode_T5B4M9.xml +1 -0
  49. data/spec/fixtures/geocode_calgary_ab.xml +1 -0
  50. data/spec/fixtures/geocode_newyork_ny.xml +1 -0
  51. data/spec/fixtures/google_calgary_ab.xml +1 -0
  52. data/spec/fixtures/yahoo_90210.xml +1 -0
  53. data/spec/query_spec.rb +469 -0
  54. data/spec/service_google_spec.rb +144 -0
  55. data/spec/service_wunderground_spec.rb +330 -0
  56. data/spec/service_yahoo_spec.rb +299 -0
  57. data/spec/services_spec.rb +1106 -0
  58. data/spec/spec_helper.rb +14 -0
  59. data/spec/units_spec.rb +101 -0
  60. data/spec/weather_spec.rb +265 -0
  61. metadata +119 -0
@@ -0,0 +1,6 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'services/service'
4
+ require 'services/wunderground'
5
+ require 'services/google'
6
+ require 'services/yahoo'
@@ -0,0 +1,146 @@
1
+ module Barometer
2
+ #
3
+ # = Google Weather
4
+ # www.google.com
5
+ # NOTE: Google does not have an official API
6
+ #
7
+ # - key required: NO
8
+ # - registration required: NO
9
+ # - supported countries: ALL
10
+ #
11
+ # === performs geo coding
12
+ # - city: YES (except postalcode query)
13
+ # - coordinates: NO
14
+ #
15
+ # === time info
16
+ # - sun rise/set: NO
17
+ # - provides timezone: NO
18
+ # - requires TZInfo: NO
19
+ #
20
+ # == resources
21
+ # - API: http://unknown
22
+ #
23
+ # === Possible queries:
24
+ # -
25
+ #
26
+ # where query can be:
27
+ # - zipcode (US or Canadian)
28
+ # - city state; city, state
29
+ # - city
30
+ # - state
31
+ # - country
32
+ #
33
+ class Google < Service
34
+
35
+ def self.accepted_formats
36
+ [:zipcode, :postalcode, :geocode]
37
+ end
38
+
39
+ def self.source_name
40
+ :google
41
+ end
42
+
43
+ def self._measure(measurement, query, metric=true)
44
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
45
+ raise ArgumentError unless query.is_a?(Barometer::Query)
46
+ measurement.source = self.source_name
47
+
48
+ begin
49
+ result = self.get_all(query.preferred, metric)
50
+ rescue Timeout::Error => e
51
+ return measurement
52
+ end
53
+
54
+ measurement.current = self.build_current(result, metric)
55
+ measurement.forecast = self.build_forecast(result, metric)
56
+ measurement.location = self.build_location(query.geo)
57
+ measurement
58
+ end
59
+
60
+ def self.build_current(data, metric=true)
61
+ raise ArgumentError unless data.is_a?(Hash)
62
+ current = CurrentMeasurement.new
63
+
64
+ if data && data['forecast_information'] &&
65
+ data['forecast_information']['current_date_time']
66
+ current.time = data['forecast_information']['current_date_time']['data']
67
+ end
68
+
69
+ if data['current_conditions']
70
+ data = data['current_conditions']
71
+ current.icon = data['icon']['data'] if data['icon']
72
+ current.condition = data['condition']['data'] if data['condition']
73
+
74
+ humidity_match = data['humidity']['data'].match(/[\d]+/)
75
+ current.humidity = humidity_match[0].to_i if humidity_match
76
+
77
+ current.temperature = Temperature.new(metric)
78
+ current.temperature << [data['temp_c']['data'], data['temp_f']['data']]
79
+
80
+ current.wind = Speed.new(metric)
81
+ begin
82
+ current.wind << data['wind_condition']['data'].match(/[\d]+/)[0]
83
+ current.wind.direction = data['wind_condition']['data'].match(/Wind:.*?([\w]+).*?at/)[1]
84
+ rescue
85
+ end
86
+ end
87
+ current
88
+ end
89
+
90
+ def self.build_forecast(data, metric=true)
91
+ raise ArgumentError unless data.is_a?(Hash)
92
+
93
+ forecasts = []
94
+ return forecasts unless data && data['forecast_information'] &&
95
+ data['forecast_information']['forecast_date']
96
+ start_date = Date.parse(data['forecast_information']['forecast_date']['data'])
97
+ data = data['forecast_conditions'] if data['forecast_conditions']
98
+
99
+ # go through each forecast and create an instance
100
+ d = 0
101
+ data.each do |forecast|
102
+ forecast_measurement = ForecastMeasurement.new
103
+ forecast_measurement.icon = forecast['icon']['data'] if forecast['icon']
104
+ forecast_measurement.condition = forecast['condition']['data'] if forecast['condition']
105
+
106
+ if (start_date + d).strftime("%a").downcase == forecast['day_of_week']['data'].downcase
107
+ forecast_measurement.date = start_date + d
108
+ end
109
+
110
+ forecast_measurement.high = Temperature.new(metric)
111
+ forecast_measurement.high << forecast['high']['data']
112
+ forecast_measurement.low = Temperature.new(metric)
113
+ forecast_measurement.low << forecast['low']['data']
114
+
115
+ forecasts << forecast_measurement
116
+ d += 1
117
+ end
118
+ forecasts
119
+ end
120
+
121
+ def self.build_location(geo=nil)
122
+ raise ArgumentError unless (geo.nil? || geo.is_a?(Barometer::Geo))
123
+ location = Location.new
124
+ if geo
125
+ location.city = geo.locality
126
+ location.state_code = geo.region
127
+ location.country = geo.country
128
+ location.country_code = geo.country_code
129
+ location.latitude = geo.latitude
130
+ location.longitude = geo.longitude
131
+ end
132
+ location
133
+ end
134
+
135
+ # use HTTParty to get the current weather
136
+ def self.get_all(query, metric=true)
137
+ Barometer::Google.get(
138
+ "http://google.com/ig/api",
139
+ :query => {:weather => query, :hl => (metric ? "en-GB" : "en-US")},
140
+ :format => :xml,
141
+ :timeout => Barometer.timeout
142
+ )['xml_api_reply']['weather']
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,6 @@
1
+ module Barometer
2
+
3
+ class Noaa < Service
4
+ end
5
+
6
+ end
@@ -0,0 +1,324 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+
4
+ $:.unshift(File.dirname(__FILE__))
5
+ # load some changes to Httparty
6
+ require 'extensions/httparty'
7
+
8
+ module Barometer
9
+ #
10
+ # Service Class
11
+ #
12
+ # This is a base class for creating alternate weather api-consuming
13
+ # drivers. Each driver inherits from this class.
14
+ #
15
+ # Basically, all a service is required to do is take a query
16
+ # (ie "Paris") and return a complete Barometer::Measurement instance.
17
+ #
18
+ class Service
19
+
20
+ # all service drivers will use the HTTParty gem
21
+ include HTTParty
22
+
23
+ # Retrieves the weather source Service object
24
+ def self.source(source_name)
25
+ raise ArgumentError unless (source_name.is_a?(String) || source_name.is_a?(Symbol))
26
+ source_name = source_name.to_s.split("_").collect{ |s| s.capitalize }.join('')
27
+ raise ArgumentError unless Barometer.const_defined?(source_name)
28
+ raise ArgumentError unless Barometer.const_get(source_name).superclass == Barometer::Service
29
+ Barometer.const_get(source_name)
30
+ end
31
+
32
+ #
33
+ # get current weather and future (forecasted) weather
34
+ #
35
+ def self.measure(query, metric=true)
36
+ raise ArgumentError unless query.is_a?(Barometer::Query)
37
+
38
+ measurement = Barometer::Measurement.new(self.source_name, metric)
39
+ if self.meets_requirements?(query)
40
+ query.convert!(self.accepted_formats)
41
+ measurement = self._measure(measurement, query, metric) if query.preferred
42
+ end
43
+ measurement
44
+ end
45
+
46
+ def self.meets_requirements?(query=nil)
47
+ self.supports_country?(query) && (!self.requires_keys? || self.has_keys?)
48
+ end
49
+
50
+ #
51
+ # NOTE: The following methods MUST be re-defined by each driver.
52
+ #
53
+
54
+ # STUB: define this method to indicate what query formats are accepted
55
+ def self.accepted_formats
56
+ raise NotImplementedError
57
+ end
58
+
59
+ # STUB: define this method to measure the current & future weather
60
+ def self._measure(measurement=nil, query=nil, metric=true)
61
+ raise NotImplementedError
62
+ end
63
+
64
+ # STUB: define this method to actually retireve the source_name
65
+ def self.source_name
66
+ raise NotImplementedError
67
+ end
68
+
69
+ # STUB: define this method to check for the existance of API keys,
70
+ # this method is NOT needed if requires_keys? returns false
71
+ def self.has_keys?
72
+ raise NotImplementedError
73
+ end
74
+
75
+ #
76
+ # NOTE: The following methods can be re-defined by each driver. [OPTIONAL]
77
+ #
78
+
79
+ # DEFAULT: override this if you need to determine if the country is specified
80
+ def self.supports_country?(query=nil)
81
+ true
82
+ end
83
+
84
+ # DEFAULT: override this if you need to determine if API keys are required
85
+ def self.requires_keys?
86
+ false
87
+ end
88
+
89
+ #
90
+ # answer simple questions
91
+ #
92
+
93
+ #
94
+ # WINDY?
95
+ #
96
+ def self.windy?(measurement, threshold=10, utc_time=nil)
97
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
98
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
99
+ raise ArgumentError unless (utc_time.is_a?(Time) || utc_time.nil?)
100
+
101
+ measurement.current?(utc_time) ?
102
+ self.currently_windy?(measurement, threshold) :
103
+ self.forecasted_windy?(measurement, threshold, utc_time)
104
+ end
105
+
106
+ # cookie cutter answer, a driver can override this if they answer it differently
107
+ # if a service doesn't support obtaining the wind value, it will be ignored
108
+ def self.currently_windy?(measurement, threshold=10)
109
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
110
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
111
+ return nil if (!measurement.current || !measurement.current.wind?)
112
+ measurement.metric? ?
113
+ measurement.current.wind.kph.to_f >= threshold.to_f :
114
+ measurement.current.wind.mph.to_f >= threshold.to_f
115
+ end
116
+
117
+ # no driver can currently answer this question, so it doesn't have any code
118
+ def self.forecasted_windy?(measurement, threshold, utc_time); nil; end
119
+
120
+ #
121
+ # WET?
122
+ #
123
+ def self.wet?(measurement, threshold=50, utc_time=nil)
124
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
125
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
126
+ raise ArgumentError unless (utc_time.is_a?(Time) || utc_time.nil?)
127
+ measurement.current?(utc_time) ?
128
+ self.currently_wet?(measurement, threshold) :
129
+ self.forecasted_wet?(measurement, threshold, utc_time)
130
+ end
131
+
132
+ # cookie cutter answer
133
+ def self.currently_wet?(measurement, threshold=50)
134
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
135
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
136
+ return nil unless measurement.current
137
+ self.currently_wet_by_icon?(measurement.current) ||
138
+ self.currently_wet_by_dewpoint?(measurement) ||
139
+ self.currently_wet_by_humidity?(measurement.current) ||
140
+ self.currently_wet_by_pop?(measurement, threshold)
141
+ end
142
+
143
+ # cookie cutter answer
144
+ def self.currently_wet_by_dewpoint?(measurement)
145
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
146
+ return nil if (!measurement.current || !measurement.current.temperature? ||
147
+ !measurement.current.dew_point?)
148
+ measurement.metric? ?
149
+ measurement.current.temperature.c.to_f <= measurement.current.dew_point.c.to_f :
150
+ measurement.current.temperature.f.to_f <= measurement.current.dew_point.f.to_f
151
+ end
152
+
153
+ # cookie cutter answer
154
+ def self.currently_wet_by_humidity?(current_measurement)
155
+ raise ArgumentError unless current_measurement.is_a?(Barometer::CurrentMeasurement)
156
+ return nil unless current_measurement.humidity?
157
+ current_measurement.humidity.to_i >= 99
158
+ end
159
+
160
+ # cookie cutter answer
161
+ def self.currently_wet_by_pop?(measurement, threshold=50)
162
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
163
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
164
+ return nil unless measurement.forecast
165
+ # get todays forecast
166
+ forecast_measurement = measurement.for
167
+ return nil unless forecast_measurement
168
+ forecast_measurement.pop.to_f >= threshold.to_f
169
+ end
170
+
171
+ # cookie cutter answer
172
+ def self.forecasted_wet?(measurement, threshold=50, utc_time=nil)
173
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
174
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
175
+ raise ArgumentError unless (utc_time.is_a?(Time) || utc_time.nil?)
176
+ return nil unless measurement.forecast
177
+ forecast_measurement = measurement.for(utc_time)
178
+ return nil unless forecast_measurement
179
+ self.forecasted_wet_by_icon?(forecast_measurement) ||
180
+ self.forecasted_wet_by_pop?(forecast_measurement, threshold)
181
+ end
182
+
183
+ # cookie cutter answer
184
+ def self.forecasted_wet_by_pop?(forecast_measurement, threshold=50)
185
+ raise ArgumentError unless forecast_measurement.is_a?(Barometer::ForecastMeasurement)
186
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
187
+ return nil unless forecast_measurement.pop?
188
+ forecast_measurement.pop.to_f >= threshold.to_f
189
+ end
190
+
191
+ def self.currently_wet_by_icon?(current_measurement)
192
+ raise ArgumentError unless current_measurement.is_a?(Barometer::CurrentMeasurement)
193
+ return nil unless self.wet_icon_codes
194
+ return nil unless current_measurement.icon?
195
+ current_measurement.icon.is_a?(String) ?
196
+ self.wet_icon_codes.include?(current_measurement.icon.to_s.downcase) :
197
+ self.wet_icon_codes.include?(current_measurement.icon)
198
+ end
199
+
200
+ def self.forecasted_wet_by_icon?(forecast_measurement)
201
+ raise ArgumentError unless forecast_measurement.is_a?(Barometer::ForecastMeasurement)
202
+ return nil unless self.wet_icon_codes
203
+ return nil unless forecast_measurement.icon?
204
+ forecast_measurement.icon.is_a?(String) ?
205
+ self.wet_icon_codes.include?(forecast_measurement.icon.to_s.downcase) :
206
+ self.wet_icon_codes.include?(forecast_measurement.icon)
207
+ end
208
+
209
+ # this returns an array of codes that indicate "wet"
210
+ def self.wet_icon_codes; nil; end
211
+
212
+ #
213
+ # DAY?
214
+ #
215
+ def self.day?(measurement, utc_time=nil)
216
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
217
+ raise ArgumentError unless (utc_time.is_a?(Time) || utc_time.nil?)
218
+
219
+ measurement.current?(utc_time) ?
220
+ self.currently_day?(measurement) :
221
+ self.forecasted_day?(measurement, utc_time)
222
+ end
223
+
224
+ def self.currently_day?(measurement)
225
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
226
+ return nil unless measurement.current && measurement.current.sun
227
+ self.currently_after_sunrise?(measurement.current) &&
228
+ self.currently_before_sunset?(measurement.current)
229
+ end
230
+
231
+ def self.currently_after_sunrise?(current_measurement)
232
+ raise ArgumentError unless current_measurement.is_a?(Barometer::CurrentMeasurement)
233
+ return nil unless current_measurement.sun && current_measurement.sun.rise
234
+ Time.now.utc >= current_measurement.sun.rise
235
+ end
236
+
237
+ def self.currently_before_sunset?(current_measurement)
238
+ raise ArgumentError unless current_measurement.is_a?(Barometer::CurrentMeasurement)
239
+ return nil unless current_measurement.sun && current_measurement.sun.set
240
+ Time.now.utc <= current_measurement.sun.set
241
+ end
242
+
243
+ def self.forecasted_day?(measurement, utc_time=nil)
244
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
245
+ raise ArgumentError unless (utc_time.is_a?(Time) || utc_time.nil?)
246
+ return nil unless measurement.forecast
247
+ forecast_measurement = measurement.for(utc_time)
248
+ return nil unless forecast_measurement
249
+ self.forecasted_after_sunrise?(forecast_measurement, utc_time) &&
250
+ self.forecasted_before_sunset?(forecast_measurement, utc_time)
251
+ end
252
+
253
+ def self.forecasted_after_sunrise?(forecast_measurement, utc_time)
254
+ raise ArgumentError unless forecast_measurement.is_a?(Barometer::ForecastMeasurement)
255
+ raise ArgumentError unless utc_time.is_a?(Time)
256
+ return nil unless forecast_measurement.sun && forecast_measurement.sun.rise
257
+ utc_time >= forecast_measurement.sun.rise
258
+ end
259
+
260
+ def self.forecasted_before_sunset?(forecast_measurement, utc_time)
261
+ raise ArgumentError unless forecast_measurement.is_a?(Barometer::ForecastMeasurement)
262
+ raise ArgumentError unless utc_time.is_a?(Time)
263
+ return nil unless forecast_measurement.sun && forecast_measurement.sun.set
264
+ utc_time <= forecast_measurement.sun.set
265
+ end
266
+
267
+ #
268
+ # SUNNY?
269
+ #
270
+ def self.sunny?(measurement, utc_time=nil)
271
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
272
+ raise ArgumentError unless (utc_time.is_a?(Time) || utc_time.nil?)
273
+ measurement.current?(utc_time) ?
274
+ self.currently_sunny?(measurement) :
275
+ self.forecasted_sunny?(measurement, utc_time)
276
+ end
277
+
278
+ # cookie cutter answer
279
+ def self.currently_sunny?(measurement)
280
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
281
+ return nil unless measurement.current
282
+ return false if self.currently_day?(measurement) == false
283
+ self.currently_sunny_by_icon?(measurement.current)
284
+ end
285
+
286
+ # cookie cutter answer
287
+ def self.forecasted_sunny?(measurement, utc_time=nil)
288
+ raise ArgumentError unless measurement.is_a?(Barometer::Measurement)
289
+ raise ArgumentError unless (utc_time.is_a?(Time) || utc_time.nil?)
290
+ return nil unless measurement.forecast
291
+ return false if self.forecasted_day?(measurement, utc_time) == false
292
+ forecast_measurement = measurement.for(utc_time)
293
+ return nil unless forecast_measurement
294
+ self.forecasted_sunny_by_icon?(forecast_measurement)
295
+ end
296
+
297
+ def self.currently_sunny_by_icon?(current_measurement)
298
+ raise ArgumentError unless current_measurement.is_a?(Barometer::CurrentMeasurement)
299
+ return nil unless self.sunny_icon_codes
300
+ return nil unless current_measurement.icon?
301
+ current_measurement.icon.is_a?(String) ?
302
+ self.sunny_icon_codes.include?(current_measurement.icon.to_s.downcase) :
303
+ self.sunny_icon_codes.include?(current_measurement.icon)
304
+ end
305
+
306
+ def self.forecasted_sunny_by_icon?(forecast_measurement)
307
+ raise ArgumentError unless forecast_measurement.is_a?(Barometer::ForecastMeasurement)
308
+ return nil unless self.sunny_icon_codes
309
+ return nil unless forecast_measurement.icon?
310
+ forecast_measurement.icon.is_a?(String) ?
311
+ self.sunny_icon_codes.include?(forecast_measurement.icon.to_s.downcase) :
312
+ self.sunny_icon_codes.include?(forecast_measurement.icon)
313
+ end
314
+
315
+ # this returns an array of codes that indicate "sunny"
316
+ def self.sunny_icon_codes; nil; end
317
+
318
+ end
319
+
320
+ end
321
+
322
+ # def key_name
323
+ # # what variables holds the api key?
324
+ # end