shopping_parser/
lib.rs

1//! # Grammar Rules Documentation
2//!
3//! ## Rules Overview
4//!
5//! - **WHITESPACE** - matches spaces or tabs.
6//! - **product_name** - matches a product name in the format `Product name: <name>`.
7//! - **category** - matches a category in the format `Category: <category>`.
8//! - **price** - matches a price in the format `Price: <amount> <currency>/<unit>`.
9//! - **calories** - matches the caloric content in the format `Calories: <calories> cal`.
10//! - **proteins** - matches the protein content in grams in the format `Proteins: <amount> g`.
11//! - **carbohydrates** - matches the carbohydrate content in grams in the format `Carbohydrates: <amount> g`.
12//! - **fats** - matches the fat content in grams in the format `Fats: <amount> g`.
13//! - **currency_amount** - matches an amount with an optional decimal part.
14//! - **currency** - supported currencies: `UAH`, `USD`, `EUR`.
15//! - **unit** - supported units: `kg`, `l`, `ml`, `pcs`, `g`.
16//! - **name** - represents the name of a product, consisting of alphabetic characters and allowing multiple words separated by spaces.
17//! - **product** - matches a single product entry with all properties.
18//! - **products** - matches a list of products, each separated by a blank line.
19//! - **shopping_item** - matches a single shopping item in the format `<name> <quantity> <unit>`.
20//! - **shopping_list** - matches a shopping list with multiple items separated by commas.
21
22use anyhow::anyhow;
23use pest::iterators::Pair;
24use pest::Parser;
25use pest_derive::Parser;
26use serde::{Deserialize, Serialize};
27use std::fs;
28use std::fs::File;
29use std::io::{self, Read, Write};
30
31#[derive(Parser)]
32#[grammar = "./grammar.pest"]
33pub struct Grammar;
34
35#[derive(Serialize, Deserialize, Debug)]
36pub struct Product {
37    pub product_name: String,
38    pub category: String,
39    pub price_per_unit: f64,
40    pub unit: String,
41    pub calories: f64,
42    pub proteins: f64,
43    pub carbohydrates: f64,
44    pub fats: f64,
45}
46
47#[derive(Debug)]
48pub struct ShoppingItem {
49    pub product_name: String,
50    pub quantity: f64,
51    pub unit: String,
52}
53
54impl Product {
55    pub fn from_pair(pair: Pair<Rule>) -> Self {
56        let mut product_name = String::new();
57        let mut category = String::new();
58        let mut price_per_unit = 0.0;
59        let mut unit = String::new();
60        let mut calories = 0.0;
61        let mut proteins = 0.0;
62        let mut carbohydrates = 0.0;
63        let mut fats = 0.0;
64
65        for field in pair.into_inner() {
66            match field.as_rule() {
67                Rule::product_name => {
68                    product_name = field
69                        .into_inner()
70                        .find(|inner| inner.as_rule() == Rule::name)
71                        .map(|name| name.as_str().to_string())
72                        .unwrap_or_default();
73                }
74                Rule::category => {
75                    category = field.as_str().replace("Category:", "").trim().to_string();
76                }
77                Rule::price => {
78                    for inner in field.into_inner() {
79                        match inner.as_rule() {
80                            Rule::currency_amount => {
81                                price_per_unit =
82                                    inner.as_str().trim().parse::<f64>().unwrap_or(0.0);
83                            }
84                            Rule::currency => {
85                                unit = inner.as_str().to_string();
86                            }
87                            Rule::unit => {
88                                unit.push('/');
89                                unit.push_str(inner.as_str());
90                            }
91                            _ => {}
92                        }
93                    }
94                }
95                Rule::calories => {
96                    let calories_str = field
97                        .as_str()
98                        .replace("Calories:", "")
99                        .replace("cal", "")
100                        .trim()
101                        .to_string();
102                    calories = calories_str.parse::<f64>().unwrap_or(0.0);
103                }
104                Rule::proteins => {
105                    let proteins_str = field
106                        .as_str()
107                        .replace("Proteins:", "")
108                        .replace("g", "")
109                        .trim()
110                        .to_string();
111                    proteins = proteins_str.parse::<f64>().unwrap_or(0.0);
112                }
113                Rule::carbohydrates => {
114                    let carbohydrates_str = field
115                        .as_str()
116                        .replace("Carbohydrates:", "")
117                        .replace("g", "")
118                        .trim()
119                        .to_string();
120                    carbohydrates = carbohydrates_str.parse::<f64>().unwrap_or(0.0);
121                }
122                Rule::fats => {
123                    let fats_str = field
124                        .as_str()
125                        .replace("Fats:", "")
126                        .replace("g", "")
127                        .trim()
128                        .to_string();
129                    fats = fats_str.parse::<f64>().unwrap_or(0.0);
130                }
131                _ => {}
132            }
133        }
134
135        Product {
136            product_name,
137            category,
138            price_per_unit,
139            unit,
140            calories,
141            proteins,
142            carbohydrates,
143            fats,
144        }
145    }
146}
147
148pub fn parse_shopping_list(input: &str) -> Result<Vec<ShoppingItem>, anyhow::Error> {
149    let pairs = Grammar::parse(Rule::shopping_list, input)
150        .map_err(|e| anyhow!("Parsing shopping list failed: {}", e))?;
151
152    let mut items = Vec::new();
153
154    for pair in pairs {
155        match pair.as_rule() {
156            Rule::shopping_item => {
157                let mut product_name = String::new();
158                let mut quantity = 0.0;
159                let mut unit = String::new();
160
161                for field in pair.into_inner() {
162                    match field.as_rule() {
163                        Rule::name => {
164                            product_name = field.as_str().to_string();
165                        }
166                        Rule::currency_amount => {
167                            quantity = field.as_str().parse::<f64>().unwrap_or(0.0);
168                        }
169                        Rule::unit => {
170                            unit = field.as_str().to_string();
171                        }
172                        _ => {}
173                    }
174                }
175
176                items.push(ShoppingItem {
177                    product_name,
178                    quantity,
179                    unit,
180                });
181            }
182            _ => {}
183        }
184    }
185
186    Ok(items)
187}
188
189
190
191pub fn parse_products_file(file_path: &str) -> Result<Vec<Product>, anyhow::Error> {
192    let content = fs::read_to_string(file_path)?;
193    let pairs = Grammar::parse(Rule::products, &content)
194        .map_err(|e| anyhow!("Parsing failed: {}", e))?
195        .next()
196        .ok_or_else(|| anyhow!("No products found in input file"))?;
197
198    let products: Vec<Product> = pairs
199        .into_inner()
200        .filter_map(|pair| {
201            if pair.as_rule() == Rule::product {
202                Some(Product::from_pair(pair))
203            } else {
204                None
205            }
206        })
207        .collect();
208
209    Ok(products)
210}
211
212pub fn save_products_to_json(products: &[Product], output_path: &str) -> io::Result<()> {
213    let json_data = serde_json::to_string_pretty(products)?;
214    let mut file = File::create(output_path)?;
215    file.write_all(json_data.as_bytes())?;
216    Ok(())
217}
218
219pub fn load_products_from_json(input_path: &str) -> Result<Vec<Product>, io::Error> {
220    let mut file = File::open(input_path)?;
221    let mut data = String::new();
222    file.read_to_string(&mut data)?;
223    let products: Vec<Product> = serde_json::from_str(&data)?;
224    Ok(products)
225}
226