rtp-connect 1.6 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,28 +36,8 @@ module RTP
36
36
  # @raise [ArgumentError] if given a string containing an invalid number of elements
37
37
  #
38
38
  def self.load(string, parent)
39
- # Get the quote-less values:
40
- values = string.to_s.values
41
- low_limit = 4
42
- high_limit = 13
43
- raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
44
- RTP.logger.warn "The number of elements (#{values.length}) for this Prescription record exceeds the known number of data items for this record (#{high_limit}). This may indicate an invalid record or that the RTP format has recently been expanded with new items." if values.length > high_limit
45
39
  p = self.new(parent)
46
- # Assign the values to attributes:
47
- p.keyword = values[0]
48
- p.course_id = values[1]
49
- p.rx_site_name = values[2]
50
- p.technique = values[3]
51
- p.modality = values[4]
52
- p.dose_spec = values[5]
53
- p.rx_depth = values[6]
54
- p.dose_ttl = values[7]
55
- p.dose_tx = values[8]
56
- p.pattern = values[9]
57
- p.rx_note = values[10]
58
- p.number_of_fields = values[11]
59
- p.crc = values[-1]
60
- return p
40
+ p.load(string)
61
41
  end
62
42
 
63
43
  # Creates a new Prescription site.
@@ -65,6 +45,7 @@ module RTP
65
45
  # @param [Record] parent a record which is used to determine the proper parent of this instance
66
46
  #
67
47
  def initialize(parent)
48
+ super('RX_DEF', 4, 13)
68
49
  # Child objects:
69
50
  @site_setup = nil
70
51
  @fields = Array.new
@@ -72,7 +53,22 @@ module RTP
72
53
  # Parent relation (may get more than one type of record here):
73
54
  @parent = get_parent(parent.to_record, Plan)
74
55
  @parent.add_prescription(self)
75
- @keyword = 'RX_DEF'
56
+ @attributes = [
57
+ # Required:
58
+ :keyword,
59
+ :course_id,
60
+ :rx_site_name,
61
+ # Optional:
62
+ :technique,
63
+ :modality,
64
+ :dose_spec,
65
+ :rx_depth,
66
+ :dose_ttl,
67
+ :dose_tx,
68
+ :pattern,
69
+ :rx_note,
70
+ :number_of_fields
71
+ ]
76
72
  end
77
73
 
78
74
  # Checks for equality.
@@ -133,28 +129,6 @@ module RTP
133
129
  state.hash
134
130
  end
135
131
 
136
- # Collects the values (attributes) of this instance.
137
- #
138
- # @note The CRC is not considered part of the actual values and is excluded.
139
- # @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
140
- #
141
- def values
142
- return [
143
- @keyword,
144
- @course_id,
145
- @rx_site_name,
146
- @technique,
147
- @modality,
148
- @dose_spec,
149
- @rx_depth,
150
- @dose_ttl,
151
- @dose_tx,
152
- @pattern,
153
- @rx_note,
154
- @number_of_fields
155
- ]
156
- end
157
-
158
132
  # Returns self.
159
133
  #
160
134
  # @return [Prescription] self
@@ -180,18 +154,6 @@ module RTP
180
154
 
181
155
  alias :to_str :to_s
182
156
 
183
- # Sets the keyword attribute.
184
- #
185
- # @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
186
- # @param [#to_s] value the new attribute value
187
- # @raise [ArgumentError] if given an unexpected keyword
188
- #
189
- def keyword=(value)
190
- value = value.to_s.upcase
191
- raise ArgumentError, "Invalid keyword. Expected 'RX_DEF', got #{value}." unless value == "RX_DEF"
192
- @keyword = value
193
- end
194
-
195
157
  # Sets the course_id attribute.
196
158
  #
197
159
  # @param [nil, #to_s] value the new attribute value
@@ -10,6 +10,18 @@ module RTP
10
10
  # The CRC is used to validate the integrity of the content of the RTP string line.
11
11
  attr_reader :crc
12
12
 
13
+ # Creates a new Record.
14
+ #
15
+ # @param [String] keyword the keyword which identifies this record
16
+ # @param [Integer] min_elements the minimum number of data elements required for this record
17
+ # @param [Integer] max_elements the maximum supported number of data elements for this record
18
+ #
19
+ def initialize(keyword, min_elements, max_elements)
20
+ @keyword = keyword
21
+ @min_elements = min_elements
22
+ @max_elements = max_elements
23
+ end
24
+
13
25
  # Sets the crc (checksum) attribute.
14
26
  #
