Working on formula validation to accomodate functions, WIP
This commit is contained in:
parent
eba2aaa8aa
commit
841fcb2807
@ -26,11 +26,11 @@ 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 after: %{part}'
|
invalid_formula: 'interpretation failed: %{msg}'
|
||||||
disallowed_token: 'includes disallowed token "%{token}"
|
disallowed_token: 'includes disallowed token "%{token}"
|
||||||
(of type %{ttype}) at %{location}'
|
(of type %{ttype}) at %{location}'
|
||||||
disallowed_function_call: 'includes disallowed function call %{function}'
|
disallowed_function_call: 'includes disallowed function call: %{function}'
|
||||||
unknown_quantity: 'contains undefined quantities: %{quantities}'
|
unknown_quantity: 'contains undefined quantity: %{quantity}'
|
||||||
body_trackers:
|
body_trackers:
|
||||||
index:
|
index:
|
||||||
heading: 'Summary'
|
heading: 'Summary'
|
||||||
|
@ -2,6 +2,7 @@ module BodyTracking
|
|||||||
module Formula
|
module Formula
|
||||||
require 'ripper'
|
require 'ripper'
|
||||||
QUANTITY_TTYPES = [:on_ident, :on_tstring_content, :on_const]
|
QUANTITY_TTYPES = [:on_ident, :on_tstring_content, :on_const]
|
||||||
|
FUNCTIONS = ['abs', 'nil?']
|
||||||
|
|
||||||
class Formula
|
class Formula
|
||||||
def initialize(project, formula)
|
def initialize(project, formula)
|
||||||
@ -13,19 +14,35 @@ module BodyTracking
|
|||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# 1st: check if formula is valid Ruby code
|
# 1st: check if formula is valid Ruby code
|
||||||
tokenized_length = Ripper.tokenize(@formula).join.length
|
#tokenized_length = Ripper.tokenize(@formula).join.length
|
||||||
unless tokenized_length == @formula.length
|
#unless tokenized_length == @formula.length
|
||||||
errors << [:invalid_formula, {part: @formula[0...tokenized_length]}]
|
# errors << [:invalid_formula, {part: @formula[0...tokenized_length]}]
|
||||||
|
#end
|
||||||
|
begin
|
||||||
|
eval("-> { #{@formula} }")
|
||||||
|
rescue ScriptError => e
|
||||||
|
errors << [:invalid_formula, {msg: e.message}]
|
||||||
end
|
end
|
||||||
|
|
||||||
# 2nd: check if formula contains only allowed token types
|
# 2nd: check if formula contains only allowed token types
|
||||||
|
# 3rd: check for disallowed function calls (they are not detected by Ripper.lex)
|
||||||
|
# FIXME: this is unreliable (?) detection of function calls, should be replaced
|
||||||
|
# with parsing Ripper.sexp if necessary
|
||||||
identifiers = []
|
identifiers = []
|
||||||
|
disallowed_functions = Set.new
|
||||||
|
prev_ttype, prev_token = nil, nil
|
||||||
Ripper.lex(@formula).each do |location, ttype, token|
|
Ripper.lex(@formula).each do |location, ttype, token|
|
||||||
|
puts ttype, token
|
||||||
case
|
case
|
||||||
when QUANTITY_TTYPES.include?(ttype)
|
when QUANTITY_TTYPES.include?(prev_ttype) && ttype == :on_lparen
|
||||||
|
disallowed_functions << prev_token unless FUNCTIONS.include?(prev_token)
|
||||||
|
identifiers -= [prev_token]
|
||||||
|
when prev_ttype == :on_period && QUANTITY_TTYPES.include?(ttype)
|
||||||
|
disallowed_functions << token unless FUNCTIONS.include?(token)
|
||||||
|
when is_token_quantity?(ttype, token)
|
||||||
identifiers << token
|
identifiers << token
|
||||||
when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end,
|
when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end,
|
||||||
:on_lparen, :on_rparen].include?(ttype)
|
:on_lparen, :on_rparen, :on_period].include?(ttype)
|
||||||
when :on_op == ttype &&
|
when :on_op == ttype &&
|
||||||
['+', '-', '*', '/', '%', '**', '==', '!=', '>', '<', '>=', '<=', '<=>', '===',
|
['+', '-', '*', '/', '%', '**', '==', '!=', '>', '<', '>=', '<=', '<=>', '===',
|
||||||
'..', '...', '?:', 'and', 'or', 'not', '&&', '||', '!'].include?(token)
|
'..', '...', '?:', 'and', 'or', 'not', '&&', '||', '!'].include?(token)
|
||||||
@ -33,20 +50,14 @@ module BodyTracking
|
|||||||
else
|
else
|
||||||
errors << [:disallowed_token, {token: token, ttype: ttype, location: location}]
|
errors << [:disallowed_token, {token: token, ttype: ttype, location: location}]
|
||||||
end
|
end
|
||||||
|
prev_ttype, prev_token = ttype, token unless ttype == :on_sp
|
||||||
end
|
end
|
||||||
|
disallowed_functions.each { |f| errors << [:disallowed_function_call, {function: f}] }
|
||||||
# 3rd: check for disallowed function calls (they are not detected by Ripper.lex)
|
|
||||||
# FIXME: this is unreliable (?) detection of function calls, should be replaced
|
|
||||||
# with parsing Ripper.sexp if necessary
|
|
||||||
function = Ripper.slice(@formula, 'ident [sp]* lparen')
|
|
||||||
errors << [:disallowed_function_call, {function: function}] if function
|
|
||||||
|
|
||||||
# 4th: check if identifiers used in formula correspond to existing quantities
|
# 4th: check if identifiers used in formula correspond to existing quantities
|
||||||
identifiers.uniq!
|
identifiers.uniq!
|
||||||
quantities = @project.quantities.where(name: identifiers).pluck(:name)
|
quantities = @project.quantities.where(name: identifiers).pluck(:name)
|
||||||
if quantities.length != identifiers.length
|
(identifiers - quantities).each { |q| errors << [:unknown_quantity, {quantity: q}] }
|
||||||
errors << [:unknown_quantity, {quantities: identifiers - quantities}]
|
|
||||||
end
|
|
||||||
|
|
||||||
errors
|
errors
|
||||||
end
|
end
|
||||||
@ -57,27 +68,31 @@ module BodyTracking
|
|||||||
|
|
||||||
def get_quantities
|
def get_quantities
|
||||||
q_names = Ripper.lex(@formula).map do |*, ttype, token|
|
q_names = Ripper.lex(@formula).map do |*, ttype, token|
|
||||||
token if QUANTITY_TTYPES.include?(ttype)
|
token if is_token_quantity?(ttype, token)
|
||||||
end.compact
|
end.compact
|
||||||
@project.quantities.where(name: q_names).to_a
|
@project.quantities.where(name: q_names).to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate(inputs)
|
def calculate(inputs)
|
||||||
paramed_formula = Ripper.lex(@formula).map do |*, ttype, token|
|
paramed_formula = Ripper.lex(@formula).map do |*, ttype, token|
|
||||||
QUANTITY_TTYPES.include?(ttype) ? "params['#{token}'].to_d" : token
|
is_token_quantity?(ttype, token) ? "params['#{token}'].to_d" : token
|
||||||
end.join
|
end.join
|
||||||
|
|
||||||
inputs.map do |i, values|
|
inputs.map do |i, values|
|
||||||
begin
|
begin
|
||||||
[i, get_binding(values).eval(paramed_formula)]
|
[i, [get_binding(values).eval(paramed_formula), nil]]
|
||||||
rescue
|
rescue Exception
|
||||||
[i, nil]
|
[i, [nil, nil]]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def is_token_quantity?(ttype, token)
|
||||||
|
QUANTITY_TTYPES.include?(ttype) && !FUNCTIONS.include?(token)
|
||||||
|
end
|
||||||
|
|
||||||
def get_binding(params)
|
def get_binding(params)
|
||||||
binding
|
binding
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user