data_miner 2.5.2 → 3.0.0.alpha

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.
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ 3.0.0.alpha / 2013-07-24
2
+
3
+ * breaking changes
4
+
5
+ * :dictionary is now just a plain Hash-like object (responds to []) - and no longer attempts to refresh between runs
6
+ * no more unit conversions
7
+ * always nullifies blank strings
8
+ * always overwrites columns whether there was a non-null value there before or not (no more :overwrite option)
9
+ * don't use synthesize option, just pass a block
10
+ * synthesized values get the same whitespace compression and stripping as other values
11
+ * not tested against the Earth library, which has particular requirements and makes testing too complicated
12
+ * doesn't keep DataMiner::Run or DataMiner::Run::ColumnStatistic records
13
+ * no more :matcher option
14
+
15
+ * enhancements
16
+
17
+ * if you have a postgres hstore column called "foo", you can do store 'foo.bar'
18
+
1
19
  2.5.2 / 2013-07-05
2
20
 
3
21
  * Bug fixes
data/Gemfile CHANGED
@@ -2,8 +2,6 @@ source :rubygems
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'conversions'
6
-
7
5
  if RUBY_VERSION >= '1.9'
8
6
  gem 'unicode_utils'
9
7
  end
data/data_miner.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
8
8
  s.email = ["seamus@abshere.net", "rossmeissl@gmail.com", "dkastner@gmail.com", "ijhough@gmail.com", "towerhe@gmail.com"]
9
9
  s.homepage = "https://github.com/seamusabshere/data_miner"
10
10
  s.summary = %{Download, pull out of a ZIP/TAR/GZ/BZ2 archive, parse, correct, and import XLS, ODS, XML, CSV, HTML, etc. into your ActiveRecord models.}
11
- s.description = %q{Download, pull out of a ZIP/TAR/GZ/BZ2 archive, parse, correct, and import XLS, ODS, XML, CSV, HTML, etc. into your ActiveRecord models. You can also convert units.}
11
+ s.description = %q{Download, pull out of a ZIP/TAR/GZ/BZ2 archive, parse, correct, and import XLS, ODS, XML, CSV, HTML, etc. into your ActiveRecord models. Uses Upsert internally for speed.}
12
12
 
13
13
  s.rubyforge_project = "data_miner"
14
14
 
@@ -17,8 +17,6 @@ Gem::Specification.new do |s|
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_runtime_dependency 'aasm'
21
- s.add_runtime_dependency 'active_record_inline_schema', '>=0.6.1'
22
20
  s.add_runtime_dependency 'activerecord', '> 3'
23
21
  s.add_runtime_dependency 'activesupport', '> 3'
24
22
  s.add_runtime_dependency 'errata', '>=1.0.1'
@@ -28,11 +26,9 @@ Gem::Specification.new do |s|
28
26
  s.add_runtime_dependency 'unix_utils'
29
27
  s.add_runtime_dependency 'roo', '>=1.10.3'
30
28
 
31
- s.add_development_dependency 'dkastner-alchemist'
32
- s.add_development_dependency 'conversions'
33
- s.add_development_dependency 'earth'
29
+ s.add_development_dependency 'pry'
30
+ s.add_development_dependency 'active_record_inline_schema'
34
31
  s.add_development_dependency 'fuzzy_match'
35
- s.add_development_dependency 'lock_method'
36
32
  s.add_development_dependency 'minitest'
37
33
  s.add_development_dependency 'minitest-reporters'
38
34
  s.add_development_dependency 'rake'
data/lib/data_miner.rb CHANGED
@@ -17,13 +17,10 @@ end
17
17
  require 'data_miner/active_record_class_methods'
18
18
  require 'data_miner/attribute'
19
19
  require 'data_miner/script'
20
- require 'data_miner/dictionary'
21
20
  require 'data_miner/step'
22
21
  require 'data_miner/step/import'
23
22
  require 'data_miner/step/process'
24
23
  require 'data_miner/step/sql'
25
- require 'data_miner/run'
26
- require 'data_miner/unit_converter'
27
24
 
28
25
  # A singleton class that holds global configuration for data mining.
29
26
  #
@@ -46,23 +43,6 @@ class DataMiner
46
43
  def compress_whitespace(str)
