diff --git a/app/models/formula.rb b/app/models/formula.rb
new file mode 100644
index 0000000..8871e15
--- /dev/null
+++ b/app/models/formula.rb
@@ -0,0 +1,58 @@
+class Formula < ActiveRecord::Base
+ include BodyTracking::FormulaBuilder
+
+ attr_reader :parts, :quantities
+
+ belongs_to :quantity, inverse_of: :formula, required: true
+
+ validates :code, presence: true
+ validate do
+ parse.each { |message, params| errors.add(:code, message, params) }
+ end
+
+ after_initialize do
+ if new_record?
+ self.zero_nil = true if self.zero_nil.nil?
+ end
+ end
+
+ private
+
+ def parse
+ parser = FormulaBuilder.new(self.code)
+ identifiers, parts = parser.parse
+ errors = parser.errors
+
+ quantities = Quantity.where(project: self.quantity.project, name: identifiers)
+ quantities_names = quantities.pluck(:name)
+ (identifiers - quantities_names).each do |q|
+ errors << [:unknown_quantity, {quantity: q}]
+ end
+
+ @parts, @quantities = parts, quantities.to_a if errors.empty?
+ errors
+ end
+
+ def calculate(inputs)
+ quantities = inputs.map { |q, v| [q.name, v.transpose[0]] }.to_h
+ length = quantities.values.first.length
+
+ raise(InvalidFormula, 'Invalid formula') unless self.valid?
+ raise InvalidInputs unless quantities.values.all? { |v| v.length == length }
+
+ args = []
+ @parts.each do |p|
+ code = p[:type] == :indexed ?
+ "length.times.map { |index| #{p[:content]} }" : p[:content]
+ args << get_binding(quantities, args, length).eval(code)
+ end
+ args.last.map { |v| [v, nil] }
+ rescue Exception => e
+ puts e.message
+ [[nil, nil]] * length
+ end
+
+ def get_binding(quantities, args, length)
+ binding
+ end
+end
diff --git a/app/models/quantity.rb b/app/models/quantity.rb
index 5e558b8..e01556a 100644
--- a/app/models/quantity.rb
+++ b/app/models/quantity.rb
@@ -1,6 +1,4 @@
class Quantity < ActiveRecord::Base
- include BodyTracking::Formula
-
enum domain: {
diet: 0,
measurement: 1,
@@ -18,12 +16,16 @@ class Quantity < ActiveRecord::Base
has_and_belongs_to_many :column_views
has_many :readouts
+ has_one :formula, inverse_of: :quantity, dependent: :destroy, validate: true
+ accepts_nested_attributes_for :formula, allow_destroy: true, reject_if: proc { |attrs|
+ attrs['code'].blank?
+ }
+
validates :name, presence: true, uniqueness: {scope: :project_id}
validates :domain, inclusion: {in: domains.keys}
validate if: -> { parent.present? } do
errors.add(:parent, :parent_domain_mismatch) unless domain == parent.domain
end
- validates :formula, formula: {allow_nil: true}
after_initialize do
if new_record?
@@ -31,8 +33,6 @@ class Quantity < ActiveRecord::Base
end
end
- delegate :valid?, :quantities, :calculate, to: :f_obj, prefix: :formula, allow_nil: true
-
def movable?(direction)
case direction
when :up
@@ -58,10 +58,4 @@ class Quantity < ActiveRecord::Base
quantities
end
-
- private
-
- def f_obj
- Formula.new(self.project, self.formula) if self.formula?
- end
end
diff --git a/app/views/quantities/_index.html.erb b/app/views/quantities/_index.html.erb
index 8cd2434..e345f54 100644
--- a/app/views/quantities/_index.html.erb
+++ b/app/views/quantities/_index.html.erb
@@ -43,7 +43,7 @@
<%= q.domain %> |
<%= q.description %> |
- <%= checked_image q.formula? %> |
+ <%= checked_image q.formula %> |
<%= link_to l(:button_edit), edit_quantity_path(q), {
remote: true,
diff --git a/db/migrate/001_create_schema.rb b/db/migrate/001_create_schema.rb
index 11f94fa..07dbe69 100644
--- a/db/migrate/001_create_schema.rb
+++ b/db/migrate/001_create_schema.rb
@@ -11,7 +11,6 @@ class CreateSchema < ActiveRecord::Migration
t.references :project
t.integer :domain
t.string :name
- t.string :formula
t.string :description
# fields for awesome_nested_set
t.references :parent
@@ -20,6 +19,13 @@ class CreateSchema < ActiveRecord::Migration
t.timestamps null: false
end
+ create_table :formulas do |t|
+ t.references :quantity
+ t.string :code
+ t.boolean :zero_nil
+ t.timestamps null: false
+ end
+
create_table :column_views do |t|
t.references :project
t.string :name
diff --git a/db/migrate/002_load_defaults.rb b/db/migrate/002_load_defaults.rb
index b140860..2de9ea2 100644
--- a/db/migrate/002_load_defaults.rb
+++ b/db/migrate/002_load_defaults.rb
@@ -138,7 +138,7 @@ class LoadDefaults < ActiveRecord::Migration
# Calculated quantities go at the and to make sure dependencies exist
e2 = Quantity.create project: nil, domain: :diet, parent: e1, name: "Calculated",
description: "Total energy calculated from macronutrients",
- formula: "4*(Proteins || 0) + 9*(Fats || 0) + 4*(Carbohydrates || 0)"
+ formula_attributes: {code: "4*Proteins + 9*Fats + 4*Carbohydrates", zero_nil: true}
Source.create project: nil, name: "nutrition label",
description: "nutrition facts taken from package nutrition label"
diff --git a/lib/body_tracking/formula.rb b/lib/body_tracking/formula_builder.rb
similarity index 76%
rename from lib/body_tracking/formula.rb
rename to lib/body_tracking/formula_builder.rb
index de3e702..8ff3832 100644
--- a/lib/body_tracking/formula.rb
+++ b/lib/body_tracking/formula_builder.rb
@@ -1,84 +1,11 @@
module BodyTracking
- module Formula
+ module FormulaBuilder
require 'ripper'
require 'set'
class InvalidFormula < RuntimeError; end
class InvalidInputs < RuntimeError; end
- class Formula
- def initialize(project, formula)
- @project = project
- @formula = formula
- @parts = nil
- @quantities = nil
- end
-
- def validate
- parser = FormulaBuilder.new(@formula)
- identifiers, parts = parser.parse
- errors = parser.errors
-
- quantities = Quantity.where(project: @project, name: identifiers)
- quantities_names = quantities.pluck(:name)
- (identifiers - quantities_names).each do |q|
- errors << [:unknown_quantity, {quantity: q}]
- end
-
- @parts, @quantities = parts, quantities if errors.empty?
- errors
- end
-
- def valid?
- @quantities || self.validate.empty?
- end
-
- def quantities
- raise(InvalidFormula, 'Invalid formula') unless self.valid?
-
- @quantities.to_a
- end
-
- def calculate(inputs)
- quantities = inputs.map { |q, v| [q.name, v.transpose[0]] }.to_h
- length = quantities.values.first.length
-
- raise(InvalidFormula, 'Invalid formula') unless self.valid?
- raise InvalidInputs unless quantities.values.all? { |v| v.length == length }
-
- args = []
- @parts.each do |p|
- code = p[:type] == :indexed ?
- "length.times.map { |index| #{p[:content]} }" : p[:content]
- args << get_binding(quantities, args, length).eval(code)
- end
- args.last.map { |v| [v, nil] }
- rescue Exception => e
- puts e.message
- [[nil, nil]] * length
- end
-
- private
-
- def get_binding(quantities, args, length)
- binding
- end
- end
-
-
- class FormulaValidator < ActiveModel::EachValidator
- def initialize(options)
- super(options)
- end
-
- def validate_each(record, attribute, value)
- Formula.new(record.project, value).validate.each do |message, params|
- record.errors.add(attribute, message, params)
- end
- end
- end
-
-
# List of events with parameter count:
# https://github.com/racker/ruby-1.9.3-lucid/blob/master/ext/ripper/eventids1.c
class FormulaBuilder < Ripper::SexpBuilder
|