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)
}