roker 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/lib/numeric.rb +44 -0
- data/lib/roker.rb +173 -0
- data/lib/time.rb +121 -0
- data/lib/time_layout.rb +97 -0
- data/lib/time_span.rb +59 -0
- data/lib/weather_parameter.rb +68 -0
- data/roker +42 -0
- data/roker.gemspec +75 -0
- data/test/helper.rb +17 -0
- data/test/test_roker.rb +94 -0
- data/test/test_time.rb +84 -0
- data/test/test_time_layout.rb +155 -0
- data/test/test_time_span.rb +91 -0
- data/test/test_weather_parameter.rb +135 -0
- data/test/weather_xml.xml +177 -0
- metadata +110 -0
data/.document
ADDED
data/.gitignore
ADDED
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
|
data/lib/time_layout.rb
ADDED
@@ -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
|