green-button-data 0.1.0 → 0.1.1
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 +13 -5
- data/README.md +1 -0
- data/green-button-data.gemspec +2 -1
- data/lib/green-button-data/parser/feed.rb +1 -1
- data/lib/green-button-data/version.rb +1 -1
- data/spec/fixtures/PGEApplicationInformation.xml +43 -0
- data/spec/fixtures/PGEAuthorization.xml +25 -0
- data/spec/fixtures/PGEIntervalBlock.xml +25946 -0
- data/spec/fixtures/PGELocalTimeParameters.xml +16 -0
- data/spec/fixtures/PGEReadingType.xml +1158 -0
- data/spec/fixtures/PGEUsagePoint.xml +24 -0
- data/spec/fixtures.rb +7 -1
- data/spec/green-button-data/parser/application_information_spec.rb +154 -0
- data/spec/green-button-data/parser/authorization_spec.rb +49 -0
- data/spec/green-button-data/parser/interval_block_spec.rb +36 -0
- data/spec/green-button-data/parser/local_time_parameter_spec.rb +53 -0
- data/spec/green-button-data/parser/reading_type_spec.rb +73 -0
- data/spec/green-button-data/parser/usage_point_spec.rb +19 -0
- metadata +17 -10
@@ -0,0 +1,24 @@
|
|
1
|
+
<ns1:feed xmlns:ns1="http://www.w3.org/2005/Atom">
|
2
|
+
<ns1:id xsi:type="ns1:idType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">00000000-0000-0000-0000-000000000001</ns1:id>
|
3
|
+
<ns1:title type="text" xsi:type="ns1:textType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Green Button Usage Feed</ns1:title>
|
4
|
+
<ns1:updated xsi:type="ns1:dateTimeType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2015-08-04T16:02:03.151Z</ns1:updated>
|
5
|
+
<ns1:link href="https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Subscription/0/UsagePoint" rel="self" xsi:type="ns1:linkType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
|
6
|
+
<ns1:entry>
|
7
|
+
<ns1:id xsi:type="ns1:idType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">00000000-0000-0000-0000-000000000002</ns1:id>
|
8
|
+
<ns1:link href="https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Subscription/0/UsagePoint" rel="up" xsi:type="ns1:linkType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
|
9
|
+
<ns1:link href="https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Subscription/0/UsagePoint/1" rel="self" xsi:type="ns1:linkType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
|
10
|
+
<ns1:link href="https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Subscription/0/UsagePoint/1/MeterReading" rel="related" xsi:type="ns1:linkType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
|
11
|
+
<ns1:link href="https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Subscription/0/UsagePoint/1/UsageSummary" rel="related" xsi:type="ns1:linkType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
|
12
|
+
<ns1:link href="https://api.pge.com/GreenButtonConnect/espi/1_1/resource/LocalTimeParameters/1" rel="related" xsi:type="ns1:linkType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
|
13
|
+
<ns1:title type="text" xsi:type="ns1:textType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">Green Button Data File</ns1:title>
|
14
|
+
<ns1:published xsi:type="ns1:dateTimeType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2015-08-04T16:02:03.16Z</ns1:published>
|
15
|
+
<ns1:updated xsi:type="ns1:dateTimeType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2015-08-04T16:02:03.16Z</ns1:updated>
|
16
|
+
<ns1:content xsi:type="ns1:contentType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
17
|
+
<ns0:UsagePoint xmlns:ns0="http://naesb.org/espi">
|
18
|
+
<ns0:ServiceCategory>
|
19
|
+
<ns0:kind>0</ns0:kind>
|
20
|
+
</ns0:ServiceCategory>
|
21
|
+
</ns0:UsagePoint>
|
22
|
+
</ns1:content>
|
23
|
+
</ns1:entry>
|
24
|
+
</ns1:feed>
|
data/spec/fixtures.rb
CHANGED
@@ -5,7 +5,13 @@ module Fixtures
|
|
5
5
|
espi_interval_block: "ESPIIntervalBlock.xml",
|
6
6
|
espi_local_time_parameters: "ESPILocalTimeParameters.xml",
|
7
7
|
espi_reading_type: "ESPIReadingType.xml",
|
8
|
-
espi_usage_point: "ESPIUsagePoint.xml"
|
8
|
+
espi_usage_point: "ESPIUsagePoint.xml",
|
9
|
+
pge_application_information: "PGEApplicationInformation.xml",
|
10
|
+
pge_authorization: "PGEAuthorization.xml",
|
11
|
+
pge_interval_block: "PGEIntervalBlock.xml",
|
12
|
+
pge_local_time_parameters: "PGELocalTimeParameters.xml",
|
13
|
+
pge_reading_type: "PGEReadingType.xml",
|
14
|
+
pge_usage_point: "PGEUsagePoint.xml"
|
9
15
|
}
|
10
16
|
|
11
17
|
##
|
@@ -151,4 +151,158 @@ describe GreenButtonData::Parser::ApplicationInformation do
|
|
151
151
|
end
|
152
152
|
end
|
153
153
|
end
|
154
|
+
|
155
|
+
context "PG&E namespace" do
|
156
|
+
let(:feed) { GreenButtonData::Feed }
|
157
|
+
let :application_information do
|
158
|
+
feed.parse(pge_application_information)
|
159
|
+
.entries
|
160
|
+
.first
|
161
|
+
.content
|
162
|
+
.application_information
|
163
|
+
end
|
164
|
+
|
165
|
+
subject { application_information }
|
166
|
+
|
167
|
+
it "should parse data custodian id" do
|
168
|
+
expect(subject.data_custodian_id).to eq "PGE"
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should parse data custodian application status" do
|
172
|
+
expect(subject.data_custodian_application_status).to eq 2
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should parse data custodian scope selection URI" do
|
176
|
+
expect(subject.data_custodian_scope_selection_screen_uri).to eq "https://sharemydata.pge.com/myAuthorization/?clientId=0&verified=true"
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should parse data custodian bulk request URI" do
|
180
|
+
expect(subject.data_custodian_bulk_request_uri).to eq "https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Batch/Bulk/0"
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should parse data custodian resource endpoint" do
|
184
|
+
expect(subject.data_custodian_resource_endpoint).to eq "https://api.pge.com/GreenButtonConnect/espi/1_1/resource"
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should parse third party application type" do
|
188
|
+
expect(subject.third_party_application_type).to eq 1
|
189
|
+
end
|
190
|
+
|
191
|
+
it "should parse third party application use" do
|
192
|
+
expect(subject.third_party_application_use).to eq nil
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should parse third party application description" do
|
196
|
+
expect(subject.third_party_application_description).to eq "Verdigris is an energy intelligence solution that shows facilities managers how their buildings use energy at the equipment level--all in real time."
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should parse third party phone number" do
|
200
|
+
expect(subject.third_party_phone).to eq "6502273888"
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should parse third party scope selection URI" do
|
204
|
+
expect(subject.third_party_scope_selection_screen_uri).to eq "http://api.example.com/espi/pge"
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should parse third party user portal URI" do
|
208
|
+
expect(subject.third_party_user_portal_screen_uri).to eq "https://www.example.com"
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should parse third party notify URI" do
|
212
|
+
expect(subject.third_party_notify_uri).to eq "https://api.example.com/espi/pge/notify"
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should parse authorization server URI" do
|
216
|
+
expect(subject.authorization_server_uri).to eq "https://api.pge.com"
|
217
|
+
end
|
218
|
+
|
219
|
+
it "should parse authorization server's authorization endpoint" do
|
220
|
+
expect(subject.authorization_server_authorization_endpoint).to eq "https://api.pge.com/datacustodian/oauth/v2/authorize"
|
221
|
+
end
|
222
|
+
|
223
|
+
it "should parse authorization server's registration endpoint" do
|
224
|
+
expect(subject.authorization_server_registration_endpoint).to eq nil
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should parse authorization server's token endpoint" do
|
228
|
+
expect(subject.authorization_server_token_endpoint).to eq "https://api.pge.com/datacustodian/oauth/v2/token"
|
229
|
+
end
|
230
|
+
|
231
|
+
it "should parse token endpoint's authentication method" do
|
232
|
+
expect(subject.token_endpoint_auth_method).to eq "client_secret_basic"
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should parse client name" do
|
236
|
+
expect(subject.client_name).to eq "Verdigris Technologies, Inc."
|
237
|
+
end
|
238
|
+
|
239
|
+
it "should parse client id" do
|
240
|
+
expect(subject.client_id).to eq "verdigris"
|
241
|
+
end
|
242
|
+
|
243
|
+
it "should parse client secret" do
|
244
|
+
expect(subject.client_secret).to eq "foobar"
|
245
|
+
end
|
246
|
+
|
247
|
+
it "should parse date when client ID was issued" do
|
248
|
+
expect(subject.client_id_issued_at).to eq DateTime.new 2015, 10, 21, 16, 21, 21
|
249
|
+
end
|
250
|
+
|
251
|
+
it "should parse when client secret expires" do
|
252
|
+
expect(subject.client_secret_expires_at).to eq DateTime.new 9999, 12, 31, 23, 59, 59
|
253
|
+
end
|
254
|
+
|
255
|
+
it "should parse redirect URI" do
|
256
|
+
expect(subject.redirect_uri).to eq "http://api.example.com/espi/pge/callback"
|
257
|
+
end
|
258
|
+
|
259
|
+
it "should parse software id" do
|
260
|
+
expect(subject.software_id).to eq "NA"
|
261
|
+
end
|
262
|
+
|
263
|
+
it "should parse software version" do
|
264
|
+
expect(subject.software_version).to eq "NA"
|
265
|
+
end
|
266
|
+
|
267
|
+
it "should parse scopes" do
|
268
|
+
expect(subject.scopes.size).to eq 1
|
269
|
+
expect(subject.scopes.first).to eq "FB=1_3_4_5_8_13_14_15_18_19_31_32_35_37_38_39_40;IntervalDuration=900_3600;BlockDuration=Daily;HistoryLength=63072000;SubscriptionFrequency=Daily;"
|
270
|
+
end
|
271
|
+
|
272
|
+
it "should parse grant types" do
|
273
|
+
expect(subject.grant_types.size).to eq 3
|
274
|
+
expect(subject.grant_types.first).to eq "authorization_code"
|
275
|
+
end
|
276
|
+
|
277
|
+
it "should parse response types" do
|
278
|
+
expect(subject.response_types.size).to eq 1
|
279
|
+
expect(subject.response_types.first).to eq "code"
|
280
|
+
end
|
281
|
+
|
282
|
+
it "should parse registration_client_uri" do
|
283
|
+
expect(subject.registration_client_uri).to eq "https://api.pge.com/GreenButtonConnect/espi/1_1/resource/ApplicationInformation/00000000-2222-4444-6666-8888aaaacccc"
|
284
|
+
end
|
285
|
+
|
286
|
+
it "should parse registration_access_token" do
|
287
|
+
expect(subject.registration_access_token).to eq "00001111-2222-3333-4444-cccceeeeffff"
|
288
|
+
end
|
289
|
+
|
290
|
+
describe "#application_status" do
|
291
|
+
it "should return application status as a symbol" do
|
292
|
+
expect(subject.application_status).to eq :production
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
describe "#application_type" do
|
297
|
+
it "should return application type as a symbol" do
|
298
|
+
expect(subject.application_type).to eq :web
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
describe "#application_use" do
|
303
|
+
it "should return application use as a symbol" do
|
304
|
+
expect(subject.application_use).to eq nil
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
154
308
|
end
|
@@ -49,4 +49,53 @@ describe GreenButtonData::Parser::Authorization do
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
52
|
+
|
53
|
+
context "PG&E namespace" do
|
54
|
+
let(:feed) { GreenButtonData::Feed }
|
55
|
+
let :authorization do
|
56
|
+
feed.parse(pge_authorization)
|
57
|
+
.entries
|
58
|
+
.first
|
59
|
+
.content
|
60
|
+
.authorization
|
61
|
+
end
|
62
|
+
|
63
|
+
subject { authorization }
|
64
|
+
|
65
|
+
it "should parse authorized period" do
|
66
|
+
expect(subject.authorized_period.start).to eq 1440585200
|
67
|
+
expect(subject.authorized_period.duration).to eq 2519636580
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should parse published period" do
|
71
|
+
expect(subject.published_period.start).to eq 1395513200
|
72
|
+
expect(subject.published_period.duration).to eq 2520267300
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should parse expiry date" do
|
76
|
+
expect(subject.expires_at).to eq DateTime.new 2015, 8, 27, 10, 7, 9
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should parse status" do
|
80
|
+
expect(subject.status).to eq 1
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should parse scope" do
|
84
|
+
expect(subject.scope).to eq "FB=1_3_4_5_8_13_14_15_18_19_31_32_35_37_38_39_40;IntervalDuration=900_3600;BlockDuration=Daily;HistoryLength=63072000;SubscriptionFrequency=Daily;AccountCollection=1;"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should parse resource URI" do
|
88
|
+
expect(subject.resource_uri).to eq "https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Subscription/0"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should parse authorization URI" do
|
92
|
+
expect(subject.authorization_uri).to eq "https://api.pge.com/GreenButtonConnect/espi/1_1/resource/Authorization/0"
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "#active?" do
|
96
|
+
it "should return true if status is not 0" do
|
97
|
+
expect(subject.active?).to eq true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
52
101
|
end
|
@@ -36,4 +36,40 @@ describe GreenButtonData::Parser::IntervalBlock do
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
39
|
+
|
40
|
+
context "PG&E namespace" do
|
41
|
+
let(:feed) { GreenButtonData::Feed }
|
42
|
+
let :interval_block do
|
43
|
+
feed.parse(pge_interval_block).entries.last.content.interval_block
|
44
|
+
end
|
45
|
+
|
46
|
+
subject { interval_block }
|
47
|
+
|
48
|
+
it "should parse interval" do
|
49
|
+
expect(subject.interval).to be_an_instance_of GreenButtonData::Parser::Interval
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should parse interval readings" do
|
53
|
+
expect(subject.interval_readings.size).to eq 2280
|
54
|
+
expect(subject.interval_readings.first).to be_an_instance_of GreenButtonData::Parser::IntervalReading
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#duration" do
|
58
|
+
it "should return the duration of the current interval block" do
|
59
|
+
expect(subject.duration).to eq 8208000
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#starts_at" do
|
64
|
+
it "should return the start datetime" do
|
65
|
+
expect(subject.starts_at).to eq DateTime.new 2015, 4, 30, 7
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#ends_at" do
|
70
|
+
it "should return the end datetime" do
|
71
|
+
expect(subject.ends_at).to eq DateTime.new 2015, 8, 3, 7
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
39
75
|
end
|
@@ -53,4 +53,57 @@ describe GreenButtonData::Parser::LocalTimeParameters do
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
56
|
+
|
57
|
+
context "espi namespace" do
|
58
|
+
let(:feed) { GreenButtonData::Feed }
|
59
|
+
let :local_time_parameters do
|
60
|
+
feed.parse(pge_local_time_parameters)
|
61
|
+
.entries
|
62
|
+
.first
|
63
|
+
.content
|
64
|
+
.local_time_parameters
|
65
|
+
end
|
66
|
+
|
67
|
+
subject { local_time_parameters }
|
68
|
+
|
69
|
+
it "should parse DST start rule" do
|
70
|
+
expect(subject.dst_start_rule).to eq 0x360e2000
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should parse DST end rule" do
|
74
|
+
expect(subject.dst_end_rule).to eq 0xb40e2000
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should parse DST offset" do
|
78
|
+
expect(subject.dst_offset).to eq 3600
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should parse timezone offset" do
|
82
|
+
expect(subject.tz_offset).to eq -28800
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#dst_starts_at" do
|
86
|
+
it "should return DateTime object representation of DST rule" do
|
87
|
+
expect(subject.dst_starts_at(2015)).to eq DateTime.new 2015, 3, 8, 2
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#dst_ends_at" do
|
92
|
+
it "should return DateTime object representation of DST rule" do
|
93
|
+
expect(subject.dst_ends_at(2015)).to eq DateTime.new 2015, 11, 1, 2
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#total_offset" do
|
98
|
+
it "should return total offset from DST and timezone offsets in seconds" do
|
99
|
+
expect(subject.total_offset).to eq -25200
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#total_offset_hours" do
|
104
|
+
it "should return total offset in hours" do
|
105
|
+
expect(subject.total_offset_hours).to eq -7
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
56
109
|
end
|
@@ -73,4 +73,77 @@ describe GreenButtonData::Parser::ReadingType do
|
|
73
73
|
expect(subject.argument).to eq nil
|
74
74
|
end
|
75
75
|
end
|
76
|
+
|
77
|
+
context "PG&E namespace" do
|
78
|
+
let(:feed) { GreenButtonData::Feed }
|
79
|
+
let :reading_type do
|
80
|
+
feed.parse(pge_reading_type).entries.first.content.reading_type
|
81
|
+
end
|
82
|
+
|
83
|
+
subject { reading_type }
|
84
|
+
|
85
|
+
it "should parse accumulation behavior" do
|
86
|
+
expect(subject.accumulation_behaviour).to eq :delta_data
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should parse commodity" do
|
90
|
+
expect(subject.commodity).to eq :electricity_secondary_metered
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should parse consumption tier" do
|
94
|
+
expect(subject.consumption_tier).to eq nil
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should parse currency" do
|
98
|
+
expect(subject.currency).to eq nil
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should parse data qualifier" do
|
102
|
+
expect(subject.data_qualifier).to eq :normal
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should parse flow direction" do
|
106
|
+
expect(subject.flow_direction).to eq :forward
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should parse interval length" do
|
110
|
+
expect(subject.interval_length).to eq 900
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should parse measurement kind" do
|
114
|
+
expect(subject.kind).to eq :energy
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should parse phase" do
|
118
|
+
expect(subject.phase).to eq :s12_n
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should parse power of ten multiplier" do
|
122
|
+
expect(subject.power_of_ten_multiplier).to eq 1000
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should parse time attribute" do
|
126
|
+
expect(subject.time_attribute).to eq :none
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should parse unit of measurement" do
|
130
|
+
expect(subject.uom).to eq :Wh
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should parse cpp" do
|
134
|
+
expect(subject.cpp).to eq nil
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should parse interharmonic" do
|
138
|
+
expect(subject.interharmonic).to eq nil
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should parse measuring period" do
|
142
|
+
expect(subject.measuring_period).to eq :fifteen_minute
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should parse argument" do
|
146
|
+
expect(subject.argument).to eq nil
|
147
|
+
end
|
148
|
+
end
|
76
149
|
end
|
@@ -19,4 +19,23 @@ describe GreenButtonData::Parser::UsagePoint do
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
22
|
+
|
23
|
+
context "PG&E namespace" do
|
24
|
+
let(:feed) { GreenButtonData::Feed }
|
25
|
+
let :usage_point do
|
26
|
+
feed.parse(espi_usage_point).entries.first.content.usage_point
|
27
|
+
end
|
28
|
+
|
29
|
+
subject { usage_point }
|
30
|
+
|
31
|
+
it "should parse kind" do
|
32
|
+
expect(subject.kind).to eq 0
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#service_category" do
|
36
|
+
it "should return service category as a symbol" do
|
37
|
+
expect(subject.service_category).to eq :electricity
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
22
41
|
end
|