attack-barometer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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/base.rb +52 -0
  6. data/lib/barometer/data/current.rb +93 -0
  7. data/lib/barometer/data/distance.rb +131 -0
  8. data/lib/barometer/data/forecast.rb +66 -0
  9. data/lib/barometer/data/geo.rb +98 -0
  10. data/lib/barometer/data/location.rb +20 -0
  11. data/lib/barometer/data/measurement.rb +161 -0
  12. data/lib/barometer/data/pressure.rb +133 -0
  13. data/lib/barometer/data/speed.rb +147 -0
  14. data/lib/barometer/data/sun.rb +35 -0
  15. data/lib/barometer/data/temperature.rb +164 -0
  16. data/lib/barometer/data/units.rb +55 -0
  17. data/lib/barometer/data/zone.rb +124 -0
  18. data/lib/barometer/data.rb +15 -0
  19. data/lib/barometer/extensions/graticule.rb +50 -0
  20. data/lib/barometer/extensions/httparty.rb +21 -0
  21. data/lib/barometer/query.rb +228 -0
  22. data/lib/barometer/services/google.rb +146 -0
  23. data/lib/barometer/services/noaa.rb +6 -0
  24. data/lib/barometer/services/service.rb +324 -0
  25. data/lib/barometer/services/weather_bug.rb +6 -0
  26. data/lib/barometer/services/weather_dot_com.rb +6 -0
  27. data/lib/barometer/services/wunderground.rb +285 -0
  28. data/lib/barometer/services/yahoo.rb +274 -0
  29. data/lib/barometer/services.rb +6 -0
  30. data/lib/barometer/weather.rb +187 -0
  31. data/lib/barometer.rb +52 -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
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Mark
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,266 @@
1
+ = barometer
2
+
3
+ A multi API consuming weather forecasting superstar.
4
+
5
+ Barometer provides a common public API to one or more weather services (APIs)
6
+ of your choice. Weather services can co-exist to retrieve extensive
7
+ information, or they can be used in a hierarchical configuration where lower
8
+ preferred weather services are only used if previous services are
9
+ unavailable.
10
+
11
+ == status
12
+
13
+ Currently this project is in development and will only work for a few weather
14
+ services (wunderground, google, yahoo).
15
+
16
+ Features to be added before first release:
17
+ - gem setup/config, apply to rubyforge
18
+
19
+ Features to be added in future releases:
20
+ - even more weather service drivers (noaa, weather.com, weatherbug)
21
+ - ability to query multiple services and combine/average the results
22
+ - support iaco as a query format
23
+
24
+ = dependencies
25
+
26
+ === HTTParty
27
+
28
+ Why? HTTParty was created and designed specifically for consuming web services.
29
+ I choose to use this over using the Net::HTTP library directly to allow for
30
+ faster development of this project.
31
+
32
+ HTTParty is also extended to include configurable Timoout support.
33
+
34
+ === tzinfo
35
+
36
+ Why? Barometer deals with time information for locations all over the world.
37
+ This information doesn't mean that much if it can't be converted to times
38
+ that don't correspond to the applicable timezone.
39
+ Tzinfo handles this time zone manipulation.
40
+
41
+ === graticule (very soft dependency)
42
+
43
+ Why? Barometer returns the weather for a given location. Most weather service
44
+ APIs are somewhat restricted on the query format they receive. To bridge
45
+ this gap and allow for maximum flexibility on the 'barometer' query format,
46
+ the query will be geo-coded using the Google geocoding service, if required.
47
+ Graticule can provide this geocoding interface.
48
+
49
+ Using Graticule requires a free Google API key for geocoding. It is possible
50
+ to use barometer without geocoding, though your query format will be
51
+ limited to that of the weather service API.
52
+
53
+ ALTERNATE: If you supply a Google API key but don't install the Graticule gem,
54
+ HTTParty will be used instead to provide the same geocoding. Basically
55
+ Graticule is only used if it exists.
56
+
57
+ NOTE: you can force Barometer not to use Graticule, even if you have it installed
58
+ using the following:
59
+
60
+ Barometer.skip_graticule = true
61
+
62
+ = usage
63
+
64
+ You can use barometer right out of the box, as it is configured to use one
65
+ register-less (no API key required) international weather service
66
+ (wunderground.com).
67
+
68
+ For better results, signup for a google-map key and enhance your barometer
69
+ with geo-coding.
70
+
71
+ require 'barometer'
72
+
73
+ Barometer.google_geocode_key = "THE_GOOGLE_API_KEY"
74
+ barometer = Barometer.new("Paris")
75
+ weather = barometer.measure
76
+
77
+ puts weather.current.temperture
78
+
79
+ == multiple weather API, with hierarchy
80
+
81
+ require 'barometer'
82
+
83
+ Barometer.google_geocode_key = "THE_GOOGLE_API_KEY"
84
+ # use yahoo and google, if they both fail, use wunderground
85
+ Barometer.selection = { 1 => [:yahoo, :google], 2 => :wunderground }
86
+
87
+ barometer = Barometer.new("Paris")
88
+ weather = barometer.measure
89
+
90
+ puts weather.current.temperture
91
+
92
+ == command line
93
+
94
+ You can use barometer from the command line.
95
+
96
+ # barometer berlin
97
+
98
+ This will output the weather information for the given query.
99
+
100
+ === fail
101
+
102
+ What would cause a weather service to fail? The most obvious is that the
103
+ particular weather service in currently unavailable or not reachable.
104
+ Other possible reasons would include not having the API (or a valid API
105
+ key for the particular weather service, if required), not providing a
106
+ valid query, or providing a query for a location not supported by the
107
+ weather service.
108
+
109
+ For example, if you look at the example above, the query of "Paris" refers
110
+ to a city in France. Yahoo weather services only supports
111
+ weather results for USA (at least at the time of writing). Therefore,
112
+ Barometer would not use Yahoo, just Google and failover to use Wunderground
113
+ (if needed).
114
+
115
+ === bootstrapping
116
+
117
+ You can use weather service drivers directly. Below is an example to use
118
+ Wunderground, but since the driver interface is abstracted it will be the
119
+ same for all supported services.
120
+
121
+ require 'barometer'
122
+ Barometer.google_geocode_key = "THE_GOOGLE_API_KEY"
123
+
124
+ query = Barometer::Query.new("Paris")
125
+ weather = Barometer::Service.source(:wunderground).measure(query)
126
+
127
+ puts weather.current.temperture
128
+
129
+ # OR, even more raw
130
+
131
+ measurement = Barometer::Measurement.new
132
+ weather = Barometer::Wunderground.measure_all(measurement, "Paris")
133
+
134
+ puts weather.current.temperture
135
+
136
+
137
+ NOTE: The disadvantage to using the drivers directly is that you lose the
138
+ advantage of redundancy/failover added by the Module as a whole.
139
+
140
+ NOTE: You still must create the Barometer::Query object with your query
141
+ string instead of directly feeding the query string to the service (as in
142
+ bootstrap example #1). The Barometer::Query object has behavior required
143
+ by the service that a regular String doesn't have. Using a driver directly
144
+ WILL accept a String (as in bootstrap example #2).
145
+
146
+ == searching
147
+
148
+ After you have measured the data, Barometer provides several methods to help
149
+ you get the data you are after. All examples assume you already have measured
150
+ the data as shown in the above examples.
151
+
152
+ === by preference (default service)
153
+
154
+ weather.default # returns measurement for default source
155
+ weather.current # returns current_measurement for default
156
+ weather.now # returns current_measurement for default
157
+ weather.forecast # returns all forecast_measurements for default
158
+ weather.today # returns forecast_measurement for default today
159
+ weather.tomorrow # returns forecast_measurement for default tomorrow
160
+
161
+ puts weather.now.temperature.c
162
+ puts weather.tomorrow.high.c
163
+
164
+ === by source
165
+
166
+ weather.source(:wunderground) # returns measurement for specified source
167
+ weather.sources # lists all successful sources
168
+
169
+ puts weather.source(:wunderground).current.temperature.c
170
+
171
+ === by date
172
+
173
+ # note, the date is the date of the locations weather, not the date of the
174
+ # user measuring the weather
175
+ date = Date.parse("01-01-2009")
176
+ weather.for(date) # returns forecast_measurement for default on date
177
+ weather.source(:wunderground).for(date) # same as above but specific source
178
+
179
+ puts weather.source(:wunderground).for(date).high.c
180
+
181
+ === by time
182
+
183
+ # note, the time is the time of the locations weather, not the time of the
184
+ # user measuring the weather
185
+ time = Time.parse("13:00 01-01-2009")
186
+ weather.for(time) # returns forecast_measurement for default at time
187
+ weather.source(:wunderground).for(time) # same as above but specific source
188
+
189
+ puts weather.source(:wunderground).for(time).low.f
190
+
191
+ == simple answers
192
+
193
+ After you have measured the data, Barometer provides several "simple answer"
194
+ methods to help you get answers to some basic questions. All examples assume
195
+ you already have measured the data as shown in the above examples.
196
+
197
+ All of these questions are ultimately specific to the weather source(s) you
198
+ are configured to use. All sources that have successfully measured data
199
+ will be asked, but if there is no data that can answer the question then
200
+ there will be no answer.
201
+
202
+ === is it windy?
203
+
204
+ # 1st parameter is the threshold wind speed for being windy
205
+ # 2nd parameter is the utc_time for which you want to know the answer,
206
+ # this defaults to the current time
207
+ # NOTE: in my example the values are metric, so the threshold is 10 kph
208
+
209
+ weather.windy?(10)
210
+
211
+ === is it wet?
212
+
213
+ # 1st parameter is the threshold pop (%) for being wet
214
+ # 2nd parameter is the utc_time for which you want to know the answer,
215
+ # this defaults to the current time
216
+ # NOTE: in my example the threshold is 50 %
217
+
218
+ weather.wet?(50)
219
+
220
+ === is it sunny?
221
+
222
+ # 1st parameter is the utc_time for which you want to know the answer,
223
+ # this defaults to the current time
224
+
225
+ weather.sunny?
226
+
227
+ === is it day?
228
+
229
+ # 1st parameter is the utc_time for which you want to know the answer,
230
+ # this defaults to the current time
231
+
232
+ weather.day?
233
+
234
+ === is it night?
235
+
236
+ # 1st parameter is the utc_time for which you want to know the answer,
237
+ # this defaults to the current time
238
+
239
+ weather.night?
240
+
241
+ = design
242
+
243
+ - create a Barometer instance
244
+ - supply a query, there are very little restrictions on the format:
245
+ - city, country, specific address (basically anything Google will geocode)
246
+ - US zip code (skips geocoding if weather service accepts this directly)
247
+ - postal code (skips geocoding if weather service accepts this directly)
248
+ - latitude and longitude (skips geocoding if weather service accepts this
249
+ directly)
250
+ - TODO: international airport code (skips geocoding if weather service
251
+ accepts this directly)
252
+ - if geocoding required, geocode the query
253
+ - determine which weather services will be queried (one or multiple)
254
+ - query the weather services
255
+ - save the data
256
+ - repeat weather service queries as needed
257
+
258
+ = extending
259
+
260
+ Barometer attempts to be a common API to any weather service API. I have included
261
+ several weather service 'drivers', but I know there are many more available.
262
+ Please use the provided ones as examples to create more.
263
+
264
+ == copyright
265
+
266
+ Copyright (c) 2009 Mark G. See LICENSE for details.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
data/bin/barometer ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/barometer'
4
+ #require 'rubygems'
5
+ #require 'attack-barometer'
6
+
7
+ # TODO
8
+
9
+ # set default Google Key ... maybe need a config file?
10
+ Barometer.google_geocode_key = "ABQIAAAAq8TH4offRcGrok8JVY_MyxRi_j0U6kJrkFvY4-OX2XYmEAa76BSFwMlSow1YgX8BOPUeve_shMG7xw"
11
+
12
+ # take command line paramters
13
+ # service: --yahoo, --wunderground, --google
14
+ # units: -m --metric, -i --imperial
15
+ # geocode: -g --geocode (force geocode)
16
+ # timeout: -t 15 --timeout 15
17
+ # skip: --skip (skip graticule)
18
+ # help: -h --help
19
+ # wet threshold: -w --wet
20
+ # windy threshold: -v --wind
21
+ # pop threshold: -p --pop
22
+ # time: -a --at
23
+
24
+ # prettier output
25
+ # show simple answers
26
+ # more help
27
+ # error display (out of sources, etc.)
28
+
29
+ if ARGV.size == 0
30
+ puts 'Barometer [Powered by wunderground]'
31
+ puts 'USAGE: barometer [query]'
32
+ puts 'EXAMPLES:'
33
+ puts ' barometer paris'
34
+ exit
35
+ end
36
+
37
+ barometer = Barometer.new(ARGV[0])
38
+ weather = barometer.measure(true)
39
+
40
+ def y(value)
41
+ value ? "yes" : "no"
42
+ end
43
+
44
+ if weather
45
+ puts "###################################################"
46
+ puts "# #{weather.default.location.name}"
47
+ puts "#"
48
+ puts "# (lat: #{weather.default.location.latitude}, long: #{weather.default.location.longitude})"
49
+ puts "###################################################"
50
+ puts " -- CURRENT --"
51
+ puts " temperature: #{weather.now.temperature}"
52
+ puts
53
+ puts " -- QUESTIONS --"
54
+ puts " day? : #{y(weather.day?)}"
55
+ puts " sunny?: #{y(weather.sunny?)}"
56
+ puts " windy?: #{y(weather.windy?)}"
57
+ puts " wet? : #{y(weather.wet?)}"
58
+ puts
59
+ puts
60
+ puts " -- INFO --"
61
+ puts " http://github.com/attack/barometer"
62
+ puts "---------------------------------------------------"
63
+ end
@@ -0,0 +1,52 @@
1
+ module Barometer
2
+
3
+ class Base
4
+
5
+ # allow the configuration of specific weather APIs to be used,
6
+ # and the order in which they would be used
7
+ @@selection = { 1 => [:wunderground] }
8
+ def self.selection; @@selection; end;
9
+ def self.selection=(hash); @@selection = hash; end;
10
+
11
+ attr_reader :query
12
+ attr_accessor :weather, :success
13
+
14
+ def initialize(query=nil)
15
+ @query = Barometer::Query.new(query)
16
+ @weather = Barometer::Weather.new
17
+ @success = false
18
+ end
19
+
20
+ def measure(metric=nil)
21
+ return nil unless @query
22
+
23
+ level = 1
24
+ until self.success?
25
+ if sources = @@selection[level]
26
+ if sources.is_a?(Array)
27
+ sources.each do |source|
28
+ measurement = Barometer.source(source.to_sym).measure(@query, metric)
29
+ @success = true if measurement.success?
30
+ @weather.measurements << measurement
31
+ end
32
+ else
33
+ measurement = Barometer.source(sources.to_sym).measure(@query, metric)
34
+ @success = true if measurement.success?
35
+ @weather.measurements << measurement
36
+ end
37
+ else
38
+ raise OutOfSources
39
+ end
40
+ level += 1
41
+ end
42
+
43
+ @weather
44
+ end
45
+
46
+ def success?
47
+ @success
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -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