1use crate::{convert::Converter, quantity::Value, Quantity, Recipe};
4use thiserror::Error;
5
6#[derive(Debug, Error, serde::Serialize, serde::Deserialize)]
8#[cfg_attr(feature = "ts", derive(tsify::Tsify))]
9pub enum ScaleError {
10 #[error("Cannot scale recipe: servings metadata is not a valid number")]
12 InvalidServings,
13
14 #[error("Cannot scale recipe: yield metadata is missing or invalid")]
16 InvalidYield,
17
18 #[error("Cannot scale recipe: unit mismatch (expected {expected}, got {got})")]
20 UnitMismatch { expected: String, got: String },
21}
22
23impl Recipe {
24 pub fn scale(&mut self, factor: f64, converter: &Converter) {
27 let scale_quantity = |q: &mut Quantity| {
28 if q.scalable {
29 q.value.scale(factor);
30 let _ = q.fit(converter);
31 }
32 };
33
34 if let Some(current_servings) = self.metadata.servings() {
36 if let Some(base) = current_servings.as_number() {
37 let new_servings = (base as f64 * factor).round() as u32;
38 if let Some(servings_value) =
39 self.metadata.get_mut(crate::metadata::StdKey::Servings)
40 {
41 match servings_value {
43 serde_yaml::Value::String(_) => {
44 *servings_value = serde_yaml::Value::String(new_servings.to_string());
45 }
46 _ => {
47 *servings_value =
48 serde_yaml::Value::Number(serde_yaml::Number::from(new_servings));
49 }
50 }
51 }
52 }
53 }
54
55 self.ingredients
56 .iter_mut()
57 .filter_map(|i| i.quantity.as_mut())
58 .for_each(scale_quantity);
59 self.cookware
60 .iter_mut()
61 .filter_map(|i| i.quantity.as_mut())
62 .for_each(scale_quantity);
63 self.timers
64 .iter_mut()
65 .filter_map(|i| i.quantity.as_mut())
66 .for_each(scale_quantity);
67 }
68
69 pub fn scale_to_servings(
75 &mut self,
76 target: u32,
77 converter: &Converter,
78 ) -> Result<(), ScaleError> {
79 let current_servings = self
80 .metadata
81 .servings()
82 .ok_or(ScaleError::InvalidServings)?;
83
84 let base = current_servings
85 .as_number()
86 .ok_or(ScaleError::InvalidServings)?;
87
88 let factor = target as f64 / base as f64;
89 self.scale(factor, converter);
90
91 if let Some(servings_value) = self.metadata.get_mut(crate::metadata::StdKey::Servings) {
93 match servings_value {
95 serde_yaml::Value::String(_) => {
96 *servings_value = serde_yaml::Value::String(target.to_string());
97 }
98 _ => {
99 *servings_value = serde_yaml::Value::Number(serde_yaml::Number::from(target));
100 }
101 }
102 }
103 Ok(())
104 }
105
106 pub fn scale_to_target(
122 &mut self,
123 target_value: f64,
124 target_unit: Option<&str>,
125 converter: &Converter,
126 ) -> Result<(), ScaleError> {
127 match target_unit {
128 Some("servings") | Some("serving") => {
129 let servings = target_value.round() as u32;
131 self.scale_to_servings(servings, converter)
132 }
133 Some(unit) => {
134 self.scale_to_yield(target_value, unit, converter)
136 }
137 None => {
138 self.scale(target_value, converter);
140 Ok(())
141 }
142 }
143 }
144
145 pub fn scale_to_yield(
155 &mut self,
156 target_value: f64,
157 target_unit: &str,
158 converter: &Converter,
159 ) -> Result<(), ScaleError> {
160 let yield_value = self.metadata.get("yield").ok_or(ScaleError::InvalidYield)?;
163
164 let yield_str = yield_value
165 .as_str()
166 .ok_or(ScaleError::InvalidYield)?
167 .to_string(); let parts: Vec<&str> = yield_str.split('%').collect();
171 if parts.len() != 2 {
172 return Err(ScaleError::InvalidYield);
173 }
174 let current_value = parts[0]
175 .parse::<f64>()
176 .map_err(|_| ScaleError::InvalidYield)?;
177 let current_unit = parts[1].to_string();
178
179 if current_unit != target_unit {
181 return Err(ScaleError::UnitMismatch {
182 expected: target_unit.to_string(),
183 got: current_unit.to_string(),
184 });
185 }
186
187 let factor = target_value / current_value;
188 self.scale(factor, converter);
189
190 if let Some(yield_meta) = self.metadata.get_mut("yield") {
193 *yield_meta = serde_yaml::Value::String(format!("{target_value}%{target_unit}"));
194 }
195
196 Ok(())
197 }
198}
199
200impl Value {
201 fn scale(&mut self, factor: f64) {
202 match self {
203 Value::Number(n) => {
204 *n = (n.value() * factor).into();
205 }
206 Value::Range { start, end } => {
207 *start = (start.value() * factor).into();
208 *end = (end.value() * factor).into();
209 }
210 Value::Text(_) => {}
211 }
212 }
213}