ruby_astm 1.4.1 → 1.4.3
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 +5 -5
- data/lib/mappings.json +199 -79
- data/lib/publisher/adapter.rb +0 -0
- data/lib/publisher/google_lab_interface.rb +2 -2
- data/lib/publisher/pf_lab_interface.rb +484 -0
- data/lib/publisher/poller.rb +12 -3
- data/lib/publisher/real_time_db.rb +76 -0
- data/lib/ruby_astm.rb +21 -17
- data/lib/ruby_astm/HL7/hl7_header.rb +0 -0
- data/lib/ruby_astm/HL7/hl7_observation.rb +0 -0
- data/lib/ruby_astm/HL7/hl7_order.rb +0 -0
- data/lib/ruby_astm/HL7/hl7_patient.rb +0 -0
- data/lib/ruby_astm/astm_server.rb +1 -0
- data/lib/ruby_astm/custom/siemens_abg_electrolyte_server.rb +315 -0
- data/lib/ruby_astm/frame.rb +0 -0
- data/lib/ruby_astm/header.rb +2 -4
- data/lib/ruby_astm/lab_interface.rb +113 -25
- data/lib/ruby_astm/line.rb +0 -0
- data/lib/ruby_astm/order.rb +1 -1
- data/lib/ruby_astm/patient.rb +1 -1
- data/lib/ruby_astm/query.rb +0 -0
- data/lib/ruby_astm/result.rb +1 -1
- data/lib/ruby_astm/usb_module.rb +0 -0
- metadata +35 -4
data/lib/publisher/adapter.rb
CHANGED
File without changes
|
@@ -49,8 +49,8 @@ class Google_Lab_Interface < Poller
|
|
49
49
|
## @param[String] mpg : path to mappings file. Defaults to nil.
|
50
50
|
## @param[String] credentials_path : the path to look for the credentials.json file, defaults to nil ,and will raise an error unless provided
|
51
51
|
## @param[String] token_path : the path where the oauth token will be stored, also defaults to the path of the gem : eg. ./token.yaml - be careful with write permissions, because token.yaml gets written to this path after the first authorization.
|
52
|
-
def initialize(mpg=nil,credentials_path,token_path,script_id)
|
53
|
-
super(mpg)
|
52
|
+
def initialize(mpg=nil,credentials_path,token_path,script_id,real_time_db)
|
53
|
+
super(mpg,real_time_db)
|
54
54
|
self.credentials_path = credentials_path
|
55
55
|
self.token_path = token_path
|
56
56
|
self.script_id = script_id
|
@@ -0,0 +1,484 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'publisher/poller'
|
3
|
+
require 'typhoeus'
|
4
|
+
|
5
|
+
class Pf_Lab_Interface < Poller
|
6
|
+
|
7
|
+
ORDERS = "orders"
|
8
|
+
ORDERS_SORTED_SET = "orders_sorted_set"
|
9
|
+
BARCODES = "barcodes"
|
10
|
+
BARCODE = "barcode"
|
11
|
+
BASE_URL = "http://localhost:3000/"
|
12
|
+
UPDATE_QUEUE = "update_queue"
|
13
|
+
## will look back 12 hours if no previous request is found.
|
14
|
+
DEFAULT_LOOK_BACK_IN_SECONDS = 12*3600
|
15
|
+
## time to keep old orders in memory
|
16
|
+
## 48 hours, expressed as seconds.
|
17
|
+
DEFAULT_STORAGE_TIME_FOR_ORDERS_IN_SECONDS = 48*3600
|
18
|
+
## the last request that was made and what it said.
|
19
|
+
POLL_URL_PATH = BASE_URL + "interfaces"
|
20
|
+
PUT_URL_PATH = BASE_URL + "lis_update_orders"
|
21
|
+
LAST_REQUEST = "last_request"
|
22
|
+
FROM_EPOCH = "from_epoch"
|
23
|
+
TO_EPOCH = "to_epoch"
|
24
|
+
SIZE = "size"
|
25
|
+
SKIP = "skip"
|
26
|
+
ID = "id"
|
27
|
+
REPORTS = "reports"
|
28
|
+
TESTS = "tests"
|
29
|
+
RESULT_RAW = "result_raw"
|
30
|
+
CATEGORIES = "categories"
|
31
|
+
USE_CATEGORY_FOR_LIS = "use_category_for_lis"
|
32
|
+
LIS_CODE = "lis_code"
|
33
|
+
REQUIREMENTS = "requirements"
|
34
|
+
ITEMS = "items"
|
35
|
+
CODE = "code"
|
36
|
+
ORDERS_TO_UPDATE_PER_CYCLE = 10
|
37
|
+
|
38
|
+
attr_accessor :lis_security_key
|
39
|
+
|
40
|
+
###################################################################
|
41
|
+
##
|
42
|
+
##
|
43
|
+
## FLOW OF EVENTS IN THIS FILE
|
44
|
+
##
|
45
|
+
##
|
46
|
+
###################################################################
|
47
|
+
|
48
|
+
## STEP ONE:
|
49
|
+
## PRE_POLL_LIS -> basically locks against multiple requests happening at the same time, only one request can go through at one time, this is not touched here, just inherits from poller.rb
|
50
|
+
|
51
|
+
## poll_LIS_for_requisition ->
|
52
|
+
## a. calls build_request
|
53
|
+
## b. build_request -> checks if a previous request is still open (this is done simply by checking for a redis key called LAST_REQUEST, if its found, then the previous request is open.)
|
54
|
+
|
55
|
+
## if the previous request is not open, creates a fresh request by using hte function #fresh_request_params -> this basically sets a hash with two keys : from_epoch (-> now minus some default interval), to_epoch (-> now), and these are used as the params, for the the typhoeus request.
|
56
|
+
## the response to the request is expected to contain (i.e the backend must return)
|
57
|
+
## "orders" => an array of orders
|
58
|
+
## "skip" => how many results it was told to skip (in the case of a fresh request it will be 0)
|
59
|
+
## "size" => the total size of the results that were got.
|
60
|
+
## "from_epoch" => the from_epoch sent in the request.
|
61
|
+
## "to_epoch" => the to_epoch sent in the request.
|
62
|
+
|
63
|
+
## it takes each order, and adds it to the "orders" redis hash.
|
64
|
+
## while adding the orders, it will add individual barcodes with their tests to a "barcodes" hash.
|
65
|
+
## the functions dealing with this are add_order, add_barcode.
|
66
|
+
## while deciding which barcode to add, the priority_category is chosen.
|
67
|
+
|
68
|
+
## after this is done, it will look, whether the request is complete?
|
69
|
+
## this means that the "skip" parameter + number of results returned is equal to the total "size" of all possible results.
|
70
|
+
## if yes, then it deletes the last_request key totally, so that next time a new request is made.
|
71
|
+
## if not, then it commits this last_request to the last_request key.
|
72
|
+
## only thing is that we change the skip to be the earlier skip + the number of results returned, so that the next request sent will start from
|
73
|
+
|
74
|
+
|
75
|
+
###################################################################
|
76
|
+
##
|
77
|
+
##
|
78
|
+
## UTILITY METHOD FOR THE ORDER AND BARCODE HASHES ADD AND REMOVE
|
79
|
+
##
|
80
|
+
##
|
81
|
+
###################################################################
|
82
|
+
def remove_order(order_id)
|
83
|
+
order = get_order(order_id)
|
84
|
+
order["reports"].each do |report|
|
85
|
+
report["tests"].each do |test|
|
86
|
+
remove_barcode(test["barcode"])
|
87
|
+
remove_barcode(test["code"])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
$redis.hdel(ORDERS,order[ID])
|
91
|
+
$redis.zrem(ORDERS_SORTED_SET,order[ID])
|
92
|
+
end
|
93
|
+
|
94
|
+
def remove_barcode(barcode)
|
95
|
+
return if barcode.blank?
|
96
|
+
$redis.hdel(BARCODES,barcode)
|
97
|
+
end
|
98
|
+
|
99
|
+
## @return[Hash] the entry at the barcode, or nil.
|
100
|
+
## key (order_id)
|
101
|
+
## value (array of tests registered on that barcode, the names of the tests are the machine codes, and not the lis_codes)
|
102
|
+
## this key is generated originally in add_barcode
|
103
|
+
def get_barcode(barcode)
|
104
|
+
if barcode_hash = $redis.hget(BARCODES,barcode)
|
105
|
+
JSON.parse(barcode_hash).deep_symbolize_keys
|
106
|
+
else
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_order(order_id)
|
112
|
+
if order_string = $redis.hget(ORDERS,order_id)
|
113
|
+
JSON.parse(order_string).deep_symbolize_keys
|
114
|
+
else
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
## @param[Hash] req : the requirement hash.
|
120
|
+
## @return[Hash] priority_category : the category which has been chosen as the top priority for the requirement.
|
121
|
+
def get_priority_category(req)
|
122
|
+
priority_category = req[CATEGORIES].select{|c|
|
123
|
+
c[USE_CATEGORY_FOR_LIS] == 1
|
124
|
+
}
|
125
|
+
if priority_category.blank?
|
126
|
+
priority_category = req[CATEGORIES][0]
|
127
|
+
else
|
128
|
+
priority_category = priority_category[0]
|
129
|
+
end
|
130
|
+
priority_category
|
131
|
+
end
|
132
|
+
|
133
|
+
## @param[Hash] order : order object, as a hash.
|
134
|
+
def add_order(order)
|
135
|
+
## this whole thing should be done in one transaction
|
136
|
+
order[REPORTS].each do |report|
|
137
|
+
test_machine_codes = report[TESTS].map{|c|
|
138
|
+
$inverted_mappings[c[LIS_CODE]]
|
139
|
+
}.compact.uniq
|
140
|
+
report[REQUIREMENTS].each do |req|
|
141
|
+
get_priority_category(req)[ITEMS].each do |item|
|
142
|
+
if !item[BARCODE].blank?
|
143
|
+
add_barcode(item[BARCODE],JSON.generate(
|
144
|
+
{
|
145
|
+
:order_id => order[ID],
|
146
|
+
:machine_codes => test_machine_codes
|
147
|
+
}
|
148
|
+
))
|
149
|
+
elsif !item[CODE].blank?
|
150
|
+
add_barcode(item[CODE],JSON.generate({
|
151
|
+
:order_id => order[ID],
|
152
|
+
:machine_codes => test_machine_codes
|
153
|
+
}))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
$redis.hset(ORDERS,order[ID],JSON.generate(order))
|
159
|
+
$redis.zadd(ORDERS_SORTED_SET,Time.now.to_i,order[ID])
|
160
|
+
end
|
161
|
+
|
162
|
+
## start work on simple.
|
163
|
+
|
164
|
+
def update_order(order)
|
165
|
+
$redis.hset(ORDERS,order[ID],JSON.generate(order))
|
166
|
+
end
|
167
|
+
|
168
|
+
## @param[Hash] order : the existing order
|
169
|
+
## @param[Hash] res : the result from the machine, pertaining to this order.
|
170
|
+
## @return[nil]
|
171
|
+
## @working : updates the results from res, into the order at the relevant tests inside the order.
|
172
|
+
## $MAPPINGS -> [MACHINE_CODE => LIS_CODE]
|
173
|
+
## $INVERTED_MAPPINGS -> [LIS_CODE => MACHINE_CODE]
|
174
|
+
def add_test_result(order,res)
|
175
|
+
order[REPORTS.to_sym].each do |report|
|
176
|
+
report[TESTS.to_sym].each_with_index{|t,k|
|
177
|
+
if t[LIS_CODE.to_sym] == $mappings[res[:name]]
|
178
|
+
t[RESULT_RAW.to_sym] = res[:value]
|
179
|
+
end
|
180
|
+
}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def queue_order_for_update(order)
|
185
|
+
$redis.lpush(UPDATE_QUEUE,order[ID.to_sym])
|
186
|
+
end
|
187
|
+
|
188
|
+
def add_barcode(code,order_id)
|
189
|
+
$redis.hset(BARCODES,code,order_id)
|
190
|
+
end
|
191
|
+
|
192
|
+
def get_last_request
|
193
|
+
$redis.hgetall(LAST_REQUEST)
|
194
|
+
end
|
195
|
+
|
196
|
+
=begin
|
197
|
+
def delete_last_request
|
198
|
+
$redis.del(LAST_REQUEST)
|
199
|
+
end
|
200
|
+
=end
|
201
|
+
def all_hits_downloaded?(last_request)
|
202
|
+
last_request[FROM_EPOCH] == last_request[SIZE]
|
203
|
+
end
|
204
|
+
|
205
|
+
def fresh_request_params(from_epoch=nil)
|
206
|
+
params = {}
|
207
|
+
params[TO_EPOCH] = Time.now.to_i
|
208
|
+
params[FROM_EPOCH] = from_epoch || (params[TO_EPOCH] - DEFAULT_LOOK_BACK_IN_SECONDS)
|
209
|
+
params[SKIP] = 0
|
210
|
+
params
|
211
|
+
end
|
212
|
+
|
213
|
+
def build_request
|
214
|
+
last_request = get_last_request
|
215
|
+
params = nil
|
216
|
+
if last_request.blank?
|
217
|
+
params = fresh_request_params
|
218
|
+
else
|
219
|
+
if all_hits_downloaded?(last_request)
|
220
|
+
params = fresh_request_params(last_request[:to_epoch])
|
221
|
+
else
|
222
|
+
params = last_request
|
223
|
+
end
|
224
|
+
end
|
225
|
+
params.merge!(lis_security_key: self.lis_security_key)
|
226
|
+
Typhoeus::Request.new(POLL_URL_PATH,params: params)
|
227
|
+
end
|
228
|
+
|
229
|
+
## commits the request params to redis.
|
230
|
+
## the response hash is expected to have whatever parameters were sent into it in the request.
|
231
|
+
## so it must always return:
|
232
|
+
## a -> how many it was told to skip (SKIP)
|
233
|
+
## b -> from_epoch : from which epoch it was queried.
|
234
|
+
## c -> to_epoch : to which epoch it was queried.
|
235
|
+
def commit_request_params_to_redis(response_hash)
|
236
|
+
$redis.hset(LAST_REQUEST,SKIP,response_hash[SKIP].to_i + response_hash[ORDERS].size.to_i)
|
237
|
+
$redis.hset(LAST_REQUEST,SIZE,response_hash[SIZE].to_i)
|
238
|
+
$redis.hset(LAST_REQUEST,FROM_EPOCH,response_hash[FROM_EPOCH].to_i)
|
239
|
+
$redis.hset(LAST_REQUEST,TO_EPOCH,response_hash[TO_EPOCH].to_i)
|
240
|
+
end
|
241
|
+
|
242
|
+
# since we request only a certain set of orders per request
|
243
|
+
# we need to know if the earlier request has been completed
|
244
|
+
# or we still need to rerequest the same time frame again.
|
245
|
+
def request_size_completed?(response_hash)
|
246
|
+
response_hash[SKIP].to_i + response_hash[ORDERS].size >= response_hash[SIZE]
|
247
|
+
end
|
248
|
+
###################################################################
|
249
|
+
##
|
250
|
+
##
|
251
|
+
## ENDS.
|
252
|
+
##
|
253
|
+
##
|
254
|
+
###################################################################
|
255
|
+
|
256
|
+
|
257
|
+
###################################################################
|
258
|
+
##
|
259
|
+
##
|
260
|
+
## METHODS OVERRIDDEN FROM THE BASIC POLLER.
|
261
|
+
##
|
262
|
+
##
|
263
|
+
###################################################################
|
264
|
+
## @param[String] mpg : path to mappings file. Defaults to nil.
|
265
|
+
## @param[String] lis_security_key : the security key for the LIS organization, to be dowloaded from the organizations/show/id, endpoint in the website.
|
266
|
+
def initialize(mpg=nil,lis_security_key)
|
267
|
+
super(mpg)
|
268
|
+
self.lis_security_key = lis_security_key
|
269
|
+
AstmServer.log("Initialized Lab Interface")
|
270
|
+
end
|
271
|
+
|
272
|
+
def poll_LIS_for_requisition
|
273
|
+
AstmServer.log("Polling LIS at url:#{BASE_URL}")
|
274
|
+
request = build_request
|
275
|
+
request.on_complete do |response|
|
276
|
+
if response.success?
|
277
|
+
response_hash = JSON.parse(response.body)
|
278
|
+
orders = response_hash[ORDERS]
|
279
|
+
orders.each do |order|
|
280
|
+
add_order(order)
|
281
|
+
end
|
282
|
+
commit_request_params_to_redis(response_hash)
|
283
|
+
elsif response.timed_out?
|
284
|
+
# aw hell no
|
285
|
+
# put to astm log.
|
286
|
+
AstmServer.log("Polling time out")
|
287
|
+
elsif response.code == 0
|
288
|
+
# Could not get an http response, something's wrong.
|
289
|
+
AstmServer.log(response.return_message)
|
290
|
+
else
|
291
|
+
# Received a non-successful http response.
|
292
|
+
AstmServer.log("HTTP request failed: " + response.code.to_s)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
request.run
|
296
|
+
end
|
297
|
+
|
298
|
+
=begin
|
299
|
+
data = [
|
300
|
+
{
|
301
|
+
:id => "ARUBA",
|
302
|
+
:results => [
|
303
|
+
{
|
304
|
+
:name => "TLCparam",
|
305
|
+
:value => 10
|
306
|
+
},
|
307
|
+
{
|
308
|
+
:name => "Nparam",
|
309
|
+
:value => 23
|
310
|
+
},
|
311
|
+
{
|
312
|
+
:name => "ANCparam",
|
313
|
+
:value => 25
|
314
|
+
},
|
315
|
+
{
|
316
|
+
:name => "Lparam",
|
317
|
+
:value => 10
|
318
|
+
},
|
319
|
+
{
|
320
|
+
:name => "ALCparam",
|
321
|
+
:value => 44
|
322
|
+
},
|
323
|
+
{
|
324
|
+
:name => "Mparam",
|
325
|
+
:value => 55
|
326
|
+
},
|
327
|
+
{
|
328
|
+
:name => "AMCparam",
|
329
|
+
:value => 22
|
330
|
+
},
|
331
|
+
{
|
332
|
+
:name => "Eparam",
|
333
|
+
:value => 222
|
334
|
+
},
|
335
|
+
{
|
336
|
+
:name => "AECparam",
|
337
|
+
:value => 21
|
338
|
+
},
|
339
|
+
{
|
340
|
+
:name => "BASOparam",
|
341
|
+
:value => 222
|
342
|
+
},
|
343
|
+
{
|
344
|
+
:name => "ABCparam",
|
345
|
+
:value => 300
|
346
|
+
},
|
347
|
+
{
|
348
|
+
:name => "RBCparam",
|
349
|
+
:value => 2.22
|
350
|
+
},
|
351
|
+
{
|
352
|
+
:name => "HBparam",
|
353
|
+
:value => 19
|
354
|
+
},
|
355
|
+
{
|
356
|
+
:name => "HCTparam",
|
357
|
+
:value => 22
|
358
|
+
},
|
359
|
+
{
|
360
|
+
:name => "MCVparam",
|
361
|
+
:value => 222
|
362
|
+
},
|
363
|
+
{
|
364
|
+
:name => "MCHparam",
|
365
|
+
:value => 21
|
366
|
+
},
|
367
|
+
{
|
368
|
+
:name => "MCHCparam",
|
369
|
+
:value => 10
|
370
|
+
},
|
371
|
+
{
|
372
|
+
:name => "MCVparam",
|
373
|
+
:value => 222
|
374
|
+
},
|
375
|
+
{
|
376
|
+
:name => "RDWCVparam",
|
377
|
+
:value => 12
|
378
|
+
},
|
379
|
+
{
|
380
|
+
:name => "PCparam",
|
381
|
+
:value => 1.22322
|
382
|
+
}
|
383
|
+
]
|
384
|
+
}
|
385
|
+
]
|
386
|
+
=end
|
387
|
+
|
388
|
+
def process_update_queue
|
389
|
+
#puts "came to process update queue."
|
390
|
+
order_ids = []
|
391
|
+
ORDERS_TO_UPDATE_PER_CYCLE.times do |n|
|
392
|
+
order_ids << $redis.rpop(UPDATE_QUEUE)
|
393
|
+
end
|
394
|
+
#puts "order ids popped"
|
395
|
+
#puts order_ids.to_s
|
396
|
+
orders = order_ids.map{|c|
|
397
|
+
get_order(c)
|
398
|
+
}.compact
|
399
|
+
|
400
|
+
|
401
|
+
#puts "orders are:"
|
402
|
+
#puts orders.to_s
|
403
|
+
|
404
|
+
req = Typhoeus::Request.new(PUT_URL_PATH, method: :put, body: {orders: orders}.to_json, params: {lis_security_key: self.lis_security_key}, headers: {Accept: 'application/json', "Content-Type".to_sym => 'application/json'})
|
405
|
+
|
406
|
+
|
407
|
+
req.on_complete do |response|
|
408
|
+
if response.success?
|
409
|
+
response_body = response.body
|
410
|
+
orders = JSON.parse(response.body)["orders"]
|
411
|
+
orders.each do |order|
|
412
|
+
if order["errors"].blank?
|
413
|
+
else
|
414
|
+
puts "got an error for the order."
|
415
|
+
## how many total error attempts to manage.
|
416
|
+
end
|
417
|
+
end
|
418
|
+
elsif response.timed_out?
|
419
|
+
AstmServer.log("got a time out")
|
420
|
+
elsif response.code == 0
|
421
|
+
AstmServer.log(response.return_message)
|
422
|
+
else
|
423
|
+
AstmServer.log("HTTP request failed: " + response.code.to_s)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
req.run
|
428
|
+
|
429
|
+
end
|
430
|
+
|
431
|
+
## removes any orders that start from
|
432
|
+
## now - 4 days ago
|
433
|
+
## now - 2 days ago
|
434
|
+
def remove_old_orders
|
435
|
+
stale_order_ids = $redis.zrangebyscore(ORDERS_SORTED_SET,(Time.now.to_i - DEFAULT_STORAGE_TIME_FOR_ORDERS_IN_SECONDS*2).to_s, (Time.now.to_i - DEFAULT_STORAGE_TIME_FOR_ORDERS_IN_SECONDS))
|
436
|
+
$redis.pipelined do
|
437
|
+
stale_order_ids.each do |order_id|
|
438
|
+
remove_order(order_id)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def update(data)
|
444
|
+
data.each do |result|
|
445
|
+
barcode = result[:id]
|
446
|
+
results = result[:results]
|
447
|
+
if barcode_hash = get_barcode(barcode)
|
448
|
+
if order = get_order(barcode_hash[:order_id])
|
449
|
+
## update the test results, and add the order to the final update hash.
|
450
|
+
puts "order got from barcode is:"
|
451
|
+
puts order
|
452
|
+
machine_codes = barcode_hash[:machine_codes]
|
453
|
+
## it has to be registered on this.
|
454
|
+
results.each do |res|
|
455
|
+
if machine_codes.include? res[:name]
|
456
|
+
## so we need to update to the requisite test inside the order.
|
457
|
+
add_test_result(order,res)
|
458
|
+
## commit to redis
|
459
|
+
## and then
|
460
|
+
end
|
461
|
+
end
|
462
|
+
puts "came to queue order for update"
|
463
|
+
queue_order_for_update(order)
|
464
|
+
end
|
465
|
+
else
|
466
|
+
AstmServer.log("the barcode:#{barcode}, does not exist in the barcodes hash")
|
467
|
+
## does not exist.
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
process_update_queue
|
472
|
+
remove_old_orders
|
473
|
+
|
474
|
+
end
|
475
|
+
|
476
|
+
def poll
|
477
|
+
pre_poll_LIS
|
478
|
+
poll_LIS_for_requisition
|
479
|
+
update_LIS
|
480
|
+
post_poll_LIS
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
end
|