datashift 0.15.0 → 0.16.0

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.markdown +91 -55
  3. data/VERSION +1 -1
  4. data/datashift.gemspec +8 -23
  5. data/lib/applications/jexcel_file.rb +1 -2
  6. data/lib/datashift.rb +34 -15
  7. data/lib/datashift/column_packer.rb +98 -34
  8. data/lib/datashift/data_transforms.rb +83 -0
  9. data/lib/datashift/delimiters.rb +58 -10
  10. data/lib/datashift/excel_base.rb +123 -0
  11. data/lib/datashift/exceptions.rb +45 -7
  12. data/lib/datashift/load_object.rb +25 -0
  13. data/lib/datashift/mapping_service.rb +91 -0
  14. data/lib/datashift/method_detail.rb +40 -62
  15. data/lib/datashift/method_details_manager.rb +18 -2
  16. data/lib/datashift/method_dictionary.rb +27 -10
  17. data/lib/datashift/method_mapper.rb +49 -41
  18. data/lib/datashift/model_mapper.rb +42 -22
  19. data/lib/datashift/populator.rb +258 -143
  20. data/lib/datashift/thor_base.rb +38 -0
  21. data/lib/exporters/csv_exporter.rb +57 -145
  22. data/lib/exporters/excel_exporter.rb +73 -60
  23. data/lib/generators/csv_generator.rb +65 -5
  24. data/lib/generators/generator_base.rb +69 -3
  25. data/lib/generators/mapping_generator.rb +112 -0
  26. data/lib/helpers/core_ext/csv_file.rb +33 -0
  27. data/lib/loaders/csv_loader.rb +41 -39
  28. data/lib/loaders/excel_loader.rb +130 -116
  29. data/lib/loaders/loader_base.rb +190 -146
  30. data/lib/loaders/paperclip/attachment_loader.rb +4 -4
  31. data/lib/loaders/paperclip/datashift_paperclip.rb +5 -3
  32. data/lib/loaders/paperclip/image_loading.rb +9 -7
  33. data/lib/loaders/reporter.rb +17 -8
  34. data/lib/thor/export.thor +12 -13
  35. data/lib/thor/generate.thor +1 -9
  36. data/lib/thor/import.thor +13 -24
  37. data/lib/thor/mapping.thor +65 -0
  38. data/spec/Gemfile +13 -11
  39. data/spec/Gemfile.lock +98 -93
  40. data/spec/csv_exporter_spec.rb +104 -99
  41. data/spec/csv_generator_spec.rb +159 -0
  42. data/spec/csv_loader_spec.rb +197 -16
  43. data/spec/datashift_spec.rb +9 -0
  44. data/spec/excel_exporter_spec.rb +149 -58
  45. data/spec/excel_generator_spec.rb +35 -44
  46. data/spec/excel_loader_spec.rb +196 -178
  47. data/spec/excel_spec.rb +8 -5
  48. data/spec/loader_base_spec.rb +47 -7
  49. data/spec/mapping_spec.rb +117 -0
  50. data/spec/method_dictionary_spec.rb +24 -11
  51. data/spec/method_mapper_spec.rb +5 -7
  52. data/spec/model_mapper_spec.rb +41 -0
  53. data/spec/paperclip_loader_spec.rb +3 -6
  54. data/spec/populator_spec.rb +48 -14
  55. data/spec/spec_helper.rb +85 -73
  56. data/spec/thor_spec.rb +40 -5
  57. metadata +93 -86
  58. data/lib/applications/excel_base.rb +0 -63
@@ -14,183 +14,287 @@ require 'logging'
14
14
 
15
15
  module DataShift
16
16
 
17
+ Struct.new("Substitution", :pattern, :replacement)
18
+
17
19
  class Populator
18
-
20
+
19
21
  include DataShift::Logging
20
-
22
+
21
23
  def self.insistent_method_list
22
24
  @insistent_method_list ||= [:to_s, :to_i, :to_f, :to_b]
23
25
  end
24
-
26
+
25
27
  # When looking up an association, when no field provided, try each of these in turn till a match
26
28
  # i.e find_by_name, find_by_title, find_by_id
27
29
  def self.insistent_find_by_list
28
30
  @insistent_find_by_list ||= [:name, :title, :id]
29
31
  end