15
27
  # @note This value is not used when creating an RTP string from a record (a new crc is calculated)
@@ -26,7 +38,8 @@ module RTP
26
38
  # @return [String] a proper RTPConnect type CSV string
27
39
  #
28
40
  def encode
29
- content = CSV.generate_line(values, force_quotes: true, row_sep: '') + ","
41
+ encoded_values = values.collect {|v| v && v.encode('ISO8859-1')}
42
+ content = CSV.generate_line(encoded_values, force_quotes: true, row_sep: '') + ","
30
43
  checksum = content.checksum
31
44
  # Complete string is content + checksum (in double quotes) + carriage return + line feed
32
45
  return (content + checksum.to_s.wrap + "\r\n").encode('ISO8859-1')
@@ -45,6 +58,32 @@ module RTP
45
58
  end
46
59
  end
47
60
 
61
+ # Verifies a proposed keyword attribute.
62
+ #
63
+ # @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method.
64
+ # @param [#to_s] value the proposed keyword attribute
65
+ # @raise [ArgumentError] if given an unexpected keyword
66
+ #
67
+ def keyword=(value)
68
+ value = value.to_s.upcase
69
+ raise ArgumentError, "Invalid keyword. Expected '#{@keyword}', got #{value}." unless value == @keyword
70
+ end
71
+
72
+ # Sets up a record by parsing a RTPConnect string line.
73
+ #
74
+ # @param [#to_s] string the extended treatment field definition record string line
75
+ # @return [Record] the updated Record instance
76
+ # @raise [ArgumentError] if given a string containing an invalid number of elements
77
+ #
78
+ def load(string, options={})
79
+ # Extract processed values:
80
+ values = string.to_s.values(options[:repair])
81
+ raise ArgumentError, "Invalid argument 'string': Expected at least #{@min_elements} elements for #{@keyword}, got #{values.length}." if values.length < @min_elements
82
+ RTP.logger.warn "The number of given elements (#{values.length}) exceeds the known number of data elements for this record (#{@max_elements}). This may indicate an invalid string record or that the RTP format has recently been expanded with new elements." if values.length > @max_elements
83
+ self.send(:set_attributes, values)
84
+ self
85
+ end
86
+
48
87
  # Returns self.
49
88
  #
50
89
  # @return [Record] self
@@ -53,6 +92,28 @@ module RTP
53
92
  self
54
93
  end
55
94
 
95
+ # Collects the values (attributes) of this instance.
96
+ #
97
+ # @note The CRC is not considered part of the actual values and is excluded.
98
+ # @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
99
+ #
100
+ def values
101
+ @attributes.collect {|attribute| self.send(attribute)}
102
+ end
103
+
104
+
105
+ private
106
+
107
+
108
+ # Sets the attributes of the record instance.
109
+ #
110
+ # @param [Array<String>] values the record attributes (as parsed from a record string)
111
+ #
112
+ def set_attributes(values)
113
+ @attributes.each_index {|i| self.send("#{@attributes[i]}=", values[i])}
114
+ @crc = values[-1]
115
+ end
116
+
56
117
  end
57
118
 
58
119
  end
@@ -26,6 +26,17 @@ class String
26
26
  self.split(',')
27
27
  end
28
28
 
29
+ # Reformats a string, attempting to fix broken CSV format. Note that this
30
+ # method attempts to fix the CSV in a rather primitive, crude way: Any attributes
31
+ # containing a " character, will have these characters simply removed.
32
+ #
33
+ # @return [String] the processed string
34
+ #
35
+ def repair_csv
36
+ arr = self[1..-2].split('","')
37
+ "\"#{arr.collect{|e| e.gsub('"', '')}.join('","')}\""
38
+ end
39
+
29
40
  # Removes leading & trailing double quotes from a string.
30
41
  #
31
42
  # @return [String] the processed string
@@ -38,14 +49,24 @@ class String
38
49
  # quotation (leading and trailing double-quote characters) from the extracted
39
50
  # string elements.
40
51
  #
52
+ # @param [Boolean] repair if true, the method will attempt to repair a string that fails CSV processing, and then try to process it a second time
41
53
  # @return [Array<String>] an array of the comma separated values
42
54
  #
43
- def values
55
+ def values(repair=false)
44
56
  begin
45
57
  CSV.parse(self).first
46
58
  rescue StandardError => e
