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
|