summaryrefslogtreecommitdiff
path: root/app/services/fertilizer_recipe_calculator.rb
blob: 28ba8aa781ba2688203791f4dda1e9214f509cd2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
Copyright 2019--2025 Marius PETER