From 4e537f398176ec7d878e2fe38be6c0584fbeb1a0 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 16 Feb 2020 18:34:07 +0100 Subject: [PATCH] Moved Formula to separate model --- app/models/formula.rb | 58 ++++++++++++++ app/models/quantity.rb | 16 ++-- app/views/quantities/_index.html.erb | 2 +- db/migrate/001_create_schema.rb | 8 +- db/migrate/002_load_defaults.rb | 2 +- .../{formula.rb => formula_builder.rb} | 75 +------------------ 6 files changed, 73 insertions(+), 88 deletions(-) create mode 100644 app/models/formula.rb rename lib/body_tracking/{formula.rb => formula_builder.rb} (76%) 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