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