rtp-connect 1.6 → 1.7

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.
@@ -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