ce-bucketize 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.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +241 -0
- data/Rakefile +2 -0
- data/ce-bucketize.gemspec +35 -0
- data/lib/ce-bucketize/bucketizer.rb +134 -0
- data/lib/ce-bucketize/daily_data.rb +41 -0
- data/lib/ce-bucketize/hourly_values.rb +251 -0
- data/lib/ce-bucketize/model/hour_cost.rb +27 -0
- data/lib/ce-bucketize/model/hour_value.rb +21 -0
- data/lib/ce-bucketize/model/tariff_rule.rb +134 -0
- data/lib/ce-bucketize/tariffed_hourly_costs.rb +50 -0
- data/lib/ce-bucketize/tariffed_hourly_values.rb +46 -0
- data/lib/ce-bucketize/version.rb +5 -0
- data/lib/ce-bucketize.rb +341 -0
- metadata +161 -0
@@ -0,0 +1,251 @@
|
|
1
|
+
# Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
|
2
|
+
|
3
|
+
require 'ce-bucketize/model/hour_value'
|
4
|
+
require 'ce-greenbutton/model/gb_data_reading'
|
5
|
+
require 'interpolate'
|
6
|
+
|
7
|
+
module Bucketize
|
8
|
+
# An Enumerable for hourly consumption values. The values are aggregated from
|
9
|
+
# the raw GreenButton data.
|
10
|
+
#
|
11
|
+
# Author: ahmed.seddiq
|
12
|
+
# Version: 1.0
|
13
|
+
class HourlyValues
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
# hour in seconds
|
17
|
+
HOUR = 3600
|
18
|
+
|
19
|
+
# The length (in seconds) of the maximum allowed gap in input data.
|
20
|
+
attr_accessor :max_gap_length
|
21
|
+
|
22
|
+
# This is a readonly attribute that will hold the combined interpolated
|
23
|
+
# raw data. It will be initialized in the constructor.
|
24
|
+
attr_reader :readings
|
25
|
+
|
26
|
+
|
27
|
+
# Creates a new instance of the HourlyValues collection.
|
28
|
+
#
|
29
|
+
# gb_data_description - the input GreenButton data.
|
30
|
+
# from - the start Time, must represent an hour start.
|
31
|
+
# to - the end Time, must represent an hour start.
|
32
|
+
#
|
33
|
+
# Raises ArgumentError if from or to don't represent an hour start.
|
34
|
+
def initialize(gb_data_description, from=nil, to=nil)
|
35
|
+
unless from.nil? or (from.min == 0 and from.sec == 0)
|
36
|
+
raise ArgumentError, '"from" should be an hour start'
|
37
|
+
end
|
38
|
+
unless to.nil? or (to.min == 0 and to.sec == 0)
|
39
|
+
raise ArgumentError, '"to" should be an hour start'
|
40
|
+
end
|
41
|
+
|
42
|
+
@gb_data_description = gb_data_description
|
43
|
+
@from = from
|
44
|
+
@to = to
|
45
|
+
@max_gap_length = 4 * HOUR
|
46
|
+
@readings = prepare_readings
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# The "each" method required by the Enumerable mixin. It yields the hourly
|
51
|
+
# consumption values for the GreenButton data associated with this instance.
|
52
|
+
#
|
53
|
+
# Yields HourValue instances representing the hourly consumption of the data.
|
54
|
+
def each(&block)
|
55
|
+
return enum_for(:each) if block.nil?
|
56
|
+
readings = @readings.each
|
57
|
+
|
58
|
+
finished = false # will be set to true when raw data are all consumed.
|
59
|
+
cv = 0 # will aggregate current hour value
|
60
|
+
current_reading = readings.next
|
61
|
+
t0 = current_reading.time_start # the start of the first reading
|
62
|
+
d0 = current_reading.time_duration.to_f # duration of the first reading
|
63
|
+
# last reading time.
|
64
|
+
tn = @readings.last.time_start + @readings.last.time_duration - HOUR
|
65
|
+
hour_start = @from || t0 # will point to the current hour start
|
66
|
+
last_hour_start = ((@to - HOUR) unless @to.nil?) || tn # the last hour_start
|
67
|
+
d = (t0 + d0 - hour_start).to_f # remaining seconds from the current reading.
|
68
|
+
rem = (d/d0) * current_reading.value # remaining value from the current reading.
|
69
|
+
hf = 1.to_f # remaining fraction of hour for the current hour value
|
70
|
+
until finished do
|
71
|
+
# ratio to be consumed from the current reading
|
72
|
+
rat = [1.0, (hf * HOUR) / d].min
|
73
|
+
# increment current value by the ratio from the current reading
|
74
|
+
cv = cv + rat * rem
|
75
|
+
# decrement consumed value from current reading
|
76
|
+
rem = [0, rem * (1 - rat)].max
|
77
|
+
# update remaining fraction by the consumed duration
|
78
|
+
hf = [0, hf - ((rat * d)/ HOUR)].max
|
79
|
+
# update remaining duration
|
80
|
+
d = d * (1 - rat)
|
81
|
+
|
82
|
+
if hf == 0.0
|
83
|
+
# a complete hour is calculated
|
84
|
+
yield Bucketize::HourValue.new(hour_start, cv.round(2))
|
85
|
+
# reset the current hour value and fraction
|
86
|
+
cv = 0
|
87
|
+
hf = 1.to_f
|
88
|
+
hour_start += HOUR
|
89
|
+
if hour_start > last_hour_start
|
90
|
+
finished = true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
if rem == 0
|
94
|
+
# current reading is totally consumed.
|
95
|
+
begin
|
96
|
+
# get next reading
|
97
|
+
current_reading = readings.next
|
98
|
+
d = current_reading.time_duration.to_f
|
99
|
+
rem = current_reading.value
|
100
|
+
rescue StopIteration
|
101
|
+
finished = true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Internal: this method will combine, sort, check for gaps and interpolates
|
108
|
+
# the raw data in the @gb_data_description.
|
109
|
+
#
|
110
|
+
# Returns the readings array; array of (GbDataReading)
|
111
|
+
#
|
112
|
+
# Raises TooManyDataMissingError if there is more than @max_gap_length
|
113
|
+
# missing data
|
114
|
+
#
|
115
|
+
private
|
116
|
+
def prepare_readings
|
117
|
+
# Combine all interval_readings in all gb_data instances.
|
118
|
+
readings = @gb_data_description.gb_data_array.reduce([]) do |all, gb_data|
|
119
|
+
all + gb_data.interval_readings
|
120
|
+
end
|
121
|
+
# Sort by reading time start
|
122
|
+
readings.sort_by! { |reading| reading.time_start }
|
123
|
+
|
124
|
+
# Compute average value per second
|
125
|
+
sec_avg = readings.reduce(0) do |avg, r|
|
126
|
+
avg + (r.value.to_f / r.time_duration.to_f)
|
127
|
+
end / readings.length
|
128
|
+
|
129
|
+
# check first reading
|
130
|
+
first_time = readings.first.time_start
|
131
|
+
if first_time.to_i % HOUR != 0
|
132
|
+
# add first reading to cover the missing part of the first hour.
|
133
|
+
first_reading = GbDataReading.new
|
134
|
+
first_reading.time_start = first_time - first_time.to_i % HOUR
|
135
|
+
first_reading.time_duration = first_time.to_i % HOUR
|
136
|
+
first_reading.value = (first_reading.time_duration * sec_avg).round(2)
|
137
|
+
readings = [first_reading] + readings
|
138
|
+
end
|
139
|
+
# check gaps
|
140
|
+
gaps = {}
|
141
|
+
readings.each_with_index do |reading, i|
|
142
|
+
next_time = reading.time_start + reading.time_duration
|
143
|
+
if reading.value.nil? or reading.value.to_f.nan? or reading.value == 0
|
144
|
+
# a gap detected
|
145
|
+
gaps[i] = {:from => reading.time_start, :to => next_time, :replace => true}
|
146
|
+
end
|
147
|
+
|
148
|
+
next_reading = readings[i + 1]
|
149
|
+
unless next_reading.nil? or (next_reading.time_start == next_time)
|
150
|
+
# a gap detected
|
151
|
+
unless next_reading.time_start - next_time <= @max_gap_length
|
152
|
+
raise Bucketize::TooManyDataMissingError
|
153
|
+
end
|
154
|
+
gaps[i + 1] = {:from => next_time, :to => next_reading.time_start}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
# check last reading
|
158
|
+
last_time = readings.last.time_start + readings.last.time_duration
|
159
|
+
if last_time.to_i % HOUR != 0
|
160
|
+
# add last reading to cover the missing part of last hour.
|
161
|
+
last_reading = GbDataReading.new
|
162
|
+
last_reading.time_start = last_time
|
163
|
+
last_reading.time_duration = HOUR - last_time.to_i % HOUR
|
164
|
+
last_reading.value = (last_reading.time_duration * sec_avg).round(2)
|
165
|
+
readings << last_reading
|
166
|
+
end
|
167
|
+
|
168
|
+
# interpolation
|
169
|
+
interpolate_missing_data(gaps, readings)
|
170
|
+
|
171
|
+
# get start and end reading indicies
|
172
|
+
r0 = (get_reading_for(@from, readings) unless @from.nil?) || 0
|
173
|
+
rn = (get_reading_for(@to, readings) unless @to.nil?) ||
|
174
|
+
readings.length - 1
|
175
|
+
|
176
|
+
readings[r0..rn]
|
177
|
+
end
|
178
|
+
|
179
|
+
# Get the index of reading that covers the given time.
|
180
|
+
#
|
181
|
+
# time - the time to search for.
|
182
|
+
# reading - the reading array.
|
183
|
+
#
|
184
|
+
# Returns the index of the reading that covers this time
|
185
|
+
|
186
|
+
# Raises ArgumentError if not found.
|
187
|
+
def get_reading_for(time, readings)
|
188
|
+
readings.each_with_index do |reading, i|
|
189
|
+
if time.between? reading.time_start,
|
190
|
+
reading.time_start + reading.time_duration - 1
|
191
|
+
return i
|
192
|
+
end
|
193
|
+
end
|
194
|
+
raise ArgumentError, "#{time} is out of range"
|
195
|
+
end
|
196
|
+
|
197
|
+
# Internal: interpolates the given gaps in the readings.
|
198
|
+
#
|
199
|
+
# gaps - a hash {index_of_gap_in_readings => {:from=> time, :to =>time}}
|
200
|
+
# readings - the readings array
|
201
|
+
#
|
202
|
+
# Returns Nothing, the interpolated data will be injected to the readings
|
203
|
+
# array.
|
204
|
+
def interpolate_missing_data(gaps, readings)
|
205
|
+
if gaps.size > 0
|
206
|
+
# get points for interpolation
|
207
|
+
points = readings.reduce({}) do |all, reading|
|
208
|
+
unless reading.value.nil? or reading.value.to_f.nan? or
|
209
|
+
reading.value == 0
|
210
|
+
all[reading.time_start.to_i] = reading.value
|
211
|
+
end
|
212
|
+
all
|
213
|
+
end
|
214
|
+
|
215
|
+
interpolate = Interpolate::Points.new(points)
|
216
|
+
gaps_filled = 0
|
217
|
+
gaps.each_pair do |i, gap|
|
218
|
+
missing_index = i + gaps_filled # offset for previously filled gaps
|
219
|
+
if gap[:replace]
|
220
|
+
readings[missing_index].value = interpolate.at(readings[missing_index].time_start.to_i)
|
221
|
+
else
|
222
|
+
reading_before = readings[i - 1]
|
223
|
+
unless reading_before.nil?
|
224
|
+
missing_time = gap[:from]
|
225
|
+
until missing_time >= gap[:to]
|
226
|
+
new_reading = GbDataReading.new
|
227
|
+
new_reading.time_start = missing_time
|
228
|
+
new_reading.time_duration = reading_before.time_duration
|
229
|
+
new_reading.value = interpolate.at(missing_time.to_i)
|
230
|
+
readings.insert(missing_index, new_reading)
|
231
|
+
gaps_filled += 1
|
232
|
+
missing_index += 1
|
233
|
+
missing_time += reading_before.time_duration
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
# This error will be raised if large data gaps encountered while processing.
|
246
|
+
# by default, it will check for "4 hour" gaps; this can be configured by
|
247
|
+
# the "max_gap_length" attribute in seconds.
|
248
|
+
class TooManyDataMissingError < Exception
|
249
|
+
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
|
2
|
+
|
3
|
+
require 'ce-bucketize/model/hour_value'
|
4
|
+
|
5
|
+
module Bucketize
|
6
|
+
# The consumption cost for a given hour.
|
7
|
+
#
|
8
|
+
# Author: ahmed.seddiq
|
9
|
+
# Version: 1.0
|
10
|
+
class HourCost
|
11
|
+
# A Time object represents the start of the hour in the local time of the
|
12
|
+
# consumer.
|
13
|
+
attr_accessor :hour_start
|
14
|
+
|
15
|
+
# The consumption value for this hour.
|
16
|
+
attr_accessor :value
|
17
|
+
|
18
|
+
# The consumption cost.
|
19
|
+
attr_accessor :cost
|
20
|
+
|
21
|
+
def initialize(hour_start, value, cost)
|
22
|
+
@hour_start = hour_start
|
23
|
+
@value = value
|
24
|
+
@cost = cost
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
|
2
|
+
|
3
|
+
module Bucketize
|
4
|
+
# The consumption value for a given hour.
|
5
|
+
#
|
6
|
+
# Author: ahmed.seddiq
|
7
|
+
# Version: 1.0
|
8
|
+
class HourValue
|
9
|
+
# A Time object represents the start of the hour in the local time of the
|
10
|
+
# consumer.
|
11
|
+
attr_accessor :hour_start
|
12
|
+
|
13
|
+
# The consumption value for this hour.
|
14
|
+
attr_accessor :value
|
15
|
+
|
16
|
+
def initialize(hour_start, value)
|
17
|
+
@hour_start = hour_start
|
18
|
+
@value = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'ce-bucketize'
|
5
|
+
|
6
|
+
module Bucketize
|
7
|
+
# A TariffRule is the rule that defines the way in which data will be
|
8
|
+
# aggregated.
|
9
|
+
#
|
10
|
+
# It provides the "applies_to" method which checks a given point in time
|
11
|
+
# against this rule.
|
12
|
+
#
|
13
|
+
# Author: ahmed.seddiq
|
14
|
+
# Version: 1.0
|
15
|
+
class TariffRule
|
16
|
+
# The maximum integer for the current machine.
|
17
|
+
MAX_INT = (2**((0.size * 8) - 1)) - 1
|
18
|
+
|
19
|
+
# The utility ID related to this TariffRule.
|
20
|
+
attr_accessor :utility_id
|
21
|
+
|
22
|
+
# The rate class related to this TariffRule.
|
23
|
+
attr_accessor :rate_class
|
24
|
+
|
25
|
+
# The start Date for which this TariffRule is applied. Can be nil.
|
26
|
+
attr_accessor :start_date
|
27
|
+
|
28
|
+
# The end Date for which this TariffRule is applied. Can be nil.
|
29
|
+
attr_accessor :end_date
|
30
|
+
|
31
|
+
# The time start (hour in day) for which this TariffRule is applied.
|
32
|
+
# Defaults to 1
|
33
|
+
attr_accessor :time_start
|
34
|
+
|
35
|
+
# The time end (hour in day) for which this TariffRule is applied.
|
36
|
+
# Defaults to 24
|
37
|
+
attr_accessor :time_end
|
38
|
+
|
39
|
+
# An array of days for which this TariffRule is applied. Monday is 1 and
|
40
|
+
# Sunday is 7
|
41
|
+
attr_accessor :day_of_week
|
42
|
+
|
43
|
+
# The lower bound of the monthly aggregated consumption value for which this
|
44
|
+
# TariffRule is applied. Can be nil.
|
45
|
+
attr_accessor :kwh_low
|
46
|
+
|
47
|
+
# The upper bound of the monthly aggregated consumption value for which this
|
48
|
+
# TariffRule is applied. Can be nil.
|
49
|
+
attr_accessor :kwh_high
|
50
|
+
|
51
|
+
# The tariff value.
|
52
|
+
attr_accessor :tariff
|
53
|
+
|
54
|
+
# Sets the default value for attributes.
|
55
|
+
def initialize
|
56
|
+
@time_start = 1
|
57
|
+
@time_end = 24
|
58
|
+
end
|
59
|
+
|
60
|
+
# Validates the fields of this rule.
|
61
|
+
#
|
62
|
+
# Raises Bucketize::InvalidTariffError if invalid.
|
63
|
+
def validate
|
64
|
+
not_valid_err = Bucketize::InvalidTariffError
|
65
|
+
raise not_valid_err, 'tariff is required' if @tariff.nil?
|
66
|
+
raise not_valid_err, 'time_start is require' if @time_start.nil?
|
67
|
+
raise not_valid_err, 'time_end is required' if @time_end.nil?
|
68
|
+
raise not_valid_err, 'time_end < time_start' if @time_end < @time_start
|
69
|
+
raise not_valid_err, 'time not in 1..24' unless @time_end.between? 1, 24
|
70
|
+
raise not_valid_err, 'time not in 1..24' unless @time_start.between? 1, 24
|
71
|
+
raise not_valid_err, 'utility_id is required' if @utility_id.nil?
|
72
|
+
raise not_valid_err, 'rate_class is required' if @rate_class.nil?
|
73
|
+
|
74
|
+
if not @end_date.nil? and not @start_date.nil?
|
75
|
+
raise not_valid_err, 'end_date < start_date' if @end_date < @start_date
|
76
|
+
end
|
77
|
+
unless @day_of_week.nil? or @day_of_week.all? { |d| d.between? 1, 7 }
|
78
|
+
raise not_valid_err, 'day_of_week invalid, not in 1..7 range'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Public: Checks if this rule applies to the given time and aggregated
|
83
|
+
# value.
|
84
|
+
#
|
85
|
+
# time - the Time to check against this rule.
|
86
|
+
# total_kwh - the aggregated value of the consumption till the given time.
|
87
|
+
#
|
88
|
+
# Returns true if this rule applies to the given time and aggregated value.
|
89
|
+
def applies_to?(time, total_kwh=nil)
|
90
|
+
start_date = @start_date || Time.at(0).utc
|
91
|
+
end_date = @end_date || Time.at(MAX_INT)
|
92
|
+
day_of_week = @day_of_week || (1..7).to_a
|
93
|
+
kwh_low = @kwh_low || 0
|
94
|
+
kwh_high = @kwh_high || MAX_INT
|
95
|
+
|
96
|
+
wday = time.wday
|
97
|
+
wday = 7 if wday == 0 # sunday is 7 not 0
|
98
|
+
hour = time.hour + 1 # hour from 1 - 24
|
99
|
+
time.between? start_date, end_date and
|
100
|
+
hour.between? @time_start, @time_end and
|
101
|
+
day_of_week.include? wday and
|
102
|
+
(total_kwh.nil? or total_kwh.between? kwh_low, kwh_high)
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# This operator will be called by JSON.parse to decode the json to an
|
107
|
+
# instance of TariffRule
|
108
|
+
#
|
109
|
+
# key - the property name
|
110
|
+
# value - the property value
|
111
|
+
def []=(key, value)
|
112
|
+
# convert date properties
|
113
|
+
if %w(start_date end_date).include? key and !value.nil?
|
114
|
+
value = Date.parse(value)
|
115
|
+
end
|
116
|
+
# call the setter
|
117
|
+
public_send(key + '=', value)
|
118
|
+
end
|
119
|
+
|
120
|
+
# The equal operator. Two rules are equal if and only if all instance
|
121
|
+
# variables are equal.
|
122
|
+
#
|
123
|
+
# Returns whether this object equals the given object.
|
124
|
+
def ==(other)
|
125
|
+
equals = true
|
126
|
+
instance_variables.each do |property|
|
127
|
+
equals &&= (instance_variable_get(property) ==
|
128
|
+
other.instance_variable_get(property))
|
129
|
+
end
|
130
|
+
equals
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
|
2
|
+
|
3
|
+
require 'ce-bucketize/model/hour_cost'
|
4
|
+
|
5
|
+
module Bucketize
|
6
|
+
# A collection of tariffed hourly consumption costs. This collection will be
|
7
|
+
# initialized by the input HourlyValues and the TariffRule(s). This collection
|
8
|
+
# will provide the costs after applying the given TariffRule(s).
|
9
|
+
#
|
10
|
+
# Author: ahmed.seddiq
|
11
|
+
# Version: 1.0
|
12
|
+
class TariffedHourlyCosts
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
# Initializes this instance with the given hourly values and tariff rules.
|
16
|
+
#
|
17
|
+
# hourly_values - the Collection of hourly values on which the tariff rules
|
18
|
+
# will be applied.
|
19
|
+
# tariff_rules - the Array of TariffRules to be used for aggregation.
|
20
|
+
#
|
21
|
+
def initialize(hourly_values, tariff_rules)
|
22
|
+
@hourly_values = hourly_values
|
23
|
+
@tariff_rules = tariff_rules
|
24
|
+
end
|
25
|
+
|
26
|
+
# The "each" method required by the Enumerable mixin. It yields the HourCost
|
27
|
+
# instances after applying the given TariffRule(s).
|
28
|
+
#
|
29
|
+
# Yields HourCost instances representing the tariffed hourly cost of the data.
|
30
|
+
def each(&block)
|
31
|
+
return enum_for(:each) if block.nil?
|
32
|
+
|
33
|
+
total_kwh = 0
|
34
|
+
@hourly_values.each do |h_value|
|
35
|
+
total_kwh += h_value.value
|
36
|
+
matched_rule = @tariff_rules.find {|rule| rule.applies_to? h_value.hour_start, total_kwh}
|
37
|
+
tariff = 1
|
38
|
+
tariff = matched_rule.tariff unless matched_rule.nil?
|
39
|
+
yield Bucketize::HourCost.new(h_value.hour_start, h_value.value,
|
40
|
+
h_value.value * tariff)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# This error will be raised if no matched tariff found while calculating the
|
46
|
+
# costs.
|
47
|
+
class NoMatchedTariffError < Exception
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
|
2
|
+
|
3
|
+
module Bucketize
|
4
|
+
# An Enumerable for tariffed hourly consumption values. This collection will be
|
5
|
+
# initialized by the input HourlyValues and the TariffRule(s). It will provide
|
6
|
+
# the values after applying the given TariffRule(s). If no rule was matched
|
7
|
+
# , the corresponding hourly value will be returned.
|
8
|
+
#
|
9
|
+
#
|
10
|
+
# Author: ahmed.seddiq
|
11
|
+
# Version: 1.0
|
12
|
+
class TariffedHourlyValues
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
# Initializes this instance with the given hourly values and tariff rules.
|
16
|
+
#
|
17
|
+
# hourly_values - the Collection of hourly values on which the tariff rules
|
18
|
+
# will be applied.
|
19
|
+
# tariff_rules - the Array of TariffRules to be used for aggregation.
|
20
|
+
#
|
21
|
+
def initialize(hourly_values, tariff_rules)
|
22
|
+
@hourly_values = hourly_values
|
23
|
+
@tariff_rules = tariff_rules
|
24
|
+
end
|
25
|
+
|
26
|
+
# The "each" method required by the Enumerable mixin. It yields the HourCost
|
27
|
+
# instances after applying the given TariffRule(s).
|
28
|
+
#
|
29
|
+
# Yields HourCost instances representing the tariffed hourly cost of the data.
|
30
|
+
def each(&block)
|
31
|
+
return enum_for(:each) if block.nil?
|
32
|
+
|
33
|
+
total_kwh = 0
|
34
|
+
@hourly_values.each do |h_value|
|
35
|
+
total_kwh += h_value.value
|
36
|
+
@tariff_rules.each do |rule|
|
37
|
+
if rule.applies_to? h_value.hour_start, total_kwh
|
38
|
+
h_value.value = h_value.value * rule.tariff
|
39
|
+
break
|
40
|
+
end
|
41
|
+
end
|
42
|
+
yield h_value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|