1use std::{fmt, num::IntErrorKind, ops::Not};
2
3use serde::Serialize;
4
5use super::{
6 errors::{ParseError, ParseResult},
7 ParseFromStr,
8};
9
10#[derive(Clone, Copy, Debug, Serialize)]
11pub struct Integer(u32);
12
13impl Integer {
14 fn try_parse_from_str(s: &str) -> ParseResult<Option<Self>> {
15 match s.parse() {
16 Ok(int) => Ok(Some(Self(int))),
17 Err(error) if *error.kind() != IntErrorKind::PosOverflow => Ok(None),
18 Err(_) => Err("integer is out of range".into()),
19 }
20 }
21}
22
23#[derive(Clone, Copy, Debug, Serialize)]
24pub struct Decimal {
25 int: u16,
26 frac: u16,
27}
28
29impl Decimal {
30 fn try_parse_from_str(s: &str) -> ParseResult<Option<Self>> {
31 let Some((int, frac)) = s.split_once('.') else {
32 return Ok(None);
33 };
34 match (int.parse(), frac.parse()) {
35 (Ok(int), Ok(frac)) => Ok(Some(Self { int, frac })),
36 (Err(error), _) if *error.kind() != IntErrorKind::PosOverflow => Ok(None),
37 (_, Err(error)) if *error.kind() != IntErrorKind::PosOverflow => Ok(None),
38 (Err(_), _) => Err("decimal integral part is out of range".into()),
39 (_, Err(_)) => Err("decimal fractional part is out of range".into()),
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug, Serialize)]
45pub struct Fraction {
46 numer: u8,
47 denom: u8,
48}
49
50impl Fraction {
51 fn add_integer(self, int: u8) -> Option<Self> {
52 Some(Self {
53 numer: int
54 .checked_mul(self.denom)
55 .and_then(|product| product.checked_add(self.numer))?,
56 denom: self.denom,
57 })
58 }
59
60 fn try_parse_from_str(s: &str) -> ParseResult<Option<Self>> {
61 let Some((numer, denom)) = s.split_once('/') else {
62 return Ok(None);
63 };
64 match (numer.parse(), denom.parse()) {
65 (Ok(numer), Ok(denom)) => Ok(Some(Self { numer, denom })),
66 (Err(error), _) if *error.kind() != IntErrorKind::PosOverflow => Ok(None),
67 (_, Err(error)) if *error.kind() != IntErrorKind::PosOverflow => Ok(None),
68 (Err(_), _) => Err("fraction numerator is out of range".into()),
69 (_, Err(_)) => Err("fraction denominator is out of range".into()),
70 }
71 }
72}
73
74#[derive(Debug, Serialize)]
75#[serde(rename_all = "snake_case")]
76pub enum QuantityValue {
77 Integer(Integer),
78 Decimal(Decimal),
79 Fraction(Fraction),
80}
81
82impl QuantityValue {
83 pub fn parse_from_str(value: &str) -> ParseResult<(Self, &str)> {
84 let (value, rest) = value.split_once(' ').unwrap_or((value, ""));
85 if let Some(integer) = Integer::try_parse_from_str(value)? {
86 match Self::parse_mixed_number(integer, rest)? {
87 Some((fraction, rest)) => Ok((Self::Fraction(fraction), rest)),
88 None => Ok((Self::Integer(integer), rest)),
89 }
90 } else if let Some(decimal) = Decimal::try_parse_from_str(value)? {
91 Ok((Self::Decimal(decimal), rest))
92 } else if let Some(fraction) = Fraction::try_parse_from_str(value)? {
93 Ok((Self::Fraction(fraction), rest))
94 } else {
95 Err(format!("invalid ingredient quantity value: '{value}'").into())
96 }
97 }
98
99 fn parse_mixed_number(
100 Integer(int): Integer,
101 value: &str,
102 ) -> ParseResult<Option<(Fraction, &str)>> {
103 let (value, rest) = value.split_once(' ').unwrap_or((value, ""));
104 let Some(fraction) = Fraction::try_parse_from_str(value)? else {
105 return Ok(None);
106 };
107 let fraction = int
108 .try_into()
109 .ok()
110 .and_then(|int| fraction.add_integer(int))
111 .ok_or_else(|| format!("mixed number fraction '{value}' is out of range"))?;
112 Ok(Some((fraction, rest)))
113 }
114}
115
116#[derive(Debug, Serialize)]
117pub struct Quantity {
118 pub value: QuantityValue,
119 pub unit: Option<String>,
120 pub note: Option<String>,
121}
122
123impl fmt::Display for QuantityValue {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 Self::Integer(Integer(value)) => {
127 write!(f, "{value}")
128 }
129 Self::Decimal(Decimal { int, frac }) => {
130 write!(f, "{int}.{frac}")
131 }
132 Self::Fraction(Fraction { numer, denom }) => {
133 write!(f, "{numer}/{denom}")
134 }
135 }
136 }
137}
138
139impl fmt::Display for Quantity {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(f, "{}", self.value)?;
142 if let Some(unit) = &self.unit {
143 write!(f, " {unit}")?;
144 }
145 if let Some(note) = &self.note {
146 write!(f, " ({note})")?;
147 }
148 Ok(())
149 }
150}
151
152impl ParseFromStr for Quantity {
153 fn parse_from_str(s: &str) -> ParseResult<Self> {
154 let (value, rest) = QuantityValue::parse_from_str(s)?;
155 let (unit, note) = if let Some((unit, note)) = rest
156 .split_once(" (")
157 .or_else(|| rest.strip_prefix('(').map(|note| ("", note)))
158 {
159 let unit = unit.trim().to_string();
160 let note = note
161 .strip_suffix(')')
162 .ok_or("missing closing parenthesis of quantity note")?
163 .trim();
164 (unit, note.is_empty().not().then(|| note.into()))
165 } else {
166 (rest.trim_start().into(), None)
167 };
168 let unit = unit.is_empty().not().then_some(unit);
169 Ok(Self { value, unit, note })
170 }
171}
172
173#[derive(Debug, Serialize)]
174pub struct Ingredient {
175 pub name: String,
176 pub kind: Option<String>,
177 pub quantity: Option<Quantity>,
178}
179
180impl fmt::Display for Ingredient {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "{}", self.name)?;
183 if let Some(kind) = &self.kind {
184 write!(f, ", {kind}")?;
185 }
186 if let Some(quantity) = &self.quantity {
187 write!(f, ": {quantity}")?;
188 }
189 Ok(())
190 }
191}
192
193impl ParseFromStr for Ingredient {
194 fn parse_from_str(s: &str) -> ParseResult<Self> {
195 let (name, quantity) = s.split_once(": ").map_or((s, ""), |(name, quantity)| {
196 (name.trim_end(), quantity.trim_start())
197 });
198 let (name, kind) = name.split_once(", ").map_or((name, ""), |(name, kind)| {
199 (name.trim_end(), kind.trim_start())
200 });
201 if name.is_empty() {
202 return Err(ParseError::empty("ingredient name"));
203 }
204 let kind = kind.is_empty().not().then(|| kind.into());
205 let quantity = if quantity.is_empty() {
206 None
207 } else {
208 Some(Quantity::parse_from_str(quantity)?)
209 };
210 Ok(Self {
211 name: name.into(),
212 kind,
213 quantity,
214 })
215 }
216}
217
218#[cfg(test)]
219mod tests {
220
221 use super::*;
222
223 #[test]
224 fn test_display_quantity() {
225 let mut quantity = Quantity {
226 value: QuantityValue::Decimal(Decimal { int: 0, frac: 5 }),
227 unit: None,
228 note: None,
229 };
230 assert_eq!(quantity.to_string(), "0.5");
231 quantity.value = QuantityValue::Fraction(Fraction { numer: 1, denom: 2 });
232 assert_eq!(quantity.to_string(), "1/2");
233 quantity.value = QuantityValue::Integer(Integer(1));
234 assert_eq!(quantity.to_string(), "1");
235 quantity.unit = Some("unit".into());
236 assert_eq!(quantity.to_string(), "1 unit");
237 quantity.note = Some("note".into());
238 assert_eq!(quantity.to_string(), "1 unit (note)");
239 quantity.unit = None;
240 assert_eq!(quantity.to_string(), "1 (note)");
241 }
242
243 #[test]
244 fn test_parse_quantity() {
245 let quantity = Quantity::parse_from_str("1").unwrap();
246 assert!(matches!(quantity.value, QuantityValue::Integer(Integer(1))));
247 assert_eq!(quantity.unit, None);
248 assert_eq!(quantity.note, None);
249 let quantity = Quantity::parse_from_str("0.5").unwrap();
250 assert!(matches!(
251 quantity.value,
252 QuantityValue::Decimal(Decimal { int: 0, frac: 5 })
253 ));
254 let quantity = Quantity::parse_from_str("1/2").unwrap();
255 assert!(matches!(
256 quantity.value,
257 QuantityValue::Fraction(Fraction { numer: 1, denom: 2 })
258 ));
259 let quantity = Quantity::parse_from_str("1 a unit").unwrap();
260 assert!(matches!(quantity.value, QuantityValue::Integer(Integer(1))));
261 assert_eq!(quantity.unit, Some("a unit".into()));
262 assert_eq!(quantity.note, None);
263 let quantity = Quantity::parse_from_str("1 ( a note )").unwrap();
264 assert!(matches!(quantity.value, QuantityValue::Integer(Integer(1))));
265 assert_eq!(quantity.unit, None);
266 assert_eq!(quantity.note, Some("a note".into()));
267 let quantity = Quantity::parse_from_str("10 1 unit ( a note )").unwrap();
268 assert!(matches!(
269 quantity.value,
270 QuantityValue::Integer(Integer(10))
271 ));
272 assert_eq!(quantity.unit, Some("1 unit".into()));
273 assert_eq!(quantity.note, Some("a note".into()));
274 }
275
276 #[test]
277 fn test_display_ingredient() {
278 let quantity = Quantity {
279 value: QuantityValue::Integer(Integer(1)),
280 unit: None,
281 note: None,
282 };
283 let mut ingredient = Ingredient {
284 name: "name".into(),
285 kind: None,
286 quantity: None,
287 };
288 assert_eq!(ingredient.to_string(), "name");
289 ingredient.kind = Some("kind".into());
290 assert_eq!(ingredient.to_string(), "name, kind");
291 ingredient.quantity = Some(quantity);
292 assert_eq!(ingredient.to_string(), "name, kind: 1");
293 ingredient.kind = None;
294 assert_eq!(ingredient.to_string(), "name: 1");
295 }
296
297 #[test]
298 fn test_parse_ingredient() {
299 let ingredient = Ingredient::parse_from_str("a name").unwrap();
300 assert_eq!(ingredient.name, "a name");
301 assert_eq!(ingredient.kind, None);
302 assert!(ingredient.quantity.is_none());
303 let ingredient = Ingredient::parse_from_str("a name , a kind").unwrap();
304 assert_eq!(ingredient.name, "a name");
305 assert_eq!(ingredient.kind, Some("a kind".into()));
306 assert!(ingredient.quantity.is_none());
307 let ingredient = Ingredient::parse_from_str("a name : 1 unit (note)").unwrap();
308 assert_eq!(ingredient.name, "a name");
309 assert_eq!(ingredient.kind, None);
310 assert_eq!(ingredient.quantity.unwrap().to_string(), "1 unit (note)");
311 let ingredient = Ingredient::parse_from_str("a name , a kind : 1 unit (note)").unwrap();
312 assert_eq!(ingredient.name, "a name");
313 assert_eq!(ingredient.kind, Some("a kind".into()));
314 let quantity = ingredient.quantity.unwrap();
315 assert_eq!(quantity.value.to_string(), "1");
316 assert_eq!(quantity.unit.unwrap(), "unit");
317 assert_eq!(quantity.note.unwrap(), "note");
318 let ingredient = Ingredient::parse_from_str("name: 0.5 (note)").unwrap();
319 assert_eq!(ingredient.name, "name");
320 assert_eq!(ingredient.kind, None);
321 let quantity = ingredient.quantity.unwrap();
322 assert_eq!(quantity.value.to_string(), "0.5");
323 assert!(quantity.unit.is_none());
324 assert_eq!(quantity.note.unwrap(), "note");
325 let ingredient = Ingredient::parse_from_str("name: 1/2").unwrap();
326 assert_eq!(ingredient.name, "name");
327 assert_eq!(ingredient.kind, None);
328 assert_eq!(ingredient.quantity.unwrap().to_string(), "1/2");
329 let ingredient = Ingredient::parse_from_str("name: 1 1/2").unwrap();
330 assert_eq!(ingredient.name, "name");
331 assert_eq!(ingredient.kind, None);
332 assert_eq!(ingredient.quantity.unwrap().value.to_string(), "3/2");
333 }
334}