diff options
author | Marius Peter <marius.peter@tutanota.com> | 2025-08-24 20:29:54 +0200 |
---|---|---|
committer | Marius Peter <marius.peter@tutanota.com> | 2025-08-24 20:29:54 +0200 |
commit | 52b044d6a4278c229992404ad5801769c2d13363 (patch) | |
tree | b30b34da58f26117c035391d09366b190350b1e3 /app/services/fertilizer_recipe_calculator.rb |
First commit.
Vive le Castel Peter !
Diffstat (limited to 'app/services/fertilizer_recipe_calculator.rb')
-rw-r--r-- | app/services/fertilizer_recipe_calculator.rb | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/app/services/fertilizer_recipe_calculator.rb b/app/services/fertilizer_recipe_calculator.rb new file mode 100644 index 0000000..28ba8aa --- /dev/null +++ b/app/services/fertilizer_recipe_calculator.rb @@ -0,0 +1,136 @@ +class FertilizerRecipeCalculator + NUTRIENTS = %i[nno3 p k ca mg s nnh4].freeze + + def self.call(latest, target, water_volume_l: 100_000) + new(latest, target, water_volume_l).call + end + + def initialize(latest, target, water_volume_l) + @latest = latest || NutrientMeasurement.new + @target = target || NutrientMeasurement.new + @vol_l = water_volume_l.to_f + end + + # Returns a Hash { FertilizerComponent => qty_kg } + def call + deltas_mg_per_l = NUTRIENTS.index_with do |k| + [ (value(@target, k) - value(@latest, k)), 0 ].max + end + + # mg/L * L = mg -> kg + needed_kg = deltas_mg_per_l.transform_values { |mg_l| mg_l * @vol_l / 1_000_000.0 } + + recipe = Hash.new(0.0) + + # 1) P via MAP or DAP (accounts NH4-N) + if (p_need = needed_kg[:p]) > 0 + map = fc_by_formula("MAP") || fc_by_name_like("monoammonium phosphate") + dap = fc_by_formula("DAP") || fc_by_name_like("diammonium phosphate") + carrier = [ map, dap ].compact.find { |c| positive?(c.p) } + if carrier + kg = kg_for(p_need, carrier.p) + recipe[carrier] += kg + needed_kg[:nnh4] = [ needed_kg[:nnh4] - kg * pct(carrier.nnh4), 0 ].max if positive?(carrier.nnh4) + needed_kg[:p] = 0 + end + end + + # 2) NO3-N via KNO3 then Ca(NO3)2 (accounts K/Ca) + if (n_need = needed_kg[:nno3]) > 0 + kno3 = fc_by_formula("KNO3") || fc_by_name_like("potassium nitrate") + can = fc_by_formula("Ca(NO3)2") || fc_by_name_like("calcium nitrate") + + if kno3&.nno3.to_f > 0 + kg = kg_for(n_need, kno3.nno3) + recipe[kno3] += kg + needed_kg[:k] = [ needed_kg[:k] - kg * pct(kno3.k), 0 ].max + n_need -= kg * pct(kno3.nno3) + end + + if n_need > 0 && can&.nno3.to_f > 0 + kg = kg_for(n_need, can.nno3) + recipe[can] += kg + needed_kg[:ca] = [ needed_kg[:ca] - kg * pct(can.ca), 0 ].max + n_need = 0 + end + + needed_kg[:nno3] = [ n_need, 0 ].max + end + + # 3) K via K2SO4 (accounts S) + if (k_need = needed_kg[:k]) > 0 + sop = fc_by_formula("K2SO4") || fc_by_name_like("potassium sulfate") + if sop&.k.to_f > 0 + kg = kg_for(k_need, sop.k) + recipe[sop] += kg + needed_kg[:s] = [ needed_kg[:s] - kg * pct(sop.s), 0 ].max + needed_kg[:k] = 0 + end + end + + # 4) Ca via Ca(NO3)2 (accounts NO3-N) + if (ca_need = needed_kg[:ca]) > 0 + can = recipe.keys.find { |c| norm_formula(c) == "ca(no3)2" } || + fc_by_formula("Ca(NO3)2") || fc_by_name_like("calcium nitrate") + if can&.ca.to_f > 0 + kg = kg_for(ca_need, can.ca) + recipe[can] += kg + needed_kg[:nno3] = [ needed_kg[:nno3] - kg * pct(can.nno3), 0 ].max + needed_kg[:ca] = 0 + end + end + + # 5) Mg via MgSO4 (accounts S) + if (mg_need = needed_kg[:mg]) > 0 + mgs = fc_by_formula("MgSO4") || fc_by_name_like("magnesium sulfate") + if mgs&.mg.to_f > 0 + kg = kg_for(mg_need, mgs.mg) + recipe[mgs] += kg + needed_kg[:s] = [ needed_kg[:s] - kg * pct(mgs.s), 0 ].max + needed_kg[:mg] = 0 + end + end + + # 6) S via K2SO4 or MgSO4 (whichever we already used or find) + if (s_need = needed_kg[:s]) > 0 + sop = recipe.keys.find { |c| norm_formula(c) == "k2so4" } || + fc_by_formula("K2SO4") || fc_by_name_like("potassium sulfate") + mgs = recipe.keys.find { |c| norm_formula(c) == "mgso4" } || + fc_by_formula("MgSO4") || fc_by_name_like("magnesium sulfate") + carrier = [ sop, mgs ].compact.find { |c| positive?(c.s) } + if carrier + kg = kg_for(s_need, carrier.s) + recipe[carrier] += kg + needed_kg[:k] = [ needed_kg[:k] - kg * pct(carrier.k), 0 ].max if positive?(carrier.k) + needed_kg[:mg] = [ needed_kg[:mg] - kg * pct(carrier.mg), 0 ].max if positive?(carrier.mg) + needed_kg[:s] = 0 + end + end + + recipe.delete_if { |_c, kg| kg < 0.01 } + recipe + end + + private + + def value(obj, key) = obj.public_send(key).to_f + + def fc_by_formula(formula) + FertilizerComponent.where("LOWER(formula) = ?", formula.to_s.downcase).first + end + + def fc_by_name_like(name) + FertilizerComponent.where("LOWER(name) LIKE ?", "%#{name.downcase}%").first + end + + def kg_for(need_kg_element, percent_in_product) + return 0.0 unless positive?(percent_in_product) + need_kg_element / pct(percent_in_product) + end + + def pct(v) = v.to_f / 100.0 + + def positive?(v) = v.to_f > 0.0 + + def norm_formula(c) = c.formula.to_s.downcase.gsub(/\s+/, "") +end |