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,15 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ # measurements and units
4
+ require 'data/measurement'
5
+ require 'data/current'
6
+ require 'data/forecast'
7
+ require 'data/zone'
8
+ require 'data/sun'
9
+ require 'data/geo'
10
+ require 'data/location'
11
+ require 'data/units'
12
+ require 'data/temperature'
13
+ require 'data/distance'
14
+ require 'data/speed'
15
+ require 'data/pressure'
@@ -0,0 +1,93 @@
1
+ module Barometer
2
+ #
3
+ # Current Measurement
4
+ # a data class for current weather conditions
5
+ #
6
+ # This is basically a data holding class for the current weather
7
+ # conditions.
8
+ #
9
+ class CurrentMeasurement
10
+
11
+ attr_accessor :time, :local_time
12
+ attr_reader :humidity, :icon, :condition
13
+ attr_reader :temperature, :dew_point, :heat_index, :wind_chill
14
+ attr_reader :wind, :pressure, :visibility, :sun
15
+
16
+ def time=(time)
17
+ #raise ArgumentError unless time.is_a?(Time)
18
+ @time = time
19
+ end
20
+
21
+ def humidity=(humidity)
22
+ raise ArgumentError unless
23
+ (humidity.is_a?(Fixnum) || humidity.is_a?(Float))
24
+ @humidity = humidity
25
+ end
26
+
27
+ def icon=(icon)
28
+ raise ArgumentError unless icon.is_a?(String)
29
+ @icon = icon
30
+ end
31
+
32
+ def condition=(condition)
33
+ raise ArgumentError unless condition.is_a?(String)
34
+ @condition = condition
35
+ end
36
+
37
+ def temperature=(temperature)
38
+ raise ArgumentError unless temperature.is_a?(Barometer::Temperature)
39
+ @temperature = temperature
40
+ end
41
+
42
+ def dew_point=(dew_point)
43
+ raise ArgumentError unless dew_point.is_a?(Barometer::Temperature)
44
+ @dew_point = dew_point
45
+ end
46
+
47
+ def heat_index=(heat_index)
48
+ raise ArgumentError unless heat_index.is_a?(Barometer::Temperature)
49
+ @heat_index = heat_index
50
+ end
51
+
52
+ def wind_chill=(wind_chill)
53
+ raise ArgumentError unless wind_chill.is_a?(Barometer::Temperature)
54
+ @wind_chill = wind_chill
55
+ end
56
+
57
+ def wind=(wind)
58
+ raise ArgumentError unless wind.is_a?(Barometer::Speed)
59
+ @wind = wind
60
+ end
61
+
62
+ def pressure=(pressure)
63
+ raise ArgumentError unless pressure.is_a?(Barometer::Pressure)
64
+ @pressure = pressure
65
+ end
66
+
67
+ def visibility=(visibility)
68
+ raise ArgumentError unless visibility.is_a?(Barometer::Distance)
69
+ @visibility = visibility
70
+ end
71
+
72
+ def sun=(sun)
73
+ raise ArgumentError unless sun.is_a?(Barometer::Sun)
74
+ @sun = sun
75
+ end
76
+
77
+ #
78
+ # helpers
79
+ #
80
+
81
+ # creates "?" helpers for all attributes (which maps to nil?)
82
+ def method_missing(method,*args)
83
+ # if the method ends in ?, then strip it off and see if we
84
+ # respond to the method without the ?
85
+ if (call_method = method.to_s.chomp!("?")) && respond_to?(call_method)
86
+ return send(call_method).nil? ? false : true
87
+ else
88
+ super(method,*args)
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,131 @@
1
+ module Barometer
2
+ #
3
+ # A simple Distance class
4
+ #
5
+ # Think of this like the Integer class. Enhancement
6
+ # is that you can create a number (in a certain unit), then
7
+ # get that number back already converted to another unit.
8
+ #
9
+ # All comparison operations will be done in metric
10
+ #
11
+ # NOTE: this currently only supports the scale of
12
+ # kilometers (km) and miles (m). There is currently
13
+ # no way to scale to smaller units (eg km -> m -> mm)
14
+ class Distance < Barometer::Units
15
+
16
+ METRIC_UNITS = "km"
17
+ IMPERIAL_UNITS = "m"
18
+
19
+ attr_accessor :kilometers, :miles
20
+
21
+ def initialize(metric=true)
22
+ @kilometers = nil
23
+ @miles = nil
24
+ super(metric)
25
+ end
26
+
27
+ def metric_default=(value); self.km = value; end
28
+ def imperial_default=(value); self.m = value; end
29
+
30
+ #
31
+ # CONVERTERS
32
+ #
33
+
34
+ def self.km_to_m(km)
35
+ return nil unless km && (km.is_a?(Integer) || km.is_a?(Float))
36
+ km.to_f * 0.622
37
+ end
38
+
39
+ def self.m_to_km(m)
40
+ return nil unless m && (m.is_a?(Integer) || m.is_a?(Float))
41
+ m.to_f * 1.609
42
+ end
43
+
44
+ #
45
+ # ACCESSORS
46
+ #
47
+
48
+ # store kilometers
49
+ def km=(km)
50
+ return if !km || !(km.is_a?(Integer) || km.is_a?(Float))
51
+ @kilometers = km.to_f
52
+ self.update_miles(km.to_f)
53
+ end
54
+
55
+ # store miles
56
+ def m=(m)
57
+ return if !m || !(m.is_a?(Integer) || m.is_a?(Float))
58
+ @miles = m.to_f
59
+ self.update_kilometers(m.to_f)
60
+ end
61
+
62
+ # return the stored kilometers or convert from miles
63
+ def km(as_integer=true)
64
+ km = (@kilometers || Distance.m_to_km(@miles))
65
+ km ? (as_integer ? km.to_i : (100*km).round/100.0) : nil
66
+ end
67
+
68
+ # return the stored miles or convert from kilometers
69
+ def m(as_integer=true)
70
+ m = (@miles || Distance.km_to_m(@kilometers))
71
+ m ? (as_integer ? m.to_i : (100*m).round/100.0) : nil
72
+ end
73
+
74
+ #
75
+ # OPERATORS
76
+ #
77
+
78
+ def <=>(other)
79
+ self.km <=> other.km
80
+ end
81
+
82
+ #
83
+ # HELPERS
84
+ #
85
+
86
+ # will just return the value (no units)
87
+ def to_i(metric=nil)
88
+ (metric || (metric.nil? && self.metric?)) ? self.km : self.m
89
+ end
90
+
91
+ # will just return the value (no units) with more precision
92
+ def to_f(metric=nil)
93
+ (metric || (metric.nil? && self.metric?)) ? self.km(false) : self.m(false)
94
+ end
95
+
96
+ # will return the value with units
97
+ def to_s(metric=nil)
98
+ (metric || (metric.nil? && self.metric?)) ? "#{self.km} #{METRIC_UNITS}" : "#{self.m} #{IMPERIAL_UNITS}"
99
+ end
100
+
101
+ # will just return the units (no value)
102
+ def units(metric=nil)
103
+ (metric || (metric.nil? && self.metric?)) ? METRIC_UNITS : IMPERIAL_UNITS
104
+ end
105
+
106
+ # when we set miles, it is possible the a non-equivalent value of
107
+ # kilometers remains. if so, clear it.
108
+ def update_kilometers(m)
109
+ return unless @kilometers
110
+ difference = Distance.m_to_km(m.to_f) - @kilometers
111
+ # only clear kilometers if the stored kilometers is off be more then 1 unit
112
+ # then the conversion of miles
113
+ @kilometers = nil unless difference.abs <= 1.0
114
+ end
115
+
116
+ # when we set kilometers, it is possible the a non-equivalent value of
117
+ # miles remains. if so, clear it.
118
+ def update_miles(km)
119
+ return unless @miles
120
+ difference = Distance.km_to_m(km.to_f) - @miles
121
+ # only clear miles if the stored miles is off be more then 1 unit
122
+ # then the conversion of kilometers
123
+ @miles = nil unless difference.abs <= 1.0
124
+ end
125
+
126
+ def nil?
127
+ (@kilometers || @miles) ? false : true
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,66 @@
1
+ require 'date'
2
+ module Barometer
3
+ #
4
+ # Forecast Measurement
5
+ # a data class for forecasted weather conditions
6
+ #
7
+ # This is basically a data holding class for the forecasted weather
8
+ # conditions.
9
+ #
10
+ class ForecastMeasurement
11
+
12
+ attr_reader :date, :icon, :condition
13
+ attr_reader :low, :high, :pop, :sun
14
+
15
+ def date=(date)
16
+ raise ArgumentError unless date.is_a?(Date)
17
+ @date = date
18
+ end
19
+
20
+ def icon=(icon)
21
+ raise ArgumentError unless icon.is_a?(String)
22
+ @icon = icon
23
+ end
24
+
25
+ def condition=(condition)
26
+ raise ArgumentError unless condition.is_a?(String)
27
+ @condition = condition
28
+ end
29
+
30
+ def high=(high)
31
+ raise ArgumentError unless high.is_a?(Barometer::Temperature)
32
+ @high = high
33
+ end
34
+
35
+ def low=(low)
36
+ raise ArgumentError unless low.is_a?(Barometer::Temperature)
37
+ @low = low
38
+ end
39
+
40
+ def pop=(pop)
41
+ raise ArgumentError unless pop.is_a?(Fixnum)
42
+ @pop = pop
43
+ end
44
+
45
+ def sun=(sun)
46
+ raise ArgumentError unless sun.is_a?(Barometer::Sun)
47
+ @sun = sun
48
+ end
49
+
50
+ #
51
+ # helpers
52
+ #
53
+
54
+ # creates "?" helpers for all attributes (which maps to nil?)
55
+ def method_missing(method,*args)
56
+ # if the method ends in ?, then strip it off and see if we
57
+ # respond to the method without the ?
58
+ if (call_method = method.to_s.chomp!("?")) && respond_to?(call_method)
59
+ return send(call_method).nil? ? false : true
60
+ else
61
+ super(method,*args)
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,98 @@
1
+ module Barometer
2
+ #
3
+ # A simple Geo class
4
+ #
5
+ # Used to store location data from Graticule or HTTParty and convert
6
+ # into just the data needed for geocoding
7
+ #
8
+ class Geo
9
+
10
+ attr_accessor :latitude, :longitude
11
+ attr_accessor :locality, :region, :country, :country_code
12
+
13
+ #
14
+ # this will take a Location object (either generated by Graticule
15
+ # or HTTParty), and fill in the applicable data
16
+ #
17
+ def initialize(location=nil)
18
+ return unless location
19
+
20
+ has_graticule = false
21
+ begin
22
+ Graticule
23
+ has_graticule = true
24
+ rescue
25
+ # do nothing, Graticule not available
26
+ end
27
+
28
+ if has_graticule
29
+ raise ArgumentError unless
30
+ (location.is_a?(Graticule::Location) || location.is_a?(Hash))
31
+ else
32
+ raise ArgumentError unless location.is_a?(Hash)
33
+ end
34
+
35
+ if has_graticule && location.class == Graticule::Location
36
+ self.build_from_graticule(location)
37
+ elsif location.class == Hash
38
+ self.build_from_httparty(location)
39
+ end
40
+ self
41
+ end
42
+
43
+ def build_from_graticule(location=nil)
44
+ return nil unless location
45
+
46
+ begin
47
+ require 'rubygems'
48
+ require 'graticule'
49
+ $:.unshift(File.dirname(__FILE__))
50
+ # load some changes to Graticule
51
+ # TODO: attempt to get changes into Graticule gem
52
+ require 'extensions/graticule'
53
+ end
54
+
55
+ raise ArgumentError unless location.is_a?(Graticule::Location)
56
+
57
+ @latitude = location.latitude
58
+ @longitude = location.longitude
59
+ @locality = location.locality
60
+ @region = location.region
61
+ @country = location.country
62
+ @country_code = location.country_code
63
+ end
64
+
65
+ def build_from_httparty(location=nil)
66
+ return nil unless location
67
+ raise ArgumentError unless location.is_a?(Hash)
68
+
69
+ placemark = location["Placemark"]
70
+ placemark = placemark.first if placemark.is_a?(Array)
71
+
72
+ if placemark && placemark["Point"] && placemark["Point"]["coordinates"]
73
+ @latitude = placemark["Point"]["coordinates"].split(',')[1].to_f
74
+ @longitude = placemark["Point"]["coordinates"].split(',')[0].to_f
75
+ end
76
+ if placemark && placemark["AddressDetails"] && placemark["AddressDetails"]["Country"]
77
+ if placemark["AddressDetails"]["Country"]["AdministrativeArea"]
78
+ if placemark["AddressDetails"]["Country"]["AdministrativeArea"]["SubAdministrativeArea"]
79
+ locality = placemark["AddressDetails"]["Country"]["AdministrativeArea"]["SubAdministrativeArea"]["Locality"]
80
+ else
81
+ locality = placemark["AddressDetails"]["Country"]["AdministrativeArea"]["Locality"]
82
+ end
83
+ if locality
84
+ @locality = locality["LocalityName"]
85
+ end
86
+ @region = placemark["AddressDetails"]["Country"]["AdministrativeArea"]["AdministrativeAreaName"]
87
+ end
88
+ @country = placemark["AddressDetails"]["Country"]["CountryName"]
89
+ @country_code = placemark["AddressDetails"]["Country"]["CountryNameCode"]
90
+ end
91
+ end
92
+
93
+ def coordinates
94
+ [@latitude, @longitude].join(',')
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,20 @@
1
+ module Barometer
2
+ #
3
+ # A simple Location class
4
+ #
5
+ # Used to store location information about the station that
6
+ # gave the measurement data for a weather query, or the location
7
+ # that was queried
8
+ #
9
+ class Location
10
+
11
+ attr_accessor :id, :name, :city
12
+ attr_accessor :state_name, :state_code, :country, :country_code, :zip_code
13
+ attr_accessor :latitude, :longitude
14
+
15
+ def coordinates
16
+ [@latitude, @longitude].join(',')
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,161 @@
1
+ module Barometer
2
+ #
3
+ # Measurement
4
+ # a class that holds the response from a weather service
5
+ #
6
+ # its main purpose is to hold all the data collected from a weather service
7
+ # as it is passed to the weather object
8
+ #
9
+ # this response includes
10
+ # - current weather data (using the CurrentMeasurement class)
11
+ # - forecasted weather data (an Array of instances of the ForecastMeasurement class)
12
+ # - time_zone information (for the location in question)
13
+ # - weather station information (for the station that gave collected the data)
14
+ #
15
+ class Measurement
16
+
17
+ # the weather service source
18
+ attr_reader :source
19
+ # current and forecasted data
20
+ attr_reader :current, :forecast
21
+ attr_reader :timezone, :station, :location
22
+ attr_reader :success, :time
23
+ attr_accessor :metric
24
+
25
+ def initialize(source=nil, metric=true)
26
+ @source = source
27
+ @metric = metric
28
+ @success = false
29
+ end
30
+
31
+ def success!
32
+ if current && current.temperature && !current.temperature.c.nil?
33
+ @success = true
34
+ end
35
+ end
36
+
37
+ def stamp!; @time = Time.now.utc; end
38
+ def success?; @success; end
39
+ def metric?; @metric; end
40
+ def metric!; @metric=true; end
41
+ def imperial!; @metric=false; end
42
+
43
+ # this will tell us if the measurement is still current ... if it is still
44
+ # current this means that the CurrentMeasurement can still used as now
45
+ #
46
+ # what it also means is that if you took a measurement right now (time = now)
47
+ # and then asked if current?(time_in_future) that current? would be true for
48
+ # any time_in_future within 4 hours of now
49
+ #
50
+ # where is this useful? lets say you take the measurement now (time = now),
51
+ # and then you want to know if self.windy?(5_hours_in_future) ... we could
52
+ # not use the current data for this answser as the time 5_hours_in_future
53
+ # is not current
54
+ def current?(utc_time=Time.now.utc)
55
+ return false unless @time
56
+ raise ArgumentError unless utc_time.is_a?(Time)
57
+ hours_still_current = 4
58
+ difference = (@time - utc_time).to_i
59
+ difference = (difference*(-1)).to_i if difference < 0
60
+ difference <= (60*60*hours_still_current).to_i
61
+ end
62
+
63
+ #
64
+ # Returns a forecast for a day given by a Date, DateTime,
65
+ # Time, or a string that can be parsed to a date
66
+ #
67
+ # credit: http://github.com/jdpace/weatherman/
68
+ #
69
+ def for(date=nil)
70
+ date = @timezone.today unless date || !@timezone
71
+ date ||= Date.today
72
+ return nil unless (@forecast && @forecast.size > 0)
73
+
74
+ # Format date into a Date class
75
+ date = case date.class.name
76
+ when 'String'
77
+ Date.parse(date)
78
+ when 'Date'
79
+ date
80
+ when 'DateTime'
81
+ Date.new(date.year, date.month, date.day)
82
+ when 'Time'
83
+ Date.new(date.year, date.month, date.day)
84
+ end
85
+
86
+ day = nil
87
+ @forecast.each do |f|
88
+ day = f if date == f.date
89
+ end
90
+ return day
91
+ end
92
+
93
+ def source=(source)
94
+ raise ArgumentError unless source.is_a?(Symbol)
95
+ @source = source
96
+ end
97
+
98
+ def time=(time=Time.now.utc)
99
+ raise ArgumentError unless time.is_a?(Time)
100
+ @time = time
101
+ end
102
+
103
+ def current=(current)
104
+ raise ArgumentError unless current.is_a?(Barometer::CurrentMeasurement)
105
+ @current = current
106
+ self.stamp!
107
+ # self-determine success
108
+ self.success!
109
+ end
110
+
111
+ def forecast=(forecast)
112
+ raise ArgumentError unless forecast.is_a?(Array)
113
+ @forecast = forecast
114
+ end
115
+
116
+ def timezone=(timezone)
117
+ #raise ArgumentError unless timezone.is_a?(String)
118
+ raise ArgumentError unless timezone.is_a?(Barometer::Zone)
119
+ @timezone = timezone
120
+ end
121
+
122
+ def station=(station)
123
+ raise ArgumentError unless station.is_a?(Barometer::Location)
124
+ @station = station
125
+ end
126
+
127
+ def location=(location)
128
+ raise ArgumentError unless location.is_a?(Barometer::Location)
129
+ @location = location
130
+ end
131
+
132
+ #
133
+ # simple questions
134
+ # pass questions to the source
135
+ #
136
+
137
+ def windy?(threshold=10, utc_time=Time.now.utc)
138
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
139
+ raise ArgumentError unless utc_time.is_a?(Time)
140
+ Barometer::Service.source(@source).windy?(self, threshold, utc_time)
141
+ end
142
+
143
+ def wet?(threshold=50, utc_time=Time.now.utc)
144
+ raise ArgumentError unless (threshold.is_a?(Fixnum) || threshold.is_a?(Float))
145
+ raise ArgumentError unless utc_time.is_a?(Time)
146
+ Barometer::Service.source(@source).wet?(self, threshold, utc_time)
147
+ end
148
+
149
+ def day?(utc_time=Time.now.utc)
150
+ raise ArgumentError unless utc_time.is_a?(Time)
151
+ Barometer::Service.source(@source).day?(self, utc_time)
152
+ end
153
+
154
+ def sunny?(utc_time=Time.now.utc)
155
+ raise ArgumentError unless utc_time.is_a?(Time)
156
+ return false if self.day?(utc_time) == false
157
+ Barometer::Service.source(@source).sunny?(self, utc_time)
158
+ end
159
+
160
+ end
161
+ end