shopping_parser/
lib.rs

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
//! # Grammar Rules Documentation
//!
//! ## Rules Overview
//!
//! - **WHITESPACE** - matches spaces or tabs.
//! - **product_name** - matches a product name in the format `Product name: <name>`.
//! - **category** - matches a category in the format `Category: <category>`.
//! - **price** - matches a price in the format `Price: <amount> <currency>/<unit>`.
//! - **calories** - matches the caloric content in the format `Calories: <calories> cal`.
//! - **proteins** - matches the protein content in grams in the format `Proteins: <amount> g`.
//! - **carbohydrates** - matches the carbohydrate content in grams in the format `Carbohydrates: <amount> g`.
//! - **fats** - matches the fat content in grams in the format `Fats: <amount> g`.
//! - **currency_amount** - matches an amount with an optional decimal part.
//! - **currency** - supported currencies: `UAH`, `USD`, `EUR`.
//! - **unit** - supported units: `kg`, `l`, `ml`, `pcs`, `g`.
//! - **product** - matches a single product entry with all properties.
//! - **products** - matches a list of products, each separated by a blank line.
//! - **shopping_item** - matches a single shopping item in the format `<name> <quantity> <unit>`.
//! - **shopping_list** - matches a shopping list with multiple items separated by commas.

use anyhow::anyhow;
use pest::iterators::Pair;
use pest::Parser;
use pest_derive::Parser;
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::File;
use std::io::{self, Read, Write};

#[derive(Parser)]
#[grammar = "./grammar.pest"]
pub struct Grammar;

#[derive(Serialize, Deserialize, Debug)]
pub struct Product {
    pub product_name: String,
    pub category: String,
    pub price_per_unit: f64,
    pub unit: String,
    pub calories: f64,
    pub proteins: f64,
    pub carbohydrates: f64,
    pub fats: f64,
}

#[derive(Debug)]
pub struct ShoppingItem {
    pub product_name: String,
    pub quantity: f64,
    pub unit: String,
}

impl Product {
    pub fn from_pair(pair: Pair<Rule>) -> Self {
        let mut product_name = String::new();
        let mut category = String::new();
        let mut price_per_unit = 0.0;
        let mut unit = String::new();
        let mut calories = 0.0;
        let mut proteins = 0.0;
        let mut carbohydrates = 0.0;
        let mut fats = 0.0;

        for field in pair.into_inner() {
            match field.as_rule() {
                Rule::product_name => {
                    product_name = field
                        .as_str()
                        .replace("Product name:", "")
                        .trim()
                        .to_string();
                }
                Rule::category => {
                    category = field.as_str().replace("Category:", "").trim().to_string();
                }
                Rule::price => {
                    for inner in field.into_inner() {
                        match inner.as_rule() {
                            Rule::currency_amount => {
                                price_per_unit =
                                    inner.as_str().trim().parse::<f64>().unwrap_or(0.0);
                            }
                            Rule::currency => {
                                unit = inner.as_str().to_string();
                            }
                            Rule::unit => {
                                unit.push('/');
                                unit.push_str(inner.as_str());
                            }
                            _ => {}
                        }
                    }
                }
                Rule::calories => {
                    let calories_str = field
                        .as_str()
                        .replace("Calories:", "")
                        .replace("cal", "")
                        .trim()
                        .to_string();
                    calories = calories_str.parse::<f64>().unwrap_or(0.0);
                }
                Rule::proteins => {
                    let proteins_str = field
                        .as_str()
                        .replace("Proteins:", "")
                        .replace("g", "")
                        .trim()
                        .to_string();
                    proteins = proteins_str.parse::<f64>().unwrap_or(0.0);
                }
                Rule::carbohydrates => {
                    let carbohydrates_str = field
                        .as_str()
                        .replace("Carbohydrates:", "")
                        .replace("g", "")
                        .trim()
                        .to_string();
                    carbohydrates = carbohydrates_str.parse::<f64>().unwrap_or(0.0);
                }
                Rule::fats => {
                    let fats_str = field
                        .as_str()
                        .replace("Fats:", "")
                        .replace("g", "")
                        .trim()
                        .to_string();
                    fats = fats_str.parse::<f64>().unwrap_or(0.0);
                }
                _ => {}
            }
        }

        Product {
            product_name,
            category,
            price_per_unit,
            unit,
            calories,
            proteins,
            carbohydrates,
            fats,
        }
    }
}

pub fn parse_shopping_list(input: &str) -> Result<Vec<ShoppingItem>, anyhow::Error> {
    let pairs = Grammar::parse(Rule::shopping_list, input)
        .map_err(|e| anyhow!("Parsing shopping list failed: {}", e))?;

    let mut items = Vec::new();

    for pair in pairs {
        if let Rule::shopping_item = pair.as_rule() {
            let mut product_name = String::new();
            let mut quantity = 0.0;
            let mut unit = String::new();

            for field in pair.into_inner() {
                match field.as_rule() {
                    Rule::product_name => {
                        product_name = field.as_str().to_string();
                    }
                    Rule::currency_amount => {
                        quantity = field.as_str().parse::<f64>().unwrap_or(0.0);
                    }
                    Rule::unit => {
                        unit = field.as_str().to_string();
                    }
                    _ => {}
                }
            }

            items.push(ShoppingItem {
                product_name,
                quantity,
                unit,
            });
        }
    }

    Ok(items)
}

pub fn parse_products_file(file_path: &str) -> Result<Vec<Product>, anyhow::Error> {
    let content = fs::read_to_string(file_path)?;
    let pairs = Grammar::parse(Rule::products, &content)
        .map_err(|e| anyhow!("Parsing failed: {}", e))?
        .next()
        .ok_or_else(|| anyhow!("No products found in input file"))?;

    let products: Vec<Product> = pairs
        .into_inner()
        .filter_map(|pair| {
            if pair.as_rule() == Rule::product {
                Some(Product::from_pair(pair))
            } else {
                None
            }
        })
        .collect();

    Ok(products)
}

pub fn save_products_to_json(products: &[Product], output_path: &str) -> io::Result<()> {
    let json_data = serde_json::to_string_pretty(products)?;
    let mut file = File::create(output_path)?;
    file.write_all(json_data.as_bytes())?;
    Ok(())
}

pub fn load_products_from_json(input_path: &str) -> Result<Vec<Product>, io::Error> {
    let mut file = File::open(input_path)?;
    let mut data = String::new();
    file.read_to_string(&mut data)?;
    let products: Vec<Product> = serde_json::from_str(&data)?;
    Ok(products)
}