47
44
  str.gsub(INNER_SPACE, ONE_SPACE).strip
48
45
  end
49
-
50
- # Set the unit converter.
51
- #
52
- # @note As of 2012-05-30, there are problems with the alchemist gem and the use of the conversions gem instead is recommended.
53
- #
54
- # @param [Symbol,nil] conversion_library Either +:alchemist+ or +:conversions+
55
- #
56
- # @return [nil]
57
- def unit_converter=(conversion_library)
58
- @unit_converter = UnitConverter.load conversion_library
59
- nil
60
- end
61
-
62
- # @return [#convert,nil] The user-selected unit converter or nil.
63
- def unit_converter
64
- @unit_converter
65
- end
66
46
  end
67
47
 
68
48
  INNER_SPACE = /[ ]+/
@@ -76,13 +56,14 @@ class DataMiner
76
56
  #
77
57
  # @param [optional, Array<String>] model_names Names of models to be run.
78
58
  #
79
- # @return [Array<DataMiner::Run>]
59
+ # @return nil
80
60
  def start(model_names = DataMiner.model_names)
81
61
  Script.uniq do
82
62
  model_names.map do |model_name|
83
63
  model_name.constantize.run_data_miner!
84
64
  end
85
65
  end
66
+ nil
86
67
  end
87
68
 
88
69
  # legacy
@@ -115,16 +96,6 @@ class DataMiner
115
96
  end
116
97
  end
117
98
 
118
- # Whether per-column stats like max, min, average, standard deviation, etc are enabled.
119
- def per_column_statistics?
120
- @per_column_statistics == true
121
- end
122
-
123
- # Turn on or off per-column stats.
124
- def per_column_statistics=(boolean)
125
- @per_column_statistics = boolean
126
- end
127
-
128
99
  class << self
129
100
  delegate(*DataMiner.instance_methods(false), :to => :instance)
130
101
  end
@@ -12,16 +12,9 @@ class DataMiner
12
12
  end
13
13
  end
14
14
 
15
- # Access to recordkeeping.
16
- #
17
- # @return [ActiveRecord::Relation] Records of running the data miner script.
18
- def data_miner_runs
19
- DataMiner::Run.scoped :conditions => { :model_name => name }
20
- end
21
-
22
15
  # Run this model's script.
23
16
  #
24
- # @return [DataMiner::Run]
17
+ # @return nil
25
18
  def run_data_miner!
26
19
  data_miner_script.start
27
20
  end
@@ -45,13 +38,14 @@ class DataMiner
45
38
  # end
46
39
  # end
47
40
  #
48
- # @return [Array<DataMiner::Run>]
41
+ # @return nil
49
42
  def run_data_miner_on_parent_associations!
50
43
  reflect_on_all_associations(:belongs_to).reject do |assoc|
51
- assoc.options[:polymorphic]
44
+ assoc.options['polymorphic']
52
45
  end.map do |non_polymorphic_belongs_to_assoc|
53
46
  non_polymorphic_belongs_to_assoc.klass.run_data_miner!
54
47
  end
48
+ nil
55
49
  end
56
50
 
57
51
  # Define a data miner script.
@@ -97,7 +91,7 @@ class DataMiner
97
91
  #
98
92
  # @return [nil]
99
93
  def data_miner(options = {}, &blk)
100
- unless options[:append]
94
+ unless options['append']
101
95
  @data_miner_script = nil
102
96
  end
103
97
  data_miner_script.append_block blk
@@ -9,60 +9,32 @@ class DataMiner
9
9
  # @private
10
10
  def check_options(options)
11
11
  errors = []
12
- if options[:dictionary].is_a?(Dictionary)
13
- errors << %{:dictionary must be a Hash of options}
12
+ if options.has_key?('dictionary') and not options['dictionary'].respond_to?(:[])
13
+ errors << %{:dictionary must respond to [], like a Hash does}
14
14
  end
15
15
  if (invalid_option_keys = options.keys - VALID_OPTIONS).any?
