Class: Reporter::Formula

Inherits:
Object
  • Object
show all
Defined in:
lib/reporter/formula.rb

Overview

Formula parser and calculator

Author

Matthijs Groen

This class has two main functions:

  1. to parse formula into ready-to-use-arrays

  2. use those arrays to perform calculations

Parsing formula

my_formula = Formula.new(“100% – (MAX(score – 5, 0) * 10%)”) => Formula my_formula_data = Formula.make(“100% – (MAX(score – 5, 0) * 10%)”) => Array

The array format used for formula data is [:operator, [parameter, parameter]] the parameters can also be arrays: e.g. sub-calculations

The text formula can be build with the following elements:

operators:

-

subtract. subtracts the right side from the left side argument.

*

multiply. multiplies the left side with the right side argument.

/

divide. divides the left side with the ride side argument.

+

add. adds the right side to the left side argument.

^

power. multiplies the left side by the power of the right side.

functions:

functions have the format of name(parameters) the parameters of the function will be pre calculated before the code of the function is executed. supported functions:

max

selects the biggest value from the provided values

min

selects the smallest value from the provided values

sum

creates a sum of all the provided values

avg

creates an average of all the provided values

select

selects the value with the index of the first parameter

empty

returns 1 if the given string is empty, 0 otherwise

parenthesis:

parentesis can be used to group calculation parts

variables:

terms that start with a alfabetic character and contain only alfanumeric characters and underscores can be used as variables. A hash with variables should be supplied when the calculation is performed

numeric values:

numeric values like integers, floats and percentages are also allowed. Percentages will be converted to floats. 3% and 66% will be converted to resp. 100% / 3 and 200% / 3

Performing calculations

my_formula.call(:score => 7.0) => 0.8 (using the above formula example) Formula.calculate(my_formula_data, :score => 3.0) => 1.0 (using the above formula example)

Constant Summary collapse

OPERATORS =

Known operators

"-*/+^"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(code) ⇒ Formula

parse the given code formula in an array using the format calculation = [operation, [parameter, parameter]] a parameter can ofcourse be in turn another calculation



59
60
61
62
# File 'lib/reporter/formula.rb', line 59

def initialize(code)
	@calculation = Reporter::Formula.make code
	#puts "#{@calculation.inspect}"
end

Instance Attribute Details

#calculationObject (readonly)

Returns the value of attribute calculation.



64
65
66
# File 'lib/reporter/formula.rb', line 64

def calculation
  @calculation
end

Class Method Details

.calculate(calculation, input) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/reporter/formula.rb', line 168

def self.calculate(calculation, input)
	operation, parameters = *calculation

	parameters = parameters.collect do |parameter|
		parameter.is_a?(Array) ? calculate(parameter, input) : parameter
	end

	return nil if (parameters[0].nil? or parameters[1].nil?) and [:add, :subtract, :times, :divide, :power].include? operation

	case operation
		when :add then
			parameters[0] + parameters[1]
		when :subtract then
			parameters[0] - parameters[1]
		when :times then
			parameters[0] * parameters[1]
		when :divide then
			parameters[1] == 0 ? nil : parameters[0].to_f / parameters[1].to_f
		when :power then
			parameters[0] ** parameters[1]
		# functions:
		when :max then
			parameters.compact.max
		when :min then
			parameters.compact.min
		when :sum then
			begin
				result = 0.0
				parameters.each { |value| result += value || 0.0 }
				result
			end
		when :select then
			begin
				index = parameters.shift
				index.is_a?(Numeric) ? parameters[index - 1] : nil
			end
		when :avg then
			begin
				items = parameters.compact
				result = 0.0
				items.each { |value| result += value }
				result / items.length
			end
		when :empty then
			begin
				result = parameters.collect { |item| item.to_s.strip == "" ? 1 : 0 }
				result.include?(0) ? 0 : 1 					
			end
		# variables
		when :term then
			begin
				raise "Can't find  term: #{parameters[0]}. Has keys: #{input.keys.collect(&:to_s).sort.inspect}" unless input.has_key? parameters[0]
				input[parameters[0]]
			end
		when :negative_term then
			begin
				raise "Can't find  term: #{parameters[0]}. Has keys: #{input.keys.sort.inspect}" unless input.has_key? parameters[0]
				val = input[parameters[0]]
				return nil unless val
				- val
			end
		when :literal
			parameters[0]
		when :text
			parameters[0]
		# no-op
		when nil, :percentage then
			parameters[0].to_f
	end
