roker 0.0.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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Greg
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,17 @@
1
+ = roker
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2009 Greg. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "roker"
8
+ gem.summary = %Q{Weather forecasts from weather.gov}
9
+ gem.description = %Q{Weather forecasts from weather.gov}
10
+ gem.email = "gsterndale@gmail.com"
11
+ gem.homepage = "http://github.com/gsterndale/roker"
12
+ gem.authors = ["Greg Sterndale"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_development_dependency "mocha", ">= 0.9.1"
15
+ gem.add_development_dependency "hpricot", ">= 0.6.164"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << 'lib' << 'test'
26
+ test.pattern = 'test/**/test_*.rb'
27
+ test.verbose = true
28
+ end
29
+
30
+ begin
31
+ require 'rcov/rcovtask'
32
+ Rcov::RcovTask.new do |test|
33
+ test.libs << 'test'
34
+ test.pattern = 'test/**/test_*.rb'
35
+ test.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :test => :check_dependencies
44
+
45
+ task :default => :test
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "roker #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
data/lib/numeric.rb ADDED
@@ -0,0 +1,44 @@
1
+ class Numeric
2
+ SECONDS_PER_MINUTE = 60.0
3
+ SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60
4
+ SECONDS_PER_DAY = SECONDS_PER_HOUR * 24
5
+ SECONDS_PER_WEEK = SECONDS_PER_DAY * 7
6
+ SECONDS_PER_MONTH = SECONDS_PER_DAY * 30
7
+ SECONDS_PER_YEAR = SECONDS_PER_DAY * 365.25
8
+
9
+ def years
10
+ self * SECONDS_PER_YEAR
11
+ end
12
+ alias_method :year, :years
13
+
14
+ def months
15
+ self * SECONDS_PER_MONTH
16
+ end
17
+ alias_method :month, :months
18
+
19
+ def weeks
20
+ self * SECONDS_PER_WEEK
21
+ end
22
+ alias_method :week, :weeks
23
+
24
+ def days
25
+ self * SECONDS_PER_DAY
26
+ end
27
+ alias_method :day, :days
28
+
29
+ def hours
30
+ self * SECONDS_PER_HOUR
31
+ end
32
+ alias_method :hour, :hours
33
+
34
+ def minutes
35
+ self * SECONDS_PER_MINUTE
36
+ end
37
+ alias_method :minute, :minutes
38
+
39
+ def seconds
40
+ self
41
+ end
42
+ alias_method :second, :seconds
43
+
44
+ end
data/lib/roker.rb ADDED
@@ -0,0 +1,173 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'soap/wsdlDriver'
4
+ require 'xsd/mapping'
5
+ require 'uri'
6
+ require 'open-uri'
7
+ require File.dirname(__FILE__) + '/numeric'
8
+ require File.dirname(__FILE__) + '/time'
9
+ require File.dirname(__FILE__) + '/time_layout'
10
+ require File.dirname(__FILE__) + '/time_span'
11
+ require File.dirname(__FILE__) + '/weather_parameter'
12
+
13
+ class ServiceError < RuntimeError; end
14
+
15
+ class Roker
16
+
17
+ attr_accessor :lat, :lng, :started_at, :ended_at
18
+
19
+ WSDL_URL = "http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl"
20
+ WSDL_PARAMETERS = { :maxt => true, :mint => true, :temp => true, :dew => true, :appt => false, :pop12 => true, :qpf => true, :snow => false, :sky => true, :rh => true, :wspd => true, :wdir => true, :wx => false, :icons => false, :waveh => true, :incw34 => false, :incw50 => false, :incw64 => false, :cumw34 => false, :cumw50 => false, :cumw64 => false, :wgust => false, :conhazo => false, :ptornado => false, :phail => false, :ptstmwinds => false, :pxtornado => false, :pxhail => false, :pxtstmwinds => false, :ptotsvrtstm => false, :pxtotsvrtstm =>false}
21
+ WSDL_PRODUCT = "time-series"
22
+
23
+ def initialize(attributes={})
24
+ attributes.each do |key, value|
25
+ self.send("#{key}=", value)
26
+ end
27
+ self.started_at, self.ended_at = self.ended_at, self.started_at if self.ended_at && self.started_at > self.ended_at
28
+ end
29
+
30
+
31
+ def weather_forecasts_attributes
32
+ @weather_forecasts_attributes ||= find_weather_forecasts_attributes
33
+ end
34
+
35
+ def find_weather_forecasts_attributes
36
+ weather_forecasts_attributes = []
37
+ interval = shortest_time_span
38
+ self.started_at.upto(self.ended_at, interval, false) do |current_started_at|
39
+ current_time_span = TimeSpan.new(:start_at => current_started_at, :duration => interval)
40
+ current_weather_forecast_attributes = {
41
+ :lat => self.lat,
42
+ :lng => self.lng,
43
+ :started_at => current_time_span.start_at,
44
+ :ended_at => current_time_span.end_at
45
+ }
46
+ parameters.each do |key, parameter|
47
+ current_weather_forecast_attributes[key] = parameter.value_at(current_time_span) if parameter
48
+ end
49
+ weather_forecasts_attributes << current_weather_forecast_attributes
50
+ end
51
+ weather_forecasts_attributes
52
+ end
53
+
54
+ def time_layouts
55
+ @time_layouts ||= parse_time_layouts
56
+ end
57
+
58
+ def parse_time_layouts
59
+ tls = {}
60
+ self.weather_doc.search('time-layout').each do |tl|
61
+ key = (tl/'layout-key').first.inner_html
62
+
63
+ start_at = []
64
+ tl.search('start-valid-time').each do |s|
65
+ start_at << self.class.parse_time(s.inner_html)
66
+ end
67
+
68
+ end_at = []
69
+ tl.search('end-valid-time').each do |e|
70
+ end_at << self.class.parse_time(e.inner_html)
71
+ end
72
+
73
+ tls[key] = TimeLayout.new(:start_at => start_at, :end_at => end_at, :key => key)
74
+ end
75
+ tls
76
+ end
77
+
78
+ def shortest_time_span
79
+ shorty = nil
80
+ self.time_layouts.each do |key, time_layout|
81
+ shorty = time_layout.interval if (shorty.nil? || shorty > time_layout.interval)
82
+ end
83
+ shorty
84
+ end
85
+
86
+ def parameters
87
+ @parameters ||= parse_parameters
88
+ end
89
+
90
+
91
+ def parse_parameters
92
+ {
93
+ :maximum_temperature => self.parse_parameter("temperature[@type='maximum']"),
94
+ :minimum_temperature => self.parse_parameter("temperature[@type='minimum']"),
95
+ :temperature => self.parse_parameter("temperature[@type='hourly']"),
96
+ :dewpoint_temperature => self.parse_parameter("temperature[@type='dew point']"),
97
+ :liquid_precipitation => self.parse_parameter("precipitation[@type='liquid']"),
98
+ :probability_of_precipitation => self.parse_parameter("probability-of-precipitation"),
99
+ :wind_speed => self.parse_parameter("wind-speed[@type='sustained']"),
100
+ :wind_direction => self.parse_parameter("direction[@type='wind']"),
101
+ :cloud_cover => self.parse_parameter("cloud-amount[@type='total']"),
102
+ :relative_humidity => self.parse_parameter("humidity[@type='relative']"),
103
+ :wave_height => self.parse_parameter("water-state", "waves[@type='significant']")
104
+ }
105
+ end
106
+
107
+ def parse_parameter(xpath, value_xpath=nil, calculation_method=nil)
108
+ element = self.weather_doc.at(xpath)
109
+ if element
110
+ time_layout_key = element.attributes['time-layout']
111
+ time_layout = self.time_layouts[time_layout_key]
112
+ values = []
113
+ element = element.at(value_xpath) if value_xpath
114
+ element.search('value').each do |val|
115
+ values << val.inner_html.to_f
116
+ end
117
+ WeatherParameter.new(:time_layout => time_layout, :values => values, :calculation_method => calculation_method)
118
+ end
119
+ end
120
+
121
+ def weather_doc
122
+ @weather_doc ||= Hpricot::XML(self.weather_xml)
123
+ rescue Timeout::Error,
124
+ Errno::EINVAL,
125
+ Errno::ECONNRESET,
126
+ EOFError,
127
+ Net::HTTPBadResponse,
128
+ Net::HTTPHeaderSyntaxError,
129
+ Net::ProtocolError => e
130
+ service_error = ServiceError.new
131
+ service_error.set_backtrace(e.backtrace)
132
+ raise service_error
133
+ end
134
+
135
+ def weather_xml
136
+ @weather_xml ||= weather_xml_soap
137
+ # TODO can I do away with the weather_xml_soap and just use a url and URI.parse?
138
+ # @weather_xml ||= weather_xml_uri
139
+ end
140
+
141
+ protected
142
+
143
+ def weather_xml_soap
144
+ soap_driver = SOAP::WSDLDriverFactory.new(WSDL_URL).create_rpc_driver
145
+ soap_driver.NDFDgen(self.lat, self.lng, WSDL_PRODUCT, self.started_at.strftime("%Y-%m-%dT%H:%M:%S-05:00"), self.ended_at.strftime("%Y-%m-%dT%H:%M:%S-05:00"), WSDL_PARAMETERS)
146
+ end
147
+
148
+ def weather_xml_uri
149
+ URI.parse(self.service_url).read
150
+ end
151
+
152
+ def num_days
153
+ return 1 unless self.ended_at
154
+ ((self.ended_at.beginning_of_day-self.started_at.beginning_of_day)/1.day).ceil
155
+ end
156
+
157
+ def service_url
158
+ sprintf("http://www.weather.gov/forecasts/xml/sample_products/" +
159
+ "browser_interface/ndfdBrowserClientByDay.php?" +
160
+ "&format=24+hourly&numDays=%s&lat=%s&lon=%s&startDate=%s",
161
+ num_days, self.lat, self.lng, self.started_at.to_s(:weather))
162
+ end
163
+
164
+ def self.parse_time(str='')
165
+ # "2008-11-24T07:00:00-05:00"
166
+ str =~ /(....)-(..)-(..)T(..):(..):(..)-(..):(..)/i
167
+ Time.mktime($1, $2, $3, $4, $5, $6, $7, $8)
168
+ # Time.parse(str)
169
+ rescue
170
+ raise ServiceError, "Unsupported time format #{str}"
171
+ end
172
+
173
+ end
data/lib/time.rb ADDED
@@ -0,0 +1,121 @@
1
+ class Time
2
+ def beginning_of_year
3
+ self-(self.yday-1).days-(self.hour).hours-(self.min).minutes-self.sec
4
+ end
5
+ def end_of_year
6
+ self.beginning_of_year + 365.days - 1.second
7
+ end
8
+ def beginning_of_month
9
+ self-(self.mday-1).days-(self.hour).hours-(self.min).minutes-self.sec
10
+ end
11
+ def end_of_month
12
+ self.beginning_of_month + 1.month - 1.second
13
+ end
14
+ def beginning_of_week
15
+ self-(self.wday).days-(self.hour).hours-(self.min).minutes-self.sec
16
+ end
17
+ def end_of_week
18
+ self.beginning_of_week + 1.week - 1.second
19
+ end
20
+ def beginning_of_day
21
+ self-(self.hour).hours-(self.min).minutes-self.sec
22
+ end
23
+ def end_of_day
24
+ self.beginning_of_day + 24.hours - 1.second
25
+ end
26
+ def beginning_of_hour
27
+ self-(self.min).minutes-self.sec
28
+ # Time.at((self.to_i/1.hour).floor*1.hour)
29
+ end
30
+ def end_of_hour
31
+ self.beginning_of_hour + 1.hour - 1.second
32
+ # Time.at((self.to_i/1.hour).floor*1.hour+1.hour)
33
+ end
34
+ def beginning_of_minute
35
+ self-self.sec
36
+ # Time.at((self.to_i/1.minute).floor*1.minute)
37
+ end
38
+ def end_of_minute
39
+ self.beginning_of_minute + 1.minute - 1.second
40
+ # Time.at((self.to_i/1.minute).floor*5.minute)
41
+ end
42
+
43
+ def beginning_of(secs=1.day)
44
+ case secs
45
+ when 1.hour
46
+ self.beginning_of_hour
47
+ when 1.day
48
+ self.beginning_of_day
49
+ when 1.week
50
+ self.beginning_of_week
51
+ when 1.month
52
+ self.beginning_of_month
53
+ when 1.year
54
+ self.beginning_of_year
55
+ else
56
+ if secs < 1.day
57
+ self.beginning_of_period(secs)
58
+ else
59
+ return self
60
+ end
61
+ end
62
+ end
63
+
64
+ def end_of(secs=1.day)
65
+ case secs
66
+ when 1.hour
67
+ self.end_of_hour
68
+ when 1.day
69
+ self.end_of_day
70
+ when 1.week
71
+ self.end_of_week
72
+ when 1.month
73
+ self.end_of_month
74
+ when 1.year
75
+ self.end_of_year
76
+ else
77
+ if secs < 1.day
78
+ self.end_of_period(secs)
79
+ else
80
+ return self
81
+ end
82
+ end
83
+ end
84
+
85
+ def beginning_of_period(secs=5.minutes)
86
+ return self if secs >= 1.day/2
87
+ secs_today = self - self.beginning_of_day
88
+ periods_today = (secs_today/secs).floor
89
+ self.beginning_of_day + periods_today * secs
90
+ # Time.at((self.to_i/seconds).floor*seconds)
91
+ end
92
+ def end_of_period(secs=5.minutes)
93
+ return self if secs >= 1.day/2
94
+ beginning_of(secs) + secs - 1.second
95
+ # Time.at((self.to_i/seconds).floor*seconds+seconds)
96
+ end
97
+
98
+ def upto(max, interval=1.day, inclusive=true)
99
+ t = self
100
+ while t < max || (inclusive && t <= max)
101
+ yield t
102
+ t += interval
103
+ end
104
+ end
105
+
106
+ protected
107
+
108
+ def change(options)
109
+ self.class.send(
110
+ self.utc? ? :utc_time : :local_time,
111
+ options[:year] || self.year,
112
+ options[:month] || self.month,
113
+ options[:day] || self.day,
114
+ options[:hour] || self.hour,
115
+ options[:min] || (options[:hour] ? 0 : self.min),
116
+ options[:sec] || ((options[:hour] || options[:min]) ? 0 : self.sec),
117
+ options[:usec] || ((options[:hour] || options[:min] || options[:sec]) ? 0 : self.usec)
118
+ )
119
+ end
120
+
121
+ end
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + '/numeric'
2
+ require File.dirname(__FILE__) + '/time_span'
3
+
4
+ # assert interval is constant
5
+ # assert start_at of one TimeSpan can equal end_at of another
6
+ class TimeLayout
7
+ attr_accessor :key
8
+ attr_reader :time_spans
9
+
10
+ DEFAULT_INTERVAL = 1.day
11
+
12
+ # :time_spans or :start_at required
13
+ def initialize(args)
14
+ @key = args[:key]
15
+ if args[:time_spans] and args[:time_spans].is_a?(Array)
16
+ @time_spans = args[:time_spans]
17
+ else
18
+ @time_spans = []
19
+ if args[:end_at].nil? || args[:end_at].is_a?(Array) && args[:end_at].compact.empty?
20
+ @interval = calculate_interval_from_times(args[:start_at])
21
+ args[:end_at] = nil
22
+ end
23
+ args[:start_at].each_with_index do |start_at,i|
24
+ if args[:end_at]
25
+ @time_spans << TimeSpan.new(:start_at => start_at, :end_at => args[:end_at][i])
26
+ else
27
+ @time_spans << TimeSpan.new(:start_at => start_at, :duration => self.interval)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def index_at(t)
34
+ @time_spans.each_with_index do |time_span,i|
35
+ return i if time_span.envelopes?(t, false)
36
+ end
37
+ # if t falls on the end of a time_span and not the start of any others
38
+ @time_spans.each_with_index do |time_span,i|
39
+ return i if time_span.envelopes?(t, true)
40
+ end
41
+ nil
42
+ end
43
+
44
+ def indices_enveloping(time_span)
45
+ indices = []
46
+ weights = []
47
+ @time_spans.each_with_index do |ts,i|
48
+ if ts.overlaps?(time_span, false)
49
+ indices << i
50
+ weights << ts.overlap_by(time_span, false)
51
+ end
52
+ end
53
+ return indices, weights
54
+ end
55
+
56
+ def [](i)
57
+ @time_spans[i] if (@time_spans && @time_spans.is_a?(Array) && i >= 0 && i < @time_spans.length)
58
+ end
59
+
60
+ def start_at # of entire TimeLayout
61
+ @start_at ||= self.time_spans.inject(nil){|memo,time_span| (memo && memo < time_span.start_at) ? memo : time_span.start_at }
62
+ end
63
+
64
+ def end_at # of entire TimeLayout
65
+ @end_at ||= self.time_spans.inject(nil){|memo,time_span| (memo && memo > time_span.end_at) ? memo : time_span.end_at }
66
+ end
67
+
68
+ def interval
69
+ @interval ||= (@time_spans.first.duration || calculate_interval_from_time_spans)
70
+ end
71
+
72
+ def length
73
+ @length ||= @time_spans.length
74
+ end
75
+
76
+ def time_spans=(arr=[])
77
+ @interval = nil
78
+ @start_at = nil
79
+ @end_at = nil
80
+ @time_spans = arr
81
+ end
82
+
83
+ def duration
84
+ self.end_at - self.start_at
85
+ end
86
+
87
+ protected
88
+
89
+ def calculate_interval_from_times(times=[])
90
+ times.length < 2 ? DEFAULT_INTERVAL : times[1] - times[0]
91
+ end
92
+
93
+ def calculate_interval_from_time_spans
94
+ time_spans.length < 2 ? DEFAULT_INTERVAL : time_spans[1].start_at - time_spans[0].start_at
95
+ end
96
+
97
+ end
data/lib/time_span.rb ADDED
@@ -0,0 +1,59 @@
1
+ require File.dirname(__FILE__) + '/numeric'
2
+
3
+ class TimeSpan
4
+ attr_accessor :start_at, :end_at, :duration
5
+
6
+ def initialize(args={})
7
+ @start_at = args[:start_at]
8
+
9
+ if @end_at = args[:end_at]
10
+ @end_at, @start_at = @start_at, @end_at if @start_at > @end_at
11
+ @duration = @end_at - @start_at
12
+ elsif @duration = args[:duration]
13
+ @end_at = @start_at + @duration
14
+ end
15
+ end
16
+
17
+ def first
18
+ @start_at
19
+ end
20
+
21
+ def last
22
+ @end_at
23
+ end
24
+
25
+ def envelopes?(t, include_end=true)
26
+ t >= @start_at && ( t < @end_at || (include_end && t <= @end_at) )
27
+ end
28
+
29
+ # TODO
30
+ def enveloped_by?(args, include_end=true)
31
+ case
32
+ when args.is_a?(TimeSpan)
33
+ when args.is_a?(Range)
34
+ when args.is_a?(Array)
35
+ end
36
+ end
37
+
38
+ def overlaps?(t, include_end=true)
39
+ if include_end
40
+ t.end_at >= @start_at && t.start_at <= @end_at
41
+ else
42
+ t.end_at > @start_at && t.start_at < @end_at
43
+ end
44
+ end
45
+
46
+ # ratio: overlap/duration
47
+ def overlap_by(t, include_end=true)
48
+ self.overlap(t, include_end)/@duration.to_f
49
+ end
50
+
51
+ def overlap(t, include_end=true)
52
+ return 0 unless self.overlaps?(t, include_end)
53
+ e = t.end_at > @end_at ? @end_at : t.end_at
54
+ s = t.start_at < @start_at ? @start_at : t.start_at
55
+ o = e-s
56
+ !include_end && o <= 1 ? 0.0 : o
57
+ end
58
+
59
+ end
@@ -0,0 +1,68 @@
1
+ require File.dirname(__FILE__) + '/numeric'
2
+ require File.dirname(__FILE__) + '/time_layout'
3
+ require File.dirname(__FILE__) + '/time_span'
4
+
5
+ class WeatherParameter
6
+ attr_accessor :time_layout, :values, :calculation_method
7
+
8
+ DEFAULT_CALCULATION_METHOD = :mean
9
+
10
+ def initialize(args)
11
+ @time_layout = args[:time_layout]
12
+ @values = args[:values]
13
+ @calculation_method = args[:calculation_method] || DEFAULT_CALCULATION_METHOD
14
+ end
15
+
16
+ def [](i)
17
+ @values[i] if (@values && @values.is_a?(Array) && i >= 0 && i < @values.length)
18
+ end
19
+
20
+ def value_at(t)
21
+ if t.is_a?(TimeSpan)
22
+ self.send @calculation_method, t
23
+ else
24
+ self[@time_layout.index_at(t)]
25
+ end
26
+ end
27
+
28
+ protected
29
+
30
+ def mean(t)
31
+ indices, weights = @time_layout.indices_enveloping(t)
32
+ return nil if indices.nil? || indices.empty?
33
+ values = indices.inject([]) {|memo,i| memo << @values[i]; memo }
34
+ sum = 0.0
35
+ values.each_with_index{|value,i| sum = sum + value * weights[i] }
36
+ total_weight = weights.inject{|total,w| total + w }
37
+ sum/total_weight
38
+ end
39
+
40
+ def first(t)
41
+ indices, weights = @time_layout.indices_enveloping(t)
42
+ return nil if indices.nil? || indices.empty?
43
+ values[indices.first]
44
+ end
45
+
46
+ def max(t)
47
+ indices, weights = @time_layout.indices_enveloping(t)
48
+ return nil if indices.nil? || indices.empty?
49
+ indices.map{|i| values[i] }.max
50
+ end
51
+
52
+ # def min(t)
53
+ #
54
+ # end
55
+ #
56
+ # def median(t)
57
+ #
58
+ # end
59
+ #
60
+ # def mode(t)
61
+ #
62
+ # end
63
+ #
64
+ # def last(t)
65
+ #
66
+ # end
67
+
68
+ end