47
- RTP.logger.error("Unable to parse the given string record. Probably invalid CSV format: #{self}")
48
- raise e
59
+ if repair
60
+ RTP.logger.warn("CSV processing failed. Will attempt to reformat and reprocess the string record.")
61
+ begin
62
+ CSV.parse(self.repair_csv).first
63
+ rescue StandardError => e
64
+ RTP.logger.error("Unable to parse the given string record. Probably the CSV format is invalid and beyond repair: #{self}")
65
+ end
66
+ else
67
+ RTP.logger.error("Unable to parse the given string record. Probably invalid CSV format: #{self}")
68
+ raise e
69
+ end
49
70
  end
50
71
  end
51
72
 
@@ -76,6 +97,16 @@ class Array
76
97
  return wrapped.join(',')
77
98
  end
78
99
 
100
+ # Validates the number of elements in an array and converts all elements
101
+ # to strings.
102
+ #
103
+ # @param [Integer] nr the required number of elements in the array
104
+ #
105
+ def validate_and_process(nr)
106
+ raise ArgumentError, "Invalid array length. Expected exactly #{nr} elements, got #{self.length}." unless self.length == nr
107
+ self.collect {|e| e && e.to_s.strip}
108
+ end
109
+
79
110
  end
80
111
 
81
112
  # An extension to the NilClass, facilitating a transformation from nil to
@@ -70,68 +70,8 @@ module RTP
70
70
  # @raise [ArgumentError] if given a string containing an invalid number of elements
71
71
  #
72
72
  def self.load(string, parent)
73
- # Get the quote-less values:
74
- values = string.to_s.values
75
- low_limit = 17
76
- high_limit = 53
77
- raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
78
- RTP.logger.warn "The number of elements (#{values.length}) for this Simulation Field record exceeds the known number of data items for this record (#{high_limit}). This may indicate an invalid record or that the RTP format has recently been expanded with new items." if values.length > high_limit
79
73
  sf = self.new(parent)
80
- # Assign the values to attributes:
81
- sf.keyword = values[0]
82
- sf.rx_site_name = values[1]
83
- sf.field_name = values[2]
84
- sf.field_id = values[3]
85
- sf.field_note = values[4]
86
- sf.treatment_machine = values[5]
87
- sf.gantry_angle = values[6]
88
- sf.collimator_angle = values[7]
89
- sf.field_x_mode = values[8]
90
- sf.field_x = values[9]
91
- sf.collimator_x1 = values[10]
92
- sf.collimator_x2 = values[11]
93
- sf.field_y_mode = values[12]
94
- sf.field_y = values[13]
95
- sf.collimator_y1 = values[14]
96
- sf.collimator_y2 = values[15]
97
- sf.couch_vertical = values[16]
98
- sf.couch_lateral = values[17]
99
- sf.couch_longitudinal = values[18]
100
- sf.couch_angle = values[19]
101
- sf.couch_pedestal = values[20]
102
- sf.sad = values[21]
103
- sf.ap_separation = values[22]
104
- sf.pa_separation = values[23]
105
- sf.lateral_separation = values[24]
106
- sf.tangential_separation = values[25]
107
- sf.other_label_1 = values[26]
108
- sf.ssd_1 = values[27]
109
- sf.sfd_1 = values[28]
110
- sf.other_label_2 = values[29]
111
- sf.other_measurement_1 = values[30]
112
- sf.other_measurement_2 = values[31]
113
- sf.other_label_3 = values[32]
114
- sf.other_measurement_3 = values[33]
115
- sf.other_measurement_4 = values[34]
116
- sf.other_label_4 = values[35]
117
- sf.other_measurement_5 = values[36]
118
- sf.other_measurement_6 = values[37]
119
- sf.blade_x_mode = values[38]
120
- sf.blade_x = values[39]
121
- sf.blade_x1 = values[40]
122
- sf.blade_x2 = values[41]
123
- sf.blade_y_mode = values[42]
124
- sf.blade_y = values[43]
125
- sf.blade_y1 = values[44]
126
- sf.blade_y2 = values[45]
127
- sf.ii_lateral = values[46]
128
- sf.ii_longitudinal = values[47]
129
- sf.ii_vertical = values[48]
130
- sf.kvp = values[49]
131
- sf.ma = values[50]
132
- sf.seconds = values[51]
133
- sf.crc = values[-1]
134
- return sf
74
+ sf.load(string)
135
75
  end
136
76
 
137
77
  # Creates a new SimulationField.
@@ -139,10 +79,66 @@ module RTP
139
79
  # @param [Record] parent a record which is used to determine the proper parent of this instance
140
80
  #
141
81
  def initialize(parent)
82
+ super('SIM_DEF', 17, 53)
142
83
  # Parent relation (may get more than one type of record here):