end

.calculation_to_s(calculation, input, solve = false) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/reporter/formula.rb', line 101

def self.calculation_to_s(calculation, input, solve = false)
	operation, parameters = * calculation

	string_parameters = parameters.collect do |parameter|
		parameter.is_a?(Array) ? "#{calculation_to_s(parameter, input, solve)}" : parameter
	end
	case operation
		when :add,
			:subtract,
			:times,
			:divide,
			:power then
			"(#{string_parameters[0]} #{{:add => "+",
																	 :subtract => "-",
																	 :times => "*",
																	 :divide => "/",
																	 :power => "^"}[operation]} #{string_parameters[1]})"
		# functions:
		when :max,
			:min,
			:sum,
			:select,
			:avg,
			:empty then
			if solve
				result = calculate(calculation, input)
				"#{operation}(#{string_parameters * ","})[#{result}]"
			else
				"#{operation}(#{string_parameters * ","})"
			end
		# variables
		when :text then
			"\"#{string_parameters[0]}\""
		when :term then
			"#{string_parameters[0]}[#{input[string_parameters[0]] ? input[string_parameters[0]] : "nil"}]"
		when :negative_term then
			"-#{string_parameters[0]}[#{input[string_parameters[0]] ? input[string_parameters[0]] : "nil"}]"
		when :literal then
			begin
				"nil" if string_parameters[0].nil?
			end
		# no-op
		when nil then
			string_parameters[0].to_s
		when :percentage then
			"#{string_parameters[0] * 100.0}%"
		else
			"!unsupported(#{operation}}"
	end
end

.make(code) ⇒ Object

Parses the given formula as text and returns the formula in nested array form.



67
68
69
70
71
72
73
74
75
# File 'lib/reporter/formula.rb', line 67

def self.make(code)
	#puts "parsing: #{code}"
	begin
		parse_operation(code)
	rescue StandardError => e
		puts "Error in formula: #{code}: #{e}"
		raise
	end
end

.term_list(calculation, input = {}) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/reporter/formula.rb', line 152

def self.term_list(calculation, input = {})
	operation, parameters = *calculation

	parameters = parameters.collect do |parameter|
		parameter.is_a?(Array) ? term_list(parameter, input) : parameter
	end

	case operation
		# variables
		when :term then
				input[parameters[0]] = :term
		when :negative_term then
				input[parameters[0]] = :term
	end
end

Instance Method Details

#call(input) ⇒ Object

executes the formula with a hash of given calculation terms



78
79
80
81
82
83
84
85
# File 'lib/reporter/formula.rb', line 78

def call(input)
	begin
		Reporter::Formula.calculate(@calculation, input)
	rescue StandardError => e
		Rails.logger.error "Error executing formula: #{Reporter::Formula.calculation_to_s(@calculation, input)} : #{e.message}"
		raise
	end
end

#solve(input) ⇒ Object



97
98
99
# File 'lib/reporter/formula.rb', line 97

def solve(input)
	Reporter::Formula.calculation_to_s(@calculation, input, true)
end

#term_listObject



87
88
89
90
91
# File 'lib/reporter/formula.rb', line 87

def term_list
	terms = {}
	Reporter::Formula.term_list @calculation, terms
	terms.keys
end

#to_string(input) ⇒ Object



93
94
95
# File 'lib/reporter/formula.rb', line 93

def to_string(input)
	Reporter::Formula.calculation_to_s(@calculation, input)
end