roker 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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