From fa77a691ce0cc8941fe470a762f352b27f4f0563 Mon Sep 17 00:00:00 2001 From: Marius Peter Date: Sun, 23 Nov 2025 17:54:45 +0100 Subject: Last commit. --- app/models/concerns/nutrient_vector.rb | 55 ++++++++++++++++++++++++++++ app/models/nutrient.rb | 4 ++ app/models/nutrient_measurement.rb | 12 +++--- app/models/target.rb | 67 ++++++++++++++++++++-------------- app/models/target_allocation.rb | 3 +- 5 files changed, 106 insertions(+), 35 deletions(-) create mode 100644 app/models/concerns/nutrient_vector.rb create mode 100644 app/models/nutrient.rb (limited to 'app/models') diff --git a/app/models/concerns/nutrient_vector.rb b/app/models/concerns/nutrient_vector.rb new file mode 100644 index 0000000..ba4f28a --- /dev/null +++ b/app/models/concerns/nutrient_vector.rb @@ -0,0 +1,55 @@ +module NutrientVector + extend ActiveSupport::Concern + + NUTRIENT_KEYS = %i[ + nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4 + ].freeze + + included do + # Define simple readers (nno3, p, k, ...) that read from #nutrient_values + NUTRIENT_KEYS.each do |k| + define_method(k) { nutrient_values[k] } + end + end + + # Hash-like access + def [](key) + key = key.to_sym + return nil unless NUTRIENT_KEYS.include?(key) + public_send(key) + end + + def keys = NUTRIENT_KEYS + + # Returns a copy to avoid accidental mutation + def to_h + NUTRIENT_KEYS.index_with { |k| public_send(k) } + end + + # Iterate over pairs + def each_pair + return enum_for(:each_pair) unless block_given? + NUTRIENT_KEYS.each { |k| yield k, public_send(k) } + end + + # Simple difference (self - other), useful for “how far from target?” + def delta_against(other) + NUTRIENT_KEYS.index_with { |k| (public_send(k).to_f) - (other.public_send(k).to_f) } + end + + # Percent difference relative to other (e.g., measurement vs target) + # Returns 0 when both are 0, and nil when target is 0 but measurement isn’t. + def percent_diff_against(other) + NUTRIENT_KEYS.index_with do |k| + a = public_send(k).to_f + b = other.public_send(k).to_f + if b.zero? && a.zero? + 0.0 + elsif b.zero? + nil + else + ((a - b) / b) * 100.0 + end + end + end +end diff --git a/app/models/nutrient.rb b/app/models/nutrient.rb new file mode 100644 index 0000000..0840086 --- /dev/null +++ b/app/models/nutrient.rb @@ -0,0 +1,4 @@ +class Nutrient < ApplicationRecord + validates :formula, presence: true, uniqueness: true + validates :name, presence: true +end diff --git a/app/models/nutrient_measurement.rb b/app/models/nutrient_measurement.rb index 1139af7..b56c5ed 100644 --- a/app/models/nutrient_measurement.rb +++ b/app/models/nutrient_measurement.rb @@ -1,7 +1,5 @@ class NutrientMeasurement < ApplicationRecord - NUTRIENT_FIELDS = %i[ - nno3 p k ca mg s na cl si fe zn b mn cu mo nnh4 - ].freeze + include NutrientVector validates :measured_on, presence: true validates :measured_on, uniqueness: true @@ -12,7 +10,11 @@ class NutrientMeasurement < ApplicationRecord end end - def self.nutrient_fields - NUTRIENT_FIELDS + private + + def nutrient_values + @nutrient_values ||= NutrientVector::NUTRIENT_KEYS.index_with do |k| + read_attribute(k).to_f + end end end diff --git a/app/models/target.rb b/app/models/target.rb index 9211e74..2efd3b2 100644 --- a/app/models/target.rb +++ b/app/models/target.rb @@ -1,42 +1,53 @@ -# app/models/target.rb class Target < ApplicationRecord - has_many :target_allocations, dependent: :destroy - has_many :nutrient_profiles, through: :target_allocations + include NutrientVector - accepts_nested_attributes_for :target_allocations, allow_destroy: true - # validate :percentages_sum_to_100 + has_many :target_allocations, inverse_of: :target, dependent: :destroy - def weighted_requirements - totals = Hash.new(0.0) - denom = 100.0 + after_initialize :set_defaults, if: :new_record? - target_allocations.includes(:nutrient_profile).each do |alloc| - profile = alloc.nutrient_profile - next unless profile + validate :allocations_sum_to_100 - weight = (alloc.percentage || 0).to_f / denom - next if weight <= 0 + # Recompute when allocations change; keep it simple (memoized per instance) + def recompute_nutrients! + @nutrient_values = nil + nutrient_values + end - # Prefer the helper, but gracefully fall back to slicing attributes. - reqs = if profile.respond_to?(:requirements_hash) - profile.requirements_hash - else - profile.attributes.slice(*NutrientProfile::NUTRIENT_KEYS.map(&:to_s)) - end + private + + def nutrient_values + @nutrient_values ||= get_nutrient_values + end + + def get_nutrient_values + sums = Hash.new(0.0) + allocs = target_allocations.includes(:nutrient_profile) - reqs.each do |nutrient_key, value| - next if value.nil? - totals[nutrient_key.to_s] += value.to_f * weight + allocs.each do |alloc| + weight = alloc.percentage.to_f / 100.0 + profile = alloc.nutrient_profile + NutrientVector::NUTRIENT_KEYS.each do |k| + sums[k] += profile.public_send(k).to_f * weight end end - totals + # Ensure all keys exist, even when there are no allocations + NutrientVector::NUTRIENT_KEYS.each { |k| sums[k] ||= 0.0 } + sums.freeze end - private + def set_defaults + self.name ||= "Cible #{Date.today + 1.month}" + if target_allocations.empty? + nutrient_profiles = NutrientProfile.limit(3) + nutrient_profiles.each do |profile| + target_allocations.build(nutrient_profile: profile, percentage: 0) + end + end + end - # def percentages_sum_to_100 - # sum = target_allocations.sum { |a| a.percentage.to_f } - # errors.add(:base, "La somme des pourcentages doit être égale à 100%") unless (sum - 100.0).abs <= 0.1 - # end + def allocations_sum_to_100 + sum = target_allocations.reject(&:marked_for_destruction?).sum { |a| a.percentage.to_f } + errors.add(:base, "La somme des pourcentages doit être 100%") unless (sum - 100).abs < 0.01 + end end diff --git a/app/models/target_allocation.rb b/app/models/target_allocation.rb index 6aa1dcb..5e971a0 100644 --- a/app/models/target_allocation.rb +++ b/app/models/target_allocation.rb @@ -1,7 +1,6 @@ class TargetAllocation < ApplicationRecord - belongs_to :target + belongs_to :target, inverse_of: :target_allocations, touch: true belongs_to :nutrient_profile validates :percentage, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 } - validates :nutrient_profile_id, uniqueness: { scope: :target_id } end -- cgit v1.2.3