16
16
  errors << %{Invalid options: #{invalid_option_keys.map(&:inspect).to_sentence}}
17
17
  end
18
- units_options = options.select { |k, _| k.to_s.include?('units') }
19
- if units_options.any? and DataMiner.unit_converter.nil?
20
- errors << %{You must set DataMiner.unit_converter to :alchemist or :conversions if you wish to convert units}
21
- end
22
- if units_options.any? and VALID_UNIT_DEFINITION_SETS.none? { |d| d.all? { |required_option| options[required_option].present? } }
23
- errors << %{#{units_options.inspect} is not a valid set of units definitions. Please supply a set like #{VALID_UNIT_DEFINITION_SETS.map(&:inspect).to_sentence}".}
24
- end
25
18
  errors
26
19
  end
27
20
  end
28
21
 
29
22
  VALID_OPTIONS = [
30
- :from_units,
31
- :to_units,
32
- :static,
33
- :dictionary,
34
- :matcher,
35
- :field_name,
36
- :delimiter,
37
- :split,
38
- :units,
39
- :sprintf,
40
- :nullify, # deprecated
41
- :nullify_blank_strings,
42
- :overwrite,
43
- :upcase,
44
- :units_field_name,
45
- :units_field_number,
46
- :field_number,
47
- :chars,
48
- :synthesize,
49
- ]
50
-
51
- VALID_UNIT_DEFINITION_SETS = [
52
- [:units], # no conversion
53
- [:from_units, :to_units], # yes
54
- [:units_field_name], # no
55
- [:units_field_name, :to_units], # yes
56
- [:units_field_number], # no
57
- [:units_field_number, :to_units], # yes
23
+ 'static',
24
+ 'dictionary',
25
+ 'field_name',
26
+ 'delimiter',
27
+ 'split',
28
+ 'sprintf',
29
+ 'upcase',
30
+ 'field_number',
31
+ 'chars',
58
32
  ]
59
33
 
60
34
  DEFAULT_SPLIT_PATTERN = /\s+/
61
35
  DEFAULT_SPLIT_KEEP = 0
62
36
  DEFAULT_DELIMITER = ', '
63
- DEFAULT_NULLIFY_BLANK_STRINGS = false
64
37
  DEFAULT_UPCASE = false
65
- DEFAULT_OVERWRITE = true
66
38
 
67
39
  # activerecord-3.2.6/lib/active_record/connection_adapters/column.rb
68
40
  TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON', 'yes', 'YES', 'y', 'Y']
@@ -72,24 +44,18 @@ class DataMiner
72
44
  attr_reader :step
73
45
 
74
46
  # Local column name.
75
- # @return [Symbol]
47
+ # @return [String]
76
48
  attr_reader :name
77
49
 
78
- # Synthesize a value by passing a proc that will receive +row+ and should return a final value.
50
+ # The block passed to a store argument. Synthesize a value by passing a proc that will receive +row+ and should return a final value.
51
+ #
52
+ # Unlike past versions of DataMiner, you pass this as a block, not with the :synthesize option.
79
53
  #
80
54
  # +row+ will be a +Hash+ with string keys or (less often) an +Array+
81
55
  #
82
56
  # @return [Proc]
83
57
  attr_reader :synthesize
84
58
 
85
- # An object that will be sent +#match(row)+ and should return a final value.
86
- #
87
- # Can be specified as a String which will be constantized into a class and an object of that class instantized with no arguments.
88
- #
89
- # +row+ will be a +Hash+ with string keys or (less often) an +Array+
90
- # @return [Object]
91
- attr_reader :matcher
92
-
93
59
  # Index of where to find the data in the row, starting from zero.
94
60
  #
95
61
  # If you pass a +Range+, then multiple fields will be joined together.
@@ -97,10 +63,6 @@ class DataMiner
97
63
  # @return [Integer, Range]
98
64
  attr_reader :field_number
99
65
 
100
- # Where to find the data in the row.
101
- # @return [Symbol]
102
- attr_reader :field_name
103
-
104
66
  # A delimiter to be used when joining fields together into a single final value. Used when +:field_number+ is a +Range+. Defaults to DEFAULT_DELIMITER.
105
67
  # @return [String]
106
68
  attr_reader :delimiter
@@ -117,26 +79,6 @@ class DataMiner
117
79
  # @return [Hash]
118
80
  attr_reader :split
119
81
 
120
- # Final units. May invoke a conversion using https://rubygems.org/gems/alchemist
121
- #
122
- # If a local column named +[name]_units+ exists, it will be populated with this value.
123
- #
124
- # @return [Symbol]
125
- attr_reader :to_units
126
-
127
- # Initial units. May invoke a conversion using a conversion gem like https://rubygems.org/gems/alchemist
128
- # Be sure to set DataMiner.unit_converter
129
- # @return [Symbol]
130
- attr_reader :from_units
131
-
132
- # If every row specifies its own units, index of where to find the units. Zero-based.
133
- # @return [Integer]
134
- attr_reader :units_field_number
135
-
136
- # If every row specifies its own units, where to find the units.
137
- # @return [Symbol]
138
- attr_reader :units_field_name
139
-
140
82
  # A +sprintf+-style format to apply.
141
83
  # @return [String]
142
84
  attr_reader :sprintf
@@ -145,93 +87,81 @@ class DataMiner
145
87
  # @return [String,Numeric,TrueClass,FalseClass,Object]
146
88
  attr_reader :static
147
89
 
148
- # Only meaningful for string columns. Whether to store blank input (" ") as NULL. Defaults to DEFAULT_NULLIFY_BLANK_STRINGS.
149
- # @return [TrueClass,FalseClass]
150
- attr_reader :nullify_blank_strings
151
-
152
90
  # Whether to upcase value. Defaults to DEFAULT_UPCASE.
153
91
  # @return [TrueClass,FalseClass]
154
92
  attr_reader :upcase
155
93
 
156
- # Whether to overwrite the value in a local column if it is not null. Defaults to DEFAULT_OVERWRITE.
157
- # @return [TrueClass,FalseClass]
158
- attr_reader :overwrite
94
+ # Dictionary for translating.
95
+ #
96
+ # You pass a Hash or something that responds to []
97
+ #
98
+ # @return [#[]]
99
+ attr_reader :dictionary
159
100
 
160
101
  # @private
161
- def initialize(step, name, options = {})
162
- options = options.symbolize_keys
102
+ def initialize(step, name, options = {}, &blk)
103
+ options = options.stringify_keys
163
104
  if (errors = Attribute.check_options(options)).any?
164
105
  raise ::ArgumentError, %{[data_miner] Errors on #{inspect}: #{errors.join(';')}}
165
106
  end
166
107
  @step = step
167
- @name = name.to_sym
168
- @synthesize = options[:synthesize]
169
- if @dictionary_boolean = options.has_key?(:dictionary)
170
- @dictionary_settings = options[:dictionary]
171
- end
172
- @matcher = options[:matcher].is_a?(::String) ? options[:matcher].constantize.new : options[:matcher]
173
- if @static_boolean = options.has_key?(:static)
174
- @static = options[:static]
108
+ @name = name.to_s
109
+ @synthesize = blk if block_given?
110
+ @dictionary = options['dictionary']
111
+ if @static_boolean = options.has_key?('static')
112
+ @static = options['static']
175
113
  end
176
- @field_number = options[:field_number]
177
- @field_name = options.fetch(:field_name, name).to_sym
178
- @delimiter = options.fetch :delimiter, DEFAULT_DELIMITER
179
- @chars = options[:chars]
180
- if split = options[:split]
181
- @split = split.symbolize_keys
182
- end
183
- @nullify_blank_strings = if options.has_key?(:nullify)
184
- # deprecated
185
- options[:nullify]
186
- else
187
- options.fetch :nullify_blank_strings, DEFAULT_NULLIFY_BLANK_STRINGS
114
+ @field_number = options['field_number']
115
+ @field_name_settings = options['field_name']
116
+ @delimiter = options.fetch 'delimiter', DEFAULT_DELIMITER
117
+ @chars = options['chars']
118
+ if split = options['split']
119
+ @split = split.stringify_keys
188
120
  end
189
- @upcase = options.fetch :upcase, DEFAULT_UPCASE
190
- @from_units = options[:from_units]
191
- @to_units = options[:to_units] || options[:units]
192
- @sprintf = options[:sprintf]
193
- @overwrite = options.fetch :overwrite, DEFAULT_OVERWRITE
194
- @units_field_name = options[:units_field_name]
195
- @units_field_number = options[:units_field_number]
196
- @convert_boolean = (@from_units.present? or (@to_units.present? and (@units_field_name.present? or @units_field_number.present?)))
197
- @persist_units_boolean = (@to_units.present? or @units_field_name.present? or @units_field_number.present?)
198
- @dictionary_mutex = ::Mutex.new
121
+ @upcase = options.fetch 'upcase', DEFAULT_UPCASE
122
+ @sprintf = options['sprintf']
199
123
  end
200
124
 
201
- # Dictionary for translating.
202
- #
203
- # You pass a +Hash+ of options which is used to initialize a +DataMiner::Dictionary+.
204
- #
205
- # @return [DataMiner::Dictionary]
206
- def dictionary
207
- @dictionary || @dictionary_mutex.synchronize do
208
- @dictionary ||= Dictionary.new(@dictionary_settings)
125
+ # @private
126
+ def hstore_column
127
+ return @hstore_column if defined?(@hstore_column)
128
+ @hstore_column = name.split('.', 2)[0]
129
+ end
130
+
131
+ # @private
132
+ def hstore_key
133
+ return @hstore_key if defined?(@hstore_key)
134
+ @hstore_key = name.split('.', 2)[1]
135
+ end
136
+
137
+ # Where to find the data in the row.
138
+ # @return [String]
139
+ def field_name
140
+ return @field_name if defined?(@field_name)
141
+ @field_name = if @field_name_settings
142
+ @field_name_settings.to_s
143
+ elsif hstore?
144
+ hstore_key
145
+ else
146
+ name
209
147
  end
210
148
  end
211
149
 
212
150
  # # @private
213
- # TODO make sure that nil handling is replicated when using upsert
214
151
  def set_from_row(local_record, remote_row)
215
- previously_nil = local_record.send(name).nil?
216
- currently_nil = false
217
- if previously_nil or overwrite
218
- new_value = read remote_row
219
- local_record.send "#{name}=", new_value
220
- currently_nil = new_value.nil?
221
- end
222
- if not currently_nil and persist_units? and (final_to_units = (to_units || read_units(remote_row)))
223
- local_record.send "#{name}_units=", final_to_units
152
+ new_value = read remote_row
153
+ if hstore?
154
+ local_record.send(hstore_column)[hstore_key] = new_value
155
+ else
156
+ local_record.send("#{name}=", new_value)
224
157
  end
225
158
  end
226
159
 
227
160
  # @private
228
161
  def updates(remote_row)
229
162
  v = read remote_row
230
- if persist_units?
231
- v_units = unless v.nil?
232
- to_units || read_units(remote_row)
233
- end
234
- { name => v, "#{name}_units" => v_units }
163
+ if hstore?
164
+ { hstore_column => { hstore_key => v } }
235
165
  else
236
166
  { name => v }
237
167
  end
@@ -239,27 +169,23 @@ class DataMiner
239
169
 
240
170
  # @private
241
171
  def read(row)
242
- unless column_exists?
243
- raise RuntimeError, "[data_miner] Table #{model.table_name} does not have column #{name.inspect}"
244
- end
245
- if matcher and matcher_output = matcher.match(row)
246
- return matcher_output
247
- end
248
- if synthesize
249
- return synthesize.call(row)
172
+ if not column_exists?
173
+ raise RuntimeError, "[data_miner] Table #{model.table_name} does not have column #{(hstore? ? hstore_column : name).inspect}"
250
174
  end
251
175
  value = if static?
252
176
  static
177
+ elsif synthesize
178
+ synthesize.call(row)
253
179
  elsif field_number
254
180
  if field_number.is_a?(::Range)
255
181
  field_number.map { |n| row[n] }.join(delimiter)
256
182
  else
257
183
  row[field_number]
258
184
  end
259
- elsif field_name == :row_hash
185
+ elsif field_name == 'row_hash'
260
186
  row.row_hash
261
187
  elsif row.is_a?(::Hash) or row.is_a?(::ActiveSupport::OrderedHash)
262
- row[field_name.to_s] # remote_table hash keys are always strings
188
+ row[field_name] # remote_table hash keys are always strings
263
189
  end
264
190
  if value.nil?
265
191
  return
@@ -296,47 +222,29 @@ class DataMiner
296
222
  value = value[chars]
297
223
  end
298
224
  if split
299
- pattern = split.fetch :pattern, DEFAULT_SPLIT_PATTERN
300
- keep = split.fetch :keep, DEFAULT_SPLIT_KEEP
225
+ pattern = split.fetch 'pattern', DEFAULT_SPLIT_PATTERN
226
+ keep = split.fetch 'keep', DEFAULT_SPLIT_KEEP
301
227
  value = value.to_s.split(pattern)[keep].to_s
302
228
  end
303
- if value.blank? and (not text_column? or nullify_blank_strings)
229
+ if value.blank? # TODO false is "blank"
304
230
  return
305
231
  end
306
232
  value = DataMiner.compress_whitespace value
307
233
  if upcase
308
234
  value = DataMiner.upcase value
309
235
  end
310
- if convert?
311
- value = convert_units value, row
312
- end
313
236
  if sprintf
314
- if sprintf.end_with?('f')
315
- value = value.to_f
316
- elsif sprintf.end_with?('d')
317
- value = value.to_i
318
- end
319
- value = sprintf % value
237
+ value = sprintf % value.to_f
320
238
  end
321
- if dictionary?
322
- value = dictionary.lookup(value)
239
+ if dictionary
240
+ value = dictionary[value]
323
241
  end
324
242
  value
325
243
  end
326
244
 
327
- # @private
328
- def convert_units(value, row)
329
- final_from_units = from_units || read_units(row)
330
- final_to_units = to_units || read_units(row)
331
- unless final_from_units and final_to_units
332
- raise RuntimeError, "[data_miner] Missing units: from=#{final_from_units.inspect}, to=#{final_to_units.inspect}"
333
- end
334
- DataMiner.unit_converter.convert value, final_from_units, final_to_units
335
- end
336
-
337
- # @private
338
- def refresh
339
- @dictionary = nil
245
+ def hstore?
246
+ return @hstore_boolean if defined?(@hstore_boolean)
247
+ @hstore_boolean = name.include?('.')
340
248
  end
341
249
 
342
250
  private
@@ -347,48 +255,42 @@ class DataMiner
347
255
 
348
256
  def column_exists?
349
257
  return @column_exists_boolean if defined?(@column_exists_boolean)
350
- @column_exists_boolean = model.column_names.include? name.to_s
258
+ if hstore?
259
+ @column_exists_boolean = model.column_names.include? hstore_column
260
+ else
261
+ @column_exists_boolean = model.column_names.include? name
262
+ end
351
263
  end
352
264
 
353
265
  def text_column?
354
266
  return @text_column_boolean if defined?(@text_column_boolean)
355
- @text_column_boolean = model.columns_hash[name.to_s].text?
267
+ if hstore?
268
+ @text_column_boolean = true
269
+ else
270
+ @text_column_boolean = model.columns_hash[name].text?
271
+ end
356
272
  end
357
273
 
358
274
  def number_column?
359
275
  return @number_column_boolean if defined?(@number_column_boolean)
360
- @number_column_boolean = model.columns_hash[name.to_s].number?
276
+ if hstore?
277
+ @number_column_boolean = false
278
+ else
279
+ @number_column_boolean = model.columns_hash[name].number?
280
+ end
361
281
  end
362
282
 
363
283
  def boolean_column?
364
284
  return @boolean_column_boolean if defined?(@boolean_column_boolean)
365
- @boolean_column_boolean = (model.columns_hash[name.to_s].type == :boolean)
285
+ if hstore?
286
+ @boolean_column_boolean = false
287
+ else
288
+ @boolean_column_boolean = (model.columns_hash[name].type == :boolean)
289
+ end
366
290
  end
367
291
 
368
292
  def static?
369
293
  @static_boolean
370
294
  end
371
-
372
- def dictionary?
373
- @dictionary_boolean
374
- end
375
-
376
- def convert?
377
- @convert_boolean
378
- end
379
-
380
- def persist_units?
381
- @persist_units_boolean
382
- end
383
-
384
- def read_units(row)
385
- if units = row[units_field_name || units_field_number]
386
- DataMiner.compress_whitespace(units).underscore
387
- end
388
- end
389
-
390
- def free
391
- @dictionary = nil
392
- end
393
295
  end
394
296
  end