Integarted FormulaBuilder into Formula
This commit is contained in:
parent
b8a09b10ff
commit
9803b31f78
@ -33,11 +33,10 @@ en:
|
|||||||
parent:
|
parent:
|
||||||
parent_domain_mismatch: 'parent quantity has to be in the same domain'
|
parent_domain_mismatch: 'parent quantity has to be in the same domain'
|
||||||
formula:
|
formula:
|
||||||
invalid_formula: 'interpretation failed: %{msg}'
|
disallowed_syntax: 'cannot be parsed: %{syntax}'
|
||||||
unparsable_formula: 'cannot be parsed'
|
disallowed_token: 'includes disallowed token: "%{token}"'
|
||||||
disallowed_token: 'includes disallowed token "%{token}" (of type: %{ttype})'
|
|
||||||
disallowed_keyword: 'includes disallowed keyword: "%{keyword}"'
|
disallowed_keyword: 'includes disallowed keyword: "%{keyword}"'
|
||||||
disallowed_method: 'includes disallowed method call: %{method}'
|
disallowed_method: 'includes disallowed method call: "%{method}"'
|
||||||
unknown_quantity: 'contains undefined quantity: %{quantity}'
|
unknown_quantity: 'contains undefined quantity: %{quantity}'
|
||||||
body_trackers:
|
body_trackers:
|
||||||
index:
|
index:
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
module BodyTracking
|
module BodyTracking
|
||||||
module Formula
|
module Formula
|
||||||
require 'ripper'
|
|
||||||
QUANTITY_TTYPES = [:on_ident, :on_tstring_content, :on_const]
|
|
||||||
FUNCTIONS = ['abs', 'nil?']
|
|
||||||
|
|
||||||
class InvalidFormula < RuntimeError; end
|
class InvalidFormula < RuntimeError; end
|
||||||
class Formula
|
class Formula
|
||||||
def initialize(project, formula)
|
def initialize(project, formula)
|
||||||
@project_quantities = Quantity.where(project: project)
|
@project_quantities = Quantity.where(project: project)
|
||||||
@formula = formula
|
@formula = formula
|
||||||
@paramed_formula = nil
|
@parts = nil
|
||||||
@quantities = nil
|
@quantities = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -20,84 +16,18 @@ module BodyTracking
|
|||||||
# working test vectors:
|
# working test vectors:
|
||||||
# ((Energy-Calculated)/Energy).abs > 0.2
|
# ((Energy-Calculated)/Energy).abs > 0.2
|
||||||
# Fats.nil? || Fats/Proteins > 2
|
# Fats.nil? || Fats/Proteins > 2
|
||||||
errors = []
|
|
||||||
|
|
||||||
# 1st: check if formula is syntactically valid Ruby code
|
parser = FormulaBuilder.new(@formula)
|
||||||
begin
|
identifiers, parts = parser.parse
|
||||||
eval("-> { #{@formula} }")
|
errors = parser.errors
|
||||||
rescue ScriptError => e
|
|
||||||
errors << [:invalid_formula, {msg: e.message}]
|
|
||||||
end
|
|
||||||
|
|
||||||
# 2nd: check if formula contains only allowed token types
|
|
||||||
# 3rd: check for disallowed function calls
|
|
||||||
identifiers = []
|
|
||||||
disallowed = Hash.new { |h,k| h[k] = Set.new }
|
|
||||||
|
|
||||||
stree = [Ripper.sexp(@formula)]
|
|
||||||
errors << [:unparsable_formula, {}] unless stree.first
|
|
||||||
|
|
||||||
while stree.first
|
|
||||||
ttype, token, *rest = stree.shift
|
|
||||||
case ttype
|
|
||||||
when :program, :args_add_block, :paren
|
|
||||||
stree.unshift(*token)
|
|
||||||
when :binary
|
|
||||||
operator, token2 = rest
|
|
||||||
stree.unshift(token, token2)
|
|
||||||
when :method_add_arg
|
|
||||||
stree.unshift(token, *rest)
|
|
||||||
when :call
|
|
||||||
stree.unshift(token)
|
|
||||||
dot, method = rest
|
|
||||||
ftype, fname, floc = method
|
|
||||||
disallowed[:function] << fname unless FUNCTIONS.include?(fname)
|
|
||||||
when :fcall
|
|
||||||
ftype, fname, floc = token
|
|
||||||
disallowed[:function] << fname
|
|
||||||
when :vcall
|
|
||||||
ftype, fname, floc = token
|
|
||||||
identifiers << fname
|
|
||||||
when :arg_paren
|
|
||||||
stree.unshift(token)
|
|
||||||
when :var_ref
|
|
||||||
vtype, vname, vloc = token
|
|
||||||
case vtype
|
|
||||||
when :@const
|
|
||||||
identifiers << vname
|
|
||||||
when :@kw
|
|
||||||
disallowed[:keyword] << token if vname != 'nil'
|
|
||||||
end
|
|
||||||
when :@int, :@float
|
|
||||||
else
|
|
||||||
errors << [:disallowed_token, {token: token, ttype: ttype}]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
disallowed[:function].each { |f| errors << [:disallowed_function, {function: f}] }
|
|
||||||
disallowed[:keyword].each { |k| errors << [:disallowed_keyword, {keyword: k}] }
|
|
||||||
|
|
||||||
# 4th: check if identifiers used in formula correspond to existing quantities
|
|
||||||
identifiers.uniq!
|
|
||||||
quantities = @project_quantities.where(name: identifiers)
|
quantities = @project_quantities.where(name: identifiers)
|
||||||
quantities_names = quantities.pluck(:name)
|
quantities_names = quantities.pluck(:name)
|
||||||
(identifiers - quantities_names).each do |q|
|
(identifiers - quantities_names).each do |q|
|
||||||
errors << [:unknown_quantity, {quantity: q}]
|
errors << [:unknown_quantity, {quantity: q}]
|
||||||
end
|
end
|
||||||
|
|
||||||
if errors.empty?
|
@parts, @quantities = parts, quantities if errors.empty?
|
||||||
@quantities = quantities
|
|
||||||
@paramed_formula = Ripper.lex(@formula).map do |*, ttype, token|
|
|
||||||
if QUANTITY_TTYPES.include?(ttype) && quantities_names.include?(token)
|
|
||||||
"params['#{token}'][_index]"
|
|
||||||
else
|
|
||||||
token
|
|
||||||
end
|
|
||||||
end.join
|
|
||||||
@paramed_formula =
|
|
||||||
"params.values.first.each_with_index.map { |*, _index| #{@paramed_formula} }"
|
|
||||||
end
|
|
||||||
|
|
||||||
errors
|
errors
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -111,6 +41,7 @@ module BodyTracking
|
|||||||
@quantities.to_a
|
@quantities.to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#"params.values.first.each_with_index.map { |*, _index| #{@paramed_formula} }"
|
||||||
def calculate(inputs)
|
def calculate(inputs)
|
||||||
raise RuntimeError, 'Invalid formula' unless self.valid?
|
raise RuntimeError, 'Invalid formula' unless self.valid?
|
||||||
|
|
||||||
|
230
lib/body_tracking/formula_builder.rb
Normal file
230
lib/body_tracking/formula_builder.rb
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
module BodyTracking
|
||||||
|
module FormulaBuilder
|
||||||
|
require 'ripper'
|
||||||
|
require 'set'
|
||||||
|
|
||||||
|
class FormulaBuilder < Ripper::SexpBuilder
|
||||||
|
def initialize(*args)
|
||||||
|
super(*args)
|
||||||
|
@disallowed = Hash.new { |h,k| h[k] = Set.new }
|
||||||
|
@identifiers = Set.new
|
||||||
|
@parts = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def errors
|
||||||
|
@errors = []
|
||||||
|
@disallowed.each { |k, v| v.each { |e| @errors << ["disallowed_#{k}", {k => e} ] } }
|
||||||
|
@errors
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
DECIMAL_METHODS = ['abs', 'nil?']
|
||||||
|
QUANTITY_METHODS = ['all', 'lastBefore']
|
||||||
|
METHODS = DECIMAL_METHODS + QUANTITY_METHODS
|
||||||
|
|
||||||
|
events = private_instance_methods(false).grep(/\Aon_/) {$'.to_sym}
|
||||||
|
(PARSER_EVENTS - events).each do |event|
|
||||||
|
module_eval(<<-End, __FILE__, __LINE__ + 1)
|
||||||
|
def on_#{event}(*args)
|
||||||
|
@disallowed[:token] << args.to_s + ' [#{event}]'
|
||||||
|
[:bt_unimplemented, args]
|
||||||
|
end
|
||||||
|
End
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_parse_error(error)
|
||||||
|
@disallowed[:syntax] << error
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_program(stmts)
|
||||||
|
@parts << {type: :indexed, content: join_stmts(stmts)}
|
||||||
|
[@identifiers, @parts]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_string_content
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_string_add(str, new_str)
|
||||||
|
str << new_str
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_string_literal(str)
|
||||||
|
@identifiers << str
|
||||||
|
[:bt_quantity, str]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_args_new
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_args_add(args, new_arg)
|
||||||
|
args << new_arg
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_args_add_block(args, block)
|
||||||
|
raise NotImplementedError if block
|
||||||
|
args
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_arg_paren(args)
|
||||||
|
"(" <<
|
||||||
|
args.map do |arg|
|
||||||
|
ttype, token = arg
|
||||||
|
case ttype
|
||||||
|
when :bt_quantity
|
||||||
|
"quantities['#{token}']"
|
||||||
|
when :bt_expression
|
||||||
|
@parts << {type: :indexed, content: token}
|
||||||
|
"parts[#{@parts.length - 1}]"
|
||||||
|
else
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end.join(',') <<
|
||||||
|
")"
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_stmts_new
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_stmts_add(stmts, new_stmt)
|
||||||
|
stmts << new_stmt
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_paren(stmts)
|
||||||
|
[
|
||||||
|
:bt_expression,
|
||||||
|
"(" << join_stmts(stmts) << ")"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_call(left, dot, right)
|
||||||
|
method, mtype =
|
||||||
|
case right[0]
|
||||||
|
when :bt_ident
|
||||||
|
case
|
||||||
|
when DECIMAL_METHODS.include?(right[1])
|
||||||
|
[right[1], :numeric_method]
|
||||||
|
when QUANTITY_METHODS.include?(right[1])
|
||||||
|
[right[1], :quantity_method]
|
||||||
|
else
|
||||||
|
@disallowed[:method] << right[1]
|
||||||
|
[right[1], :unknown_method]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
case left[0]
|
||||||
|
when :bt_quantity
|
||||||
|
if mtype == :quantity_method
|
||||||
|
part_index = @parts.length
|
||||||
|
@parts << {type: :unindexed,
|
||||||
|
content: "quantities['#{left[1]}']#{dot.to_s}#{method}"}
|
||||||
|
[:bt_quantity_method_call, "parts[#{part_index}]", part_index]
|
||||||
|
else
|
||||||
|
[:bt_numeric_method_call, "quantities['#{left[1]}'][_index]#{dot.to_s}#{method}"]
|
||||||
|
end
|
||||||
|
when :bt_quantity_method_call
|
||||||
|
if mtype == :quantity_method
|
||||||
|
@parts[left[2]][:content] << "#{dot.to_s}#{method}"
|
||||||
|
left
|
||||||
|
else
|
||||||
|
[:bt_numeric_method_call, "#{left[1]}#{dot.to_s}#{method}"]
|
||||||
|
end
|
||||||
|
when :bt_numeric_method_call
|
||||||
|
if mtype == :quantity_method
|
||||||
|
# TODO: add error reporting
|
||||||
|
raise NotImplementedError
|
||||||
|
else
|
||||||
|
[:bt_numeric_method_call, "#{left[1]}#{dot.to_s}#{method}"]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_fcall(token)
|
||||||
|
@disallowed[:method] = token[1]
|
||||||
|
[:bt_numeric_method_call, token[1]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_vcall(token)
|
||||||
|
case token[0]
|
||||||
|
when :bt_ident
|
||||||
|
@identifiers << token[1]
|
||||||
|
[:bt_quantity, token[1]]
|
||||||
|
else
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_method_add_arg(method, paren)
|
||||||
|
case method[0]
|
||||||
|
when :bt_quantity_method_call
|
||||||
|
@parts[method[2]][:content] << paren
|
||||||
|
method
|
||||||
|
when :bt_numeric_method_call
|
||||||
|
[:bt_numeric_method_call, "#{method[1]}#{paren}"]
|
||||||
|
else
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_binary(left, op, right)
|
||||||
|
[
|
||||||
|
:bt_expression,
|
||||||
|
[left, right].map do |side|
|
||||||
|
side[0] == :bt_quantity ? "quantities['#{side[1]}'][_index]" : "#{side[1]}"
|
||||||
|
end.join(op.to_s)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_var_ref(var_ref)
|
||||||
|
var_ref[0] == :bt_quantity ? var_ref : raise(NotImplementedError)
|
||||||
|
end
|
||||||
|
|
||||||
|
silenced_events = [:lparen, :rparen, :op, :period, :sp, :int, :float,
|
||||||
|
:tstring_beg, :tstring_end]
|
||||||
|
(SCANNER_EVENTS - silenced_events).each do |event|
|
||||||
|
module_eval(<<-End, __FILE__, __LINE__ + 1)
|
||||||
|
def on_#{event}(token)
|
||||||
|
@disallowed[:token] << token + ' [#{event}]'
|
||||||
|
[:bt_unimplemented, token]
|
||||||
|
end
|
||||||
|
End
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_const(token)
|
||||||
|
@identifiers << token
|
||||||
|
[:bt_quantity, token]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_ident(token)
|
||||||
|
[:bt_ident, token]
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_kw(token)
|
||||||
|
@disallowed[:keyword] << token unless token == 'nil'
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_tstring_content(token)
|
||||||
|
token
|
||||||
|
end
|
||||||
|
|
||||||
|
def join_stmts(stmts)
|
||||||
|
stmts.map do |stmt|
|
||||||
|
ttype, token = stmt
|
||||||
|
case ttype
|
||||||
|
when :bt_expression
|
||||||
|
token
|
||||||
|
else
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end.join(';')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue
Block a user