reporter 0.0.1

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.
@@ -0,0 +1,371 @@
1
+ # Formula parser and calculator
2
+ # Author:: Matthijs Groen
3
+ #
4
+ # This class has two main functions:
5
+ # 1. to parse formula into ready-to-use-arrays
6
+ # 2. use those arrays to perform calculations
7
+ #
8
+ #
9
+ # = Parsing formula
10
+ # my_formula = Formula.new("100% – (MAX(score – 5, 0) * 10%)") => Formula
11
+ # my_formula_data = Formula.make("100% – (MAX(score – 5, 0) * 10%)") => Array
12
+ #
13
+ # The array format used for formula data is [:operator, [parameter, parameter]]
14
+ # the parameters can also be arrays: e.g. sub-calculations
15
+ #
16
+ # The text formula can be build with the following elements:
17
+ # == operators:
18
+ # -:: subtract. subtracts the right side from the left side argument.
19
+ # *:: multiply. multiplies the left side with the right side argument.
20
+ # /:: divide. divides the left side with the ride side argument.
21
+ # +:: add. adds the right side to the left side argument.
22
+ # ^:: power. multiplies the left side by the power of the right side.
23
+ #
24
+ # == functions:
25
+ # functions have the format of name(parameters)
26
+ # the parameters of the function will be pre calculated before the code of the function is executed.
27
+ # supported functions:
28
+ #
29
+ # max:: selects the biggest value from the provided values
30
+ # min:: selects the smallest value from the provided values
31
+ # sum:: creates a sum of all the provided values
32
+ # avg:: creates an average of all the provided values
33
+ # select:: selects the value with the index of the first parameter
34
+ # empty:: returns 1 if the given string is empty, 0 otherwise
35
+ #
36
+ # == parenthesis:
37
+ # parentesis can be used to group calculation parts
38
+ #
39
+ # == variables:
40
+ # terms that start with a alfabetic character and contain only alfanumeric characters and underscores
41
+ # can be used as variables. A hash with variables should be supplied when the calculation is performed
42
+ #
43
+ # == numeric values:
44
+ # numeric values like integers, floats and percentages are also allowed. Percentages will be converted to floats.
45
+ # 3% and 66% will be converted to resp. 100% / 3 and 200% / 3
46
+ #
47
+ # = Performing calculations
48
+ # my_formula.call(:score => 7.0) => 0.8 (using the above formula example)
49
+ # Formula.calculate(my_formula_data, :score => 3.0) => 1.0 (using the above formula example)
50
+ #
51
+ class Reporter::Formula
52
+
53
+ # Known operators
54
+ OPERATORS = "-*/+^"
55
+
56
+ # parse the given code formula in an array using the format
57
+ # calculation = [operation, [parameter, parameter]]
58
+ # a parameter can ofcourse be in turn another calculation
59
+ def initialize(code)
60
+ @calculation = Reporter::Formula.make code
61
+ #puts "#{@calculation.inspect}"
62
+ end
63
+
64
+ attr_reader :calculation
65
+
66
+ # Parses the given formula as text and returns the formula in nested array form.
67
+ def self.make(code)
68
+ #puts "parsing: #{code}"
69
+ begin
70
+ parse_operation(code)
71
+ rescue StandardError => e
72
+ puts "Error in formula: #{code}: #{e}"
73
+ raise
74
+ end
75
+ end
76
+
77
+ # executes the formula with a hash of given calculation terms
78
+ def call(input)
79
+ begin
80
+ Reporter::Formula.calculate(@calculation, input)
81
+ rescue StandardError => e
82
+ Rails.logger.error "Error executing formula: #{Reporter::Formula.calculation_to_s(@calculation, input)} : #{e.message}"
83
+ raise
84
+ end
85
+ end
86
+
87
+ def term_list
88
+ terms = {}
89
+ Reporter::Formula.term_list @calculation, terms
90
+ terms.keys
91
+ end
92
+
93
+ def to_string(input)
94
+ Reporter::Formula.calculation_to_s(@calculation, input)
95
+ end
96
+
97
+ def solve(input)
98
+ Reporter::Formula.calculation_to_s(@calculation, input, true)
99
+ end
100
+
101
+ def self.calculation_to_s(calculation, input, solve = false)
102
+ operation, parameters = * calculation
103
+
104
+ string_parameters = parameters.collect do |parameter|
105
+ parameter.is_a?(Array) ? "#{calculation_to_s(parameter, input, solve)}" : parameter
106
+ end
107
+ case operation
108
+ when :add,
109
+ :subtract,
110
+ :times,
111
+ :divide,
112
+ :power then
113
+ "(#{string_parameters[0]} #{{:add => "+",
114
+ :subtract => "-",
115
+ :times => "*",
116
+ :divide => "/",
117
+ :power => "^"}[operation]} #{string_parameters[1]})"
118
+ # functions:
119
+ when :max,
120
+ :min,
121
+ :sum,
122
+ :select,
123
+ :avg,
124
+ :empty then
125
+ if solve
126
+ result = calculate(calculation, input)
127
+ "#{operation}(#{string_parameters * ","})[#{result}]"
128
+ else
129
+ "#{operation}(#{string_parameters * ","})"
130
+ end
131
+ # variables
132
+ when :text then
133
+ "\"#{string_parameters[0]}\""
134
+ when :term then
135
+ "#{string_parameters[0]}[#{input[string_parameters[0]] ? input[string_parameters[0]] : "nil"}]"
136
+ when :negative_term then
137
+ "-#{string_parameters[0]}[#{input[string_parameters[0]] ? input[string_parameters[0]] : "nil"}]"
138
+ when :literal then
139
+ begin
140
+ "nil" if string_parameters[0].nil?
141
+ end
142
+ # no-op
143
+ when nil then
144
+ string_parameters[0].to_s
145
+ when :percentage then
146
+ "#{string_parameters[0] * 100.0}%"
147
+ else
148
+ "!unsupported(#{operation}}"
149
+ end
150
+ end
151
+
152
+ def self.term_list(calculation, input = {})
153
+ operation, parameters = *calculation
154
+
155
+ parameters = parameters.collect do |parameter|
156
+ parameter.is_a?(Array) ? term_list(parameter, input) : parameter
157
+ end
158
+
159
+ case operation
160
+ # variables
161
+ when :term then
162
+ input[parameters[0]] = :term
163
+ when :negative_term then
164
+ input[parameters[0]] = :term
165
+ end
166
+ end
167
+
168
+ def self.calculate(calculation, input)
169
+ operation, parameters = *calculation
170
+
171
+ parameters = parameters.collect do |parameter|
172
+ parameter.is_a?(Array) ? calculate(parameter, input) : parameter
173
+ end
174
+
175
+ return nil if (parameters[0].nil? or parameters[1].nil?) and [:add, :subtract, :times, :divide, :power].include? operation
176
+
177
+ case operation
178
+ when :add then
179
+ parameters[0] + parameters[1]
180
+ when :subtract then
181
+ parameters[0] - parameters[1]
182
+ when :times then
183
+ parameters[0] * parameters[1]
184
+ when :divide then
185
+ parameters[1] == 0 ? nil : parameters[0].to_f / parameters[1].to_f
186
+ when :power then
187
+ parameters[0] ** parameters[1]
188
+ # functions:
189
+ when :max then
190
+ parameters.compact.max
191
+ when :min then
192
+ parameters.compact.min
193
+ when :sum then
194
+ begin
195
+ result = 0.0
196
+ parameters.each { |value| result += value || 0.0 }
197
+ result
198
+ end
199
+ when :select then
200
+ begin
201
+ index = parameters.shift
202
+ index.is_a?(Numeric) ? parameters[index - 1] : nil
203
+ end
204
+ when :avg then
205
+ begin
206
+ items = parameters.compact
207
+ result = 0.0
208
+ items.each { |value| result += value }
209
+ result / items.length
210
+ end
211
+ when :empty then
212
+ begin
213
+ result = parameters.collect { |item| item.to_s.strip == "" ? 1 : 0 }
214
+ result.include?(0) ? 0 : 1
215
+ end
216
+ # variables
217
+ when :term then
218
+ begin
219
+ raise "Can't find term: #{parameters[0]}. Has keys: #{input.keys.collect(&:to_s).sort.inspect}" unless input.has_key? parameters[0]
220
+ input[parameters[0]]
221
+ end
222
+ when :negative_term then
223
+ begin
224
+ raise "Can't find term: #{parameters[0]}. Has keys: #{input.keys.sort.inspect}" unless input.has_key? parameters[0]
225
+ val = input[parameters[0]]
226
+ return nil unless val
227
+ - val
228
+ end
229
+ when :literal
230
+ parameters[0]
231
+ when :text
232
+ parameters[0]
233
+ # no-op
234
+ when nil, :percentage then
235
+ parameters[0].to_f
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ def self.parse_operation(code)
242
+ #puts "parsing: #{code}"
243
+
244
+ # check if the code is totally surrounded by parenthesis that can be removed. remove them if possible
245
+ code = ungroup code
246
+
247
+ left, right, operator, operator_char = "", "", nil, ""
248
+ char_index, group_level, in_text = 0, 0, false
249
+ while char_index < code.length
250
+ char = code[char_index, 1]
251
+ if operator.nil? and OPERATORS.include? char and group_level == 0 and not in_text
252
+ operator = case char
253
+ when "-" then :subtract
254
+ when "+" then :add
255
+ when "*" then :times
256
+ when "/" then :divide
257
+ when "^" then :power
258
+ end
259
+ operator_char = char
260
+ else
261
+ in_text = !in_text if char == "\""
262
+ group_level += (char == "(") ? 1 : -1 if "()".include? char and not in_text
263
+ operator ? right += char : left += char
264
+ end
265
+ char_index += 1
266
+ end
267
+ begin
268
+ #puts "parse-result: #{operator}, #{left}, #{right}"
269
+
270
+ return parse_definition(left.strip) unless operator
271
+ return parse_definition(operator_char + right) if operator and left.strip == ""
272
+ return operator, [parse_operation(left.strip), parse_operation(right.strip)]
273
+ rescue StandardError => e
274
+ puts "can't parse code: \"#{code}\""
275
+ raise
276
+ end
277
+ end
278
+
279
+ def self.parse_definition(code)
280
+ code = code.strip
281
+
282
+ # text "some text"
283
+ if result = code.match(/\A"([^"]*)"\z/)
284
+ return :text, result[1]
285
+
286
+ # parse percentages 100%, 10%
287
+ elsif result = code.match(/\A([\d\.]+)%\z/)
288
+ return :percentage, [1.0 / 3.0] if result[1].to_f == 33.0
289
+ return :percentage, [2.0 / 3.0] if result[1].to_f == 66.0
290
+ return :percentage, [result[1].to_f / 100.0]
291
+
292
+ # parse function calls in the format FUNCTION(parameters)
293
+ elsif result = code.upcase.match(/\A([A-Z_]+)\((.+)\)\z/m)
294
+ return result[1].downcase.to_sym, self.parameterize(result[2][0 .. -1]).collect { |parameter| parse_operation(parameter) }
295
+
296
+ # parse numeric value
297
+ elsif code.to_i.to_s == code
298
+ return nil, [code.to_i]
299
+
300
+ # parse numeric value
301
+ elsif code.to_f.to_s == code
302
+ return nil, [code.to_f]
303
+
304
+ # parse literal
305
+ elsif result = code.upcase.match(/\ANIL\z/)
306
+ return :literal, [nil]
307
+
308
+ # parse variable term
309
+ elsif result = code.upcase.match(/\A([A-Z][A-Z0-9_]*)\z/)
310
+ return :term, [result[1].downcase.to_sym]
311
+ elsif result = code.upcase.match(/\A-([A-Z][A-Z0-9_]*)\z/)
312
+ return :negative_term, [result[1].downcase.to_sym]
313
+ else
314
+ raise "can't parse code: \"#{code}\""
315
+ end
316
+ end
317
+
318
+ # check if the code is totally surrounded by parenthesis that can be removed. remove them if possible
319
+ # examples:
320
+ # ungroup("(my code ()") => "(my code ()"
321
+ # ungroup("(my code ())") => "my code ()"
322
+ # ungroup("(my code) ()") => "(my code) ()"
323
+ # ungroup("m(my code)") => "m(my code)"
324
+ def self.ungroup(code)
325
+ # exit if the code does not start with an opening parentesis
326
+ return code unless code[0, 1] == "("
327
+ return code unless code[-1, 1] == ")"
328
+ # since we know the first character is an opening parenthesis,
329
+ # start parsing at the second character, and assume grouping level 1
330
+ group_level, char_index, in_text = 1, 1, false
331
+ while char_index < code.length
332
+ char = code[char_index, 1]
333
+ in_text = !in_text if char == "\""
334
+ group_level += 1 if char == "(" and not in_text
335
+ group_level -= 1 if char == ")" and not in_text
336
+
337
+ # only strip the first and last parenthesis if we exit the grouping AND we reached the last character
338
+ return code[1 .. -2] if group_level == 0 and char_index == code.length - 1
339
+ char_index += 1
340
+ end
341
+ code
342
+ end
343
+
344
+ #
345
+ # SUM(1, 2), SELECT(1, 2, 3)
346
+ def self.parameterize(code)
347
+ result = []
348
+ # since we know the first character is an opening parenthesis,
349
+ # start parsing at the second character, and assume grouping level 1
350
+ current_param, char = "", ""
351
+ group_level, char_index, in_text = 0, 0, false
352
+ while char_index <= code.length
353
+ char = code[char_index, 1]
354
+ in_text = !in_text if char == "\""
355
+ group_level += 1 if char == "(" and not in_text
356
+ group_level -= 1 if char == ")" and not in_text
357
+
358
+ if char == "," and group_level == 0 and not in_text
359
+ result << current_param
360
+ current_param = ""
361
+ else
362
+ current_param << char
363
+ end
364
+
365
+ char_index += 1
366
+ end
367
+ result << current_param unless current_param == ""
368
+ result
369
+ end
370
+
371
+ end
@@ -0,0 +1,37 @@
1
+ class Reporter::ResultRow
2
+
3
+ def initialize(data_set, scope_serialization)
4
+ @data_set = data_set
5
+ @scope_serialization = scope_serialization
6
+ @field_cache = { }
7
+ end
8
+
9
+ def [] field
10
+ field_cache[field] ||= load_field_values(field)[field]
11
+ end
12
+
13
+ protected
14
+
15
+ attr_reader :data_set, :scope_serialization
16
+ attr_accessor :field_cache
17
+
18
+ def load_field_values(*fields)
19
+ execute_fields *(fields + [{ :scope => scope_serialization, :row => self }])
20
+ end
21
+
22
+ def execute_fields *fields
23
+ options = fields.extract_options!
24
+ temp_scope = data_set.data_source.scopes.current_scope
25
+ field_options = {}
26
+ data_set.data_source.scopes.apply_scope options[:scope] if options[:scope]
27
+ field_options[:row] = options[:row] if options[:row]
28
+ results = {}
29
+ fields.each do |field|
30
+ results[field] = data_set.data_structure.field_value_of field, field_options
31
+ end
32
+ data_set.data_source.scopes.apply_scope temp_scope
33
+ results
34
+ end
35
+
36
+
37
+ end
@@ -0,0 +1,37 @@
1
+ class Reporter::Scope::Base
2
+
3
+ def initialize scoping, name, data_source, mappings
4
+ @data_source = data_source
5
+ @scoping = scoping
6
+ @name = name
7
+ @mappings = scoping.normalize_mapping mappings
8
+ @limit = nil
9
+ end
10
+
11
+ def limit= *args
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def change value
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def value
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def apply_on source
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def iterate &block
28
+ raise NotImplementedError
29
+ end
30
+
31
+ attr_reader :name, :mappings, :limit
32
+
33
+ protected
34
+
35
+ attr_reader :scoping, :data_source
36
+
37
+ end
@@ -0,0 +1,109 @@
1
+ class Reporter::Scope::DateScope < Reporter::Scope::Base
2
+
3
+ def initialize scoping, name, data_source, mappings
4
+ super scoping, name, data_source, mappings
5
+ @period = nil
6
+ end
7
+
8
+ def limit= period
9
+ @limit = period_as_range(period)
10
+ end
11
+
12
+ def value
13
+ active_period
14
+ end
15
+
16
+ def human_name
17
+ active_period.human_name
18
+ end
19
+
20
+ def active_period
21
+ get_period || @limit
22
+ end
23
+
24
+ def change period
25
+ scoping.serialize_scope name, period_as_range(period)
26
+ end
27
+
28
+ def apply_on source, period = nil
29
+ raise "No mapped column for source #{source}" unless mappings.has_key? source.name
30
+ column = mappings[source.name]
31
+ # The time period range is inclusive in all aspects of the report to detemine correctly if it runs to the end of a year
32
+ # or the beginning of another year.
33
+ # In SQL, the period is used as a BETWEEN statement, with the end exclusive.
34
+ # In code when we want the whole of Januari, we use 1-1 00:00 till 1-31 23:59
35
+ # in SQL, we need to use BETWEEN 1-1 00:00 AND 2-1 00:00
36
+ # when a date includes time, we need to add 1 second. If a date has no time, we need to include 1 day.
37
+
38
+ period ||= active_period.dup
39
+ period = if period.end.is_a? Date
40
+ period.begin .. period.end.advance(:days => 1)
41
+ else
42
+ period.begin .. period.end.advance(:seconds => 1)
43
+ end
44
+
45
+ case column
46
+ when String :
47
+ source.where(column.to_sym => period)
48
+ when Hash : begin
49
+ column.each do |key, value|
50
+ source = source.joins(key).where(key => { value => period })
51
+ end
52
+ source
53
+ end
54
+ end
55
+ end
56
+
57
+ def group_on source, period_type
58
+ column = mappings[source.name]
59
+ case column
60
+ when String :
61
+ "#{period_type.to_s.upcase}(#{column})"
62
+ when Hash : begin
63
+ column.each do |key, value|
64
+ table_name = table_name_of_association source, key
65
+ return "#{period_type.to_s.upcase}(#{table_name}.#{value})"
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def self.possible_scopes sources
72
+ results = []
73
+ global_dates = nil
74
+ specific_dates = { :type => :date, :match => :loose }
75
+ sources.each do |source|
76
+ global_dates = global_dates ? (global_dates & source.date_columns) : source.date_columns
77
+ specific_dates[source.model_name] = source.date_columns
78
+ end
79
+ results << specific_dates
80
+ global_dates.each do |reference|
81
+ result_hash = {:type => :date, :match => :exact}
82
+ sources.each { |source| result_hash[source.model_name] = reference }
83
+ results << result_hash
84
+ end if global_dates
85
+ results
86
+ end
87
+
88
+ private
89
+
90
+ def get_period
91
+ scoping.unserialize_scope name
92
+ end
93
+
94
+ def period_as_range period
95
+ case period
96
+ when Range :
97
+ period.dup
98
+ when Fixnum :
99
+ Date.civil(period).beginning_of_year .. Date.civil(period).end_of_year
100
+ when :year_cumulative :
101
+ active_period.begin.beginning_of_year .. active_period.end
102
+ end
103
+ end
104
+
105
+ def table_name_of_association source, name
106
+ source.reflect_on_association(name.to_sym).klass.table_name
107
+ end
108
+
109
+ end
@@ -0,0 +1,154 @@
1
+ class Reporter::Scope::ReferenceScope < Reporter::Scope::Base
2
+
3
+ def initialize scoping, name, data_source, *args
4
+ mappings = args.extract_options!
5
+ mappings = create_mappings_for_object_type(data_source, args.first, mappings) if args.first
6
+ super scoping, data_source, name, mappings
7
+ @limiter = nil
8
+ @id_collection = nil
9
+ end
10
+
11
+ def limit= object_or_array
12
+ @limiter = object_or_array
13
+ end
14
+
15
+ def group_object
16
+ scoping.unserialize_scope(name)
17
+ end
18
+
19
+ def value
20
+ group_object || @limiter
21
+ end
22
+
23
+ def change object
24
+ scoping.serialize_scope name, item
25
+ end
26
+
27
+ def human_name
28
+ limiter = group_object || @limiter
29
+ return limiter.collect { |item| human_name_for item }.to_sentence if limiter.is_a? Enumerable
30
+ human_name_for limiter
31
+ end
32
+
33
+ def apply_on source
34
+ raise "No mapped column for source #{source}" unless mappings.has_key? source.name
35
+ reference = mappings[source.name]
36
+
37
+ limiter = group_object || @limiter
38
+ if limiter
39
+ q, values = limit_through_association source, reference, limiter
40
+ source.where(q, values)
41
+ else
42
+ source
43
+ end
44
+ end
45
+
46
+ def iterate items, data_set, &block
47
+ items ||= @limiter
48
+ if items.is_a? Enumerable
49
+ items.each do |item|
50
+ scoping.serialize_scope name, item
51
+ yield data_set.get_row
52
+ end
53
+ scoping.serialize_scope name, nil
54
+ else
55
+ scoping.serialize_scope name, items
56
+ yield data_set.get_row
57
+ scoping.serialize_scope name, nil
58
+ end
59
+ end
60
+
61
+ def self.possible_scopes sources
62
+ results = []
63
+ # check reflections
64
+ reflection_references = nil
65
+ sources.each do |source|
66
+ relations = source.relations_to_objects
67
+ relation_pool = relations.keys + [source.active_record]
68
+ reflection_references = reflection_references ? (reflection_references & relation_pool) : relation_pool
69
+ end
70
+ (reflection_references || []).each do |reflection_object|
71
+ result_hash = { :type => :reference, :match => :loose, :object => reflection_object }
72
+ sources.each do |source|
73
+ fields = []
74
+ fields << "id" if (source.active_record == reflection_object)
75
+ fields += source.relations_to_objects[reflection_object] || []
76
+ result_hash[source.model_name] = fields
77
+ end
78
+ results << result_hash
79
+ end
80
+ #Rails.logger.info reflection_references.inspect
81
+
82
+ results
83
+ end
84
+
85
+ private
86
+
87
+ def limit_through_association source, reference, limiter
88
+ if reference.is_a? Array
89
+ query = [[], {}]
90
+ reference.each do |ref|
91
+ q, values = limit_through_association source, ref, limiter
92
+ query[0] << q
93
+ query[1].merge! values
94
+ end
95
+ return "(#{query[0].join ") OR ("})", query[1]
96
+ end
97
+ association = source.reflect_on_association(reference.to_sym)
98
+ if association.macro == :belongs_to
99
+ id_collection = get_ids_from limiter, reference, association
100
+ #Rails.logger.info "Belongs to association #{reference} limited by #{limiter}: #{id_collection.inspect}"
101
+ return "#{reference}_id IN(:#{reference}_ids)", { "#{reference}_ids".to_sym => id_collection }
102
+ elsif association.macro == :has_one and association.options[:through]
103
+ #Rails.logger.info "Has one through association #{reference} limited by #{limiter}"
104
+ through_association = association.options[:through]
105
+ limit_through_association source, through_association, limiter
106
+ end
107
+ end
108
+
109
+ def get_ids_from item, reference, association
110
+ return [] if item.nil?
111
+ return item.collect { |sub_item| get_ids_from sub_item, reference, association }.flatten if item.is_a? Enumerable
112
+ if item.class.ancestors.include? ActiveRecord::Base
113
+
114
+ return [item.id] if item.class == association.klass
115
+ return item.send("#{reference.to_s}_ids".to_sym) if item.respond_to? "#{reference.to_s}_ids"
116
+ return [item.send("#{reference.to_s}_id".to_sym)] if item.respond_to? "#{reference.to_s}_id"
117
+ end
118
+ []
119
+ end
120
+
121
+ def human_name_for item
122
+ item.name
123
+ end
124
+
125
+ def create_mappings_for_object_type data_source, object, mappings
126
+ possible = self.class.possible_scopes data_source.sources
127
+ #Rails.logger.info possible.inspect
128
+ possible.each do |reference_mapping|
129
+ return create_mapping_from(reference_mapping, mappings, data_source.sources) if reference_mapping[:object] == object
130
+ end
131
+ raise "No valid data-source mapping could be made with #{object.name}"
132
+ end
133
+
134
+ def create_mapping_from reference_mapping, mapping_specifics, sources
135
+ mapping = {}
136
+ sources.each do |source|
137
+ key = source.model_name.underscore.to_sym
138
+ columns = reference_mapping[source.model_name]
139
+ if columns.size == 1
140
+ mapping[key] = columns.first
141
+ else
142
+ specifics = [mapping_specifics[key]].flatten
143
+ specifics.each do |specific|
144
+ raise "No available reference to satisfy one of these columns (#{columns.to_sentence}) for datasource #{source.model_name}" unless specific and columns.include? specific
145
+ end
146
+ mapping[key] = mapping_specifics[key]
147
+ end
148
+ end
149
+ #{ :funeral => "work_area", :cbs_statistic => "work_area" }
150
+ #Rails.logger.info mapping.inspect
151
+ mapping
152
+ end
153
+
154
+ end