30
-
31
-
32
+
33
+
34
+ # Default data embedded in column headings - so effectively apply globally
35
+ # to teh whole column - hence class methods
36
+ def self.set_header_default_data(operator, data )
37
+ header_default_data[operator] = data
38
+ end
39
+
40
+ def self.header_default_data
41
+ @header_default_data ||= {}
42
+ end
43
+
44
+
32
45
  attr_reader :current_value, :original_value_before_override
46
+ attr_reader :current_col_type
47
+
33
48
  attr_reader :current_attribute_hash
34
49
  attr_reader :current_method_detail
35
-
36
- def initialize
50
+
51
+ def initialize
37
52
  @current_value = nil
53
+ @current_method_detail = nil
38
54
  @original_value_before_override = nil
39
55
  @current_attribute_hash = {}
40
56
 
41
57
  end
42
-
43
- # Set member variables to hold details, value and optional attributes.
58
+
59
+ # Convert DSL string forms into a hash
60
+ # e.g
61
+ #
62
+ # "{:name => 'autechre'}" => Hash['name'] = autechre'
63
+ # "{:cost_price => '13.45', :price => 23, :sale_price => 4.23 }"
64
+
65
+ def self.string_to_hash( str )
66
+ h = {}
67
+ str.gsub(/[{}:]/,'').split(', ').map do |e|
68
+ k,v = e.split('=>')
69
+
70
+ k.strip!
71
+ v.strip!
72
+
73
+ if( v.match(/['"]/) )
74
+ h[k] = v.gsub(/["']/, '')
75
+ elsif( v.match(/^\d+$|^\d*\.\d+$|^\.\d+$/) )
76
+ h[k] = v.to_f
77
+ else
78
+ h[k] = v
79
+ end
80
+ h
81
+ end
82
+
83
+ h
84
+ end
85
+
86
+ # Set member variables to hold details, value and optional attributes,
87
+ # to be set on the 'value' once created
44
88
  #
45
89
  # Check supplied value, validate it, and if required :
46
90
  # set to provided default value
47
91
  # prepend any provided prefixes
48
92
  # add any provided postfixes
49
93
  def prepare_data(method_detail, value)
50
-
51
- @current_value, @current_attribute_hash = value.to_s.split(Delimiters::attribute_list_start)
52
-
53
- if(@current_attribute_hash)
54
- @current_attribute_hash.strip!
55
- puts "DEBUG: Populator Value contains additional attributes"
56
- @current_attribute_hash = nil unless @current_attribute_hash.include?('}')
57
- end
58
-
59
- @current_attribute_hash ||= {}
60
-
61
- @current_method_detail = method_detail
62
-
63
- operator = method_detail.operator
64
-
65
- override_value(operator)
66
-
67
- if((value.nil? || value.to_s.empty?) && default_value(operator))
68
- @current_value = default_value(operator)
94
+
95
+ raise NilDataSuppliedError.new("No method detail supplied for prepare_data") unless(method_detail)
96
+
97
+ begin
98
+ @prepare_data_const_regexp ||= Regexp.new( Delimiters::attribute_list_start + ".*" + Delimiters::attribute_list_end)
99
+
100
+ if( value.is_a? ActiveRecord::Relation ) # Rails 4 - query no longer returns an array
101
+ @current_value = value.to_a
102
+ elsif( !DataShift::Guards.jruby? && value.class.ancestors.include?(Spreadsheet::Formula))
103
+ @current_value = value.value
104
+ elsif( value.class.ancestors.include?(ActiveRecord::Base) || value.is_a?(Array))
105
+ @current_value = value
106
+ else
107
+ @current_value = value.to_s
108
+
109
+ attribute_hash = @current_value.slice!(@prepare_data_const_regexp)
110
+
111
+ if(attribute_hash)
112
+ @current_attribute_hash = Populator::string_to_hash( attribute_hash )
113
+ logger.info "Populator for #{@current_value} has attributes #{@current_attribute_hash.inspect}"
114
+ end
115
+ end
116
+
117
+ @current_attribute_hash ||= {}
118
+
119
+ @current_method_detail = method_detail
120
+
121
+ @current_col_type = @current_method_detail.col_type
122
+
123
+ operator = method_detail.operator
124
+
125
+ if(has_override?(operator))
126
+ override_value(operator) # takes precedence over anything else
127
+ else
128
+ # if no value check for a defaults from config, headers
129
+ if(default_value(operator))
130
+ @current_value = default_value(operator)
131
+ elsif(Populator::header_default_data[operator])
132
+ @current_value = Populator::header_default_data[operator].to_s
133
+ elsif(Populator::header_default_data[operator])
134
+ @current_value = Populator::header_default_data[operator].to_s
135
+ elsif(method_detail.find_by_value)
136
+ @current_value = method_detail.find_by_value
137
+ end if(value.nil? || value.to_s.empty?)
138
+ end
139
+
140
+ substitute( operator )
141
+
142
+ @current_value = "#{prefix(operator)}#{@current_value}" if(prefix(operator))
143
+ @current_value = "#{@current_value}#{postfix(operator)}" if(postfix(operator))
144
+
145
+ rescue => e
146
+ logger.error("populator failed to prepare data supplied for operator #{method_detail.operator}")
147
+ logger.error("populator error: #{e.inspect}")
148
+ logger.error("populator stacktrace: #{e.backtrace.last}")
149
+ raise DataProcessingError.new("opulator failed to prepare data #{value} for operator #{method_detail.operator}")
69
150
  end
70
-
71
- @current_value = "#{prefix(operator)}#{@current_value}" if(prefix(operator))
72
- @current_value = "#{@current_value}#{postfix(operator)}" if(postfix(operator))
73
151
 
74
152
  return @current_value, @current_attribute_hash
75
153
  end
76
-
77
- def assign(method_detail, record, value )
78
-
79
- @current_value = value
80
-
81
- # Rails 4 - not an array any more
82
- if( value.is_a? ActiveRecord::Relation )
83
- logger.warn("Relation passed rather than value #{value.inspect}")
84
- @current_value = value.to_a
85
- end
86
154
 
87
- # logger.info("WARNING nil value supplied for Column [#{@name}]") if(@current_value.nil?)
155
+ # Main client hook
156
+
157
+ def prepare_and_assign(method_detail, record, value)
158
+
159
+ prepare_data(method_detail, value)
160
+
161
+ assign(record)
162
+ end
163
+
164
+ def assign(record)
165
+
166
+ raise NilDataSuppliedError.new("No method detail - cannot assign data") unless(current_method_detail)
167
+
168
+ operator = current_method_detail.operator
169
+
170
+ logger.debug("Populator assign - [#{current_value}] via #{current_method_detail.operator} (#{current_method_detail.operator_type})")
171
+
172
+ if( current_method_detail.operator_for(:belongs_to) )
173
+
174
+ insistent_belongs_to(current_method_detail, record, current_value)
175
+
176
+ elsif( current_method_detail.operator_for(:has_many) )
88
177
 
89
- operator = method_detail.operator
90
-
91
- if( method_detail.operator_for(:belongs_to) )
92
-
93
- #puts "DEBUG : BELONGS_TO : #{@name} : #{operator} - Lookup #{@current_value} in DB"
94
- insistent_belongs_to(method_detail, record, @current_value)
95
-
96
- elsif( method_detail.operator_for(:has_many) )
97
-
98
- puts "DEBUG : VALUE TYPE [#{value.class.name.include?(operator.classify)}] [#{ModelMapper.class_from_string(value.class.name)}]" unless(value.is_a?(Array))
99
-
100
178
  # The include? check is best I can come up with right now .. to handle module/namespaces
101
179
  # TODO - can we determine the real class type of an association
102
180
  # e.g given a association taxons, which operator.classify gives us Taxon, but actually it's Spree::Taxon
103
181
  # so how do we get from 'taxons' to Spree::Taxons ? .. check if further info in reflect_on_all_associations
104
182
 
105
- if(@current_value.is_a?(Array) || @current_value.class.name.include?(operator.classify))
106
- record.send(operator) << @current_value
107
- else
108
- puts "ERROR #{@current_value.class} - Not expected type for has_many #{operator} - cannot assign"
183
+ begin #if(current_value.is_a?(Array) || current_value.class.name.include?(operator.classify))
184
+ record.send(operator) << current_value
185
+ rescue => e
186
+ logger.error e.inspect
187
+ logger.error "Cannot assign #{current_value.inspect} (#{current_value.class}) to has_many association [#{operator}] "
109
188
  end
110
189
 
111
- elsif( method_detail.operator_for(:has_one) )
190
+ elsif( current_method_detail.operator_for(:has_one) )
112
191
 
113
192
  #puts "DEBUG : HAS_MANY : #{@name} : #{operator}(#{operator_class}) - Lookup #{@current_value} in DB"
114
- if(@current_value.is_a?(method_detail.operator_class))
115
- record.send(operator + '=', @current_value)
193
+ if(current_value.is_a?(current_method_detail.operator_class))
194
+ record.send(operator + '=', current_value)
116
195
  else
117
- logger.error("ERROR #{value.class} - Not expected type for has_one #{operator} - cannot assign")
196
+ logger.error("ERROR #{current_value.class} - Not expected type for has_one #{operator} - cannot assign")
118
197
  # TODO - Not expected type - maybe try to look it up somehow ?"
119
- #insistent_has_many(record, @current_value)
120
198
  end
121
199
 
122
- elsif( method_detail.operator_for(:assignment) && method_detail.col_type )
123
- #puts "DEBUG : COl TYPE defined for #{@name} : #{@assignment} => #{@current_value} #{@col_type.type}"
124
- # puts "DEBUG : Column [#{@name}] : COl TYPE CAST: #{@current_value} => #{@col_type.type_cast( @current_value ).inspect}"
125
- record.send( operator + '=' , method_detail.col_type.type_cast( @current_value ) )
126
-
127
- #puts "DEBUG : MethodDetails Assignment RESULT: #{record.send(operator)}"
200
+ elsif( current_method_detail.operator_for(:assignment) && current_col_type)
201
+ # 'type_cast' was changed to 'type_cast_from_database'
202
+ if Rails::VERSION::STRING < '4.2.0'
203
+ logger.debug("Assign #{current_value} => [#{operator}] (CAST 2 TYPE #{current_col_type.type_cast( current_value ).inspect})")
204
+ record.send( operator + '=' , current_method_detail.col_type.type_cast( current_value ) )
205
+ else
206
+ logger.debug("Assign #{current_value} => [#{operator}] (CAST 2 TYPE #{current_col_type.type_cast_from_database( current_value ).inspect})")
207
+ record.send( operator + '=' , current_method_detail.col_type.type_cast_from_database( current_value ) )
208
+ end
128
209
 
129
- elsif( method_detail.operator_for(:assignment) )
130
- #puts "DEBUG : Column [#{@name}] : Brute force assignment of value #{@current_value}"
210
+ elsif( current_method_detail.operator_for(:assignment) )
211
+ logger.debug("Brute force assignment of value #{current_value} => [#{operator}]")
131
212
  # brute force case for assignments without a column type (which enables us to do correct type_cast)
132
213
  # so in this case, attempt straightforward assignment then if that fails, basic ops such as to_s, to_i, to_f etc
133
- insistent_assignment(record, @current_value, operator)
214
+ insistent_assignment(record, current_value, operator)
134
215
  else
135
216
  puts "WARNING: No assignment possible on #{record.inspect} using [#{operator}]"
136
217
  logger.error("WARNING: No assignment possible on #{record.inspect} using [#{operator}]")
137
218
  end
138
219
  end
139
-
220
+
140
221
  def insistent_assignment(record, value, operator)
141
-
142
- #puts "DEBUG: RECORD CLASS #{record.class}"
222
+
143
223
  op = operator + '=' unless(operator.include?('='))
144
-
224
+
145
225
  begin
146
226
  record.send(op, value)
147
227
  rescue => e
228
+
148
229
  Populator::insistent_method_list.each do |f|
149
230
  begin
150
231
  record.send(op, value.send( f) )
151
232
  break
152
233
  rescue => e
153
- puts "DEBUG: insistent_assignment: #{e.inspect}"
154
234
  if f == Populator::insistent_method_list.last
155
- puts "I'm sorry I have failed to assign [#{value}] to #{operator}"
156
- raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
235
+ logger.error(e.inspect)
236
+ logger.error("Failed to assign [#{value}] via operator #{operator}")
237
+ raise "Failed to assign [#{value}] to #{operator}" unless value.nil?
157
238
  end
158
239
  end
159
240
  end
160
241
  end
161
242
  end
162
-
243
+
163
244
  # Attempt to find the associated object via id, name, title ....
164
245
  def insistent_belongs_to(method_detail, record, value )
165
246
 
166
247
  operator = method_detail.operator
167
-
248
+
168
249
  if( value.class == method_detail.operator_class)
250
+ logger.info("Populator assigning #{value} to belongs_to association #{operator}")
169
251
  record.send(operator) << value
170
252
  else
171
253
 
172
- insistent_find_by_list.each do |x|
173
- begin
174
- next unless method_detail.operator_class.respond_to?( "find_by_#{x}" )
175
- item = method_detail.operator_class.send("find_by_#{x}", value)
176
- if(item)
177
- record.send(operator + '=', item)
178
- break
179
- end
180
- rescue => e
181
- logger.error("Attempt to find associated object failed for #{method_detail}")
182
- if(x == Populator::insistent_method_list.last)
183
- raise "Populator failed to assign [#{value}] via moperator #{operator}" unless value.nil?
254
+ # TODO - DRY all this
255
+ if(method_detail.find_by_operator)
256
+
257
+ item = method_detail.operator_class.where(method_detail.find_by_operator => value).first_or_create
258
+
259
+ if(item)
260
+ logger.info("Populator assigning #{item.inspect} to belongs_to association #{operator}")
261
+ record.send(operator + '=', item)
262
+ else
263
+ logger.error("Could not find or create [#{value}] for belongs_to association [#{operator}]")
264
+ raise CouldNotAssignAssociation.new "Populator failed to assign [#{value}] to belongs_to association [#{operator}]"
265
+ end
266
+
267
+ else
268
+ #try the default field names
269
+ Populator::insistent_find_by_list.each do |x|
270
+ begin
271
+
272
+ next unless method_detail.operator_class.respond_to?("where")
273
+
274
+ item = method_detail.operator_class.where(x => value).first_or_create
275
+
276
+ if(item)
277
+ logger.info("Populator assigning #{item.inspect} to belongs_to association #{operator}")
278
+ record.send(operator + '=', item)
279
+ break
280
+ end
281
+ rescue => e
282
+ logger.error(e.inspect)
283
+ logger.error("Failed attempting to find belongs_to for #{method_detail.pp}")
284
+ if(x == Populator::insistent_method_list.last)
285
+ raise CouldNotAssignAssociation.new "Populator failed to assign [#{value}] to belongs_to association [#{operator}]" unless value.nil?
286
+ end
184
287
  end
185
288
  end
186
289
  end
290
+
187
291
  end
188
292
  end
189
-
293
+
190
294
  def assignment( operator, record, value )
191
- #puts "DEBUG: RECORD CLASS #{record.class}"
295
+
192
296
  op = operator + '=' unless(operator.include?('='))
193
-
297
+
194
298
  begin
195
299
  record.send(op, value)
196
300
  rescue => e
@@ -199,7 +303,6 @@ module DataShift
199
303
  record.send(op, value.send( f) )
200
304
  break
201
305
  rescue => e
202
- #puts "DEBUG: insistent_assignment: #{e.inspect}"
203
306
  if f == Populator::insistent_method_list.last
204
307
  puts "I'm sorry I have failed to assign [#{value}] to #{operator}"
205
308
  raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
@@ -208,8 +311,8 @@ module DataShift
208
311
  end
209
312
  end
210
313
  end
211
-
212
-
314
+
315
+
213
316
  # Default values and over rides can be provided in Ruby/YAML ???? config file.
214
317
  #
215
318
  # Format :
@@ -224,80 +327,93 @@ module DataShift
224
327
  #
225
328
  def configure_from(load_object_class, yaml_file)
226
329
 
227
- data = YAML::load( File.open(yaml_file) )
228
-
229
- # TODO - MOVE DEFAULTS TO OWN MODULE
230
- # decorate the loading class with the defaults/ove rides to manage itself
231
- # IDEAS .....
232
- #
233
- #unless(@default_data_objects[load_object_class])
234
- #
235
- # @default_data_objects[load_object_class] = load_object_class.new
236
-
237
- # default_data_object = @default_data_objects[load_object_class]
238
-
239
-
240
- # default_data_object.instance_eval do
241
- # def datashift_defaults=(hash)
242
- # @datashift_defaults = hash
243
- # end
244
- # def datashift_defaults
245
- # @datashift_defaults
246
- # end
247
- #end unless load_object_class.respond_to?(:datashift_defaults)
248
- #end
249
-
250
- #puts load_object_class.new.to_yaml
251
-
252
- logger.info("Read Datashift loading config: #{data.inspect}")
253
-
330
+ data = YAML::load( ERB.new( IO.read(yaml_file) ).result )
331
+
332
+ # TODO - MOVE DEFAULTS TO OWN MODULE
333
+
334
+ logger.info("Setting Populator defaults: #{data.inspect}")
335
+
254
336
  if(data[load_object_class.name])
255
-
256
- logger.info("Assigning defaults and over rides from config")
257
-
337
+
258
338
  deflts = data[load_object_class.name]['datashift_defaults']
259
339
  default_values.merge!(deflts) if deflts
260
-
340
+
341
+ logger.info("Set Populator default_values: #{default_values.inspect}")
342
+
261
343
  ovrides = data[load_object_class.name]['datashift_overrides']
262
344
  override_values.merge!(ovrides) if ovrides
345
+ logger.info("Set Populator overrides: #{override_values.inspect}")
346
+
347
+ subs = data[load_object_class.name]['datashift_substitutions']
348
+
349
+ subs.each do |o, sub|
350
+ # TODO support single array as well as multiple [[..,..], [....]]
351
+ sub.each { |tuple| set_substitution(o, tuple) }
352
+ end if(subs)
353
+
263
354
  end
264
-
265
355
 
266
356
  end
267
-
357
+
358
+ # Set a default value to be used to populate Model.operator
359
+ # Generally defaults will be used when no value supplied.
360
+ def set_substitution(operator, value )
361
+ substitutions[operator] ||= []
362
+
363
+ substitutions[operator] << Struct::Substitution.new(value[0], value[1])
364
+ end
365
+
366
+ def substitutions
367
+ @substitutions ||= {}
368
+ end
369
+
370
+ def substitute( operator )
371
+ subs = substitutions[operator] || {}
372
+
373
+ subs.each do |s|
374
+ @original_value_before_override = @current_value
375
+ @current_value = @original_value_before_override.gsub(s.pattern.to_s, s.replacement.to_s)
376
+ end
377
+ end
378
+
379
+
268
380
  # Set a value to be used to populate Model.operator
269
381
  # Generally over-rides will be used regardless of what value caller supplied.
270
382
  def set_override_value( operator, value )
271
383
  override_values[operator] = value
272
384
  end
273
-
385
+
274
386
  def override_values
275
387
  @override_values ||= {}
276
388
  end
277
-
389
+
278
390
  def override_value( operator )
279
391
  if(override_values[operator])
280
392
  @original_value_before_override = @current_value
281
-
393
+
282
394
  @current_value = @override_values[operator]
283
395
  end
284
396
  end
285
-
397
+
398
+
399
+ def has_override?( operator )
400
+ return override_values.has_key?(operator)
401
+ end
402
+
286
403
  # Set a default value to be used to populate Model.operator
287
404
  # Generally defaults will be used when no value supplied.
288
405
  def set_default_value(operator, value )
289
406
  default_values[operator] = value
290
407
  end
291
-
408
+
292
409
  def default_values
293
410
  @default_values ||= {}
294
411
  end
295
-
412
+
296
413
  # Return the default value for supplied operator
297
414
  def default_value(operator)
298
415
  default_values[operator]
299
416
  end
300
-
301
417
 
302
418
  def set_prefix( operator, value )
303
419
  prefixes[operator] = value
@@ -310,7 +426,7 @@ module DataShift
310
426
  def prefixes
311
427
  @prefixes ||= {}
312
428
  end
313
-
429
+
314
430
  def set_postfix(operator, value )
315
431
  postfixes[operator] = value
316
432
  end
@@ -318,12 +434,11 @@ module DataShift
318
434
  def postfix(operator)
319
435
  postfixes[operator]
320
436
  end
321
-
437
+
322
438
  def postfixes
323
439
  @postfixes ||= {}
324
440
  end
325
-
326
-
441
+
327
442
  end
328
-
443
+
329
444
  end