reporter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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