143
84
  @parent = get_parent(parent.to_record, Prescription)
144
85
  @parent.add_simulation_field(self)
145
- @keyword = 'SIM_DEF'
86
+ @attributes = [
87
+ # Required:
88
+ :keyword,
89
+ :rx_site_name,
90
+ :field_name,
91
+ :field_id,
92
+ :field_note,
93
+ :treatment_machine,
94
+ :gantry_angle,
95
+ :collimator_angle,
96
+ :field_x_mode,
97
+ :field_x,
98
+ :collimator_x1,
99
+ :collimator_x2,
100
+ :field_y_mode,
101
+ :field_y,
102
+ :collimator_y1,
103
+ :collimator_y2,
104
+ # Optional:
105
+ :couch_vertical,
106
+ :couch_lateral,
107
+ :couch_longitudinal,
108
+ :couch_angle,
109
+ :couch_pedestal,
110
+ :sad,
111
+ :ap_separation,
112
+ :pa_separation,
113
+ :lateral_separation,
114
+ :tangential_separation,
115
+ :other_label_1,
116
+ :ssd_1,
117
+ :sfd_1,
118
+ :other_label_2,
119
+ :other_measurement_1,
120
+ :other_measurement_2,
121
+ :other_label_3,
122
+ :other_measurement_3,
123
+ :other_measurement_4,
124
+ :other_label_4,
125
+ :other_measurement_5,
126
+ :other_measurement_6,
127
+ :blade_x_mode,
128
+ :blade_x,
129
+ :blade_x1,
130
+ :blade_x2,
131
+ :blade_y_mode,
132
+ :blade_y,
133
+ :blade_y1,
134
+ :blade_y2,
135
+ :ii_lateral,
136
+ :ii_longitudinal,
137
+ :ii_vertical,
138
+ :kvp,
139
+ :ma,
140
+ :seconds
141
+ ]
146
142
  end
147
143
 
148
144
  # Checks for equality.
@@ -179,68 +175,6 @@ module RTP
179
175
  state.hash
180
176
  end
181
177
 
182
- # Collects the values (attributes) of this instance.
183
- #
184
- # @note The CRC is not considered part of the actual values and is excluded.
185
- # @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
186
- #
187
- def values
188
- return [
189
- @keyword,
190
- @rx_site_name,
191
- @field_name,
192
- @field_id,
193
- @field_note,
194
- @treatment_machine,
195
- @gantry_angle,
196
- @collimator_angle,
197
- @field_x_mode,
198
- @field_x,
199
- @collimator_x1,
200
- @collimator_x2,
201
- @field_y_mode,
202
- @field_y,
203
- @collimator_y1,
204
- @collimator_y2,
205
- @couch_vertical,
206
- @couch_lateral,
207
- @couch_longitudinal,
208
- @couch_angle,
209
- @couch_pedestal,
210
- @sad,
211
- @ap_separation,
212
- @pa_separation,
213
- @lateral_separation,
214
- @tangential_separation,
215
- @other_label_1,
216
- @ssd_1,
217
- @sfd_1,
218
- @other_label_2,
219
- @other_measurement_1,
220
- @other_measurement_2,
221
- @other_label_3,
222
- @other_measurement_3,
223
- @other_measurement_4,
224
- @other_label_4,
225
- @other_measurement_5,
226
- @other_measurement_6,
227
- @blade_x_mode,
228
- @blade_x,
229
- @blade_x1,
230
- @blade_x2,
231
- @blade_y_mode,
232
- @blade_y,
233
- @blade_y1,
234
- @blade_y2,
235
- @ii_lateral,
236
- @ii_longitudinal,
237
- @ii_vertical,
238
- @kvp,
239
- @ma,
240
- @seconds
241
- ]
242
- end
243
-
244
178
  # Returns self.
245
179
  #
246
180
  # @return [SimulationField] self
@@ -266,18 +200,6 @@ module RTP
266
200
 
267
201
  alias :to_str :to_s
268
202
 
269
- # Sets the keyword attribute.
270
- #
271
- # @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
272
- # @param [#to_s] value the new attribute value
273
- # @raise [ArgumentError] if given an unexpected keyword
274
- #
275
- def keyword=(value)
276
- value = value.to_s.upcase
277
- raise ArgumentError, "Invalid keyword. Expected 'SIM_DEF', got #{value}." unless value == "SIM_DEF"
278
- @keyword = value
279
- end
280
-
281
203
  # Sets the rx_site_name attribute.
282
204
  #
283
205
  # @param [nil, #to_s] value the new attribute value