use std::fmt::Display; use itertools::Itertools; use scraper::ElementRef; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Dish { name: String, image_src: Option<String>, price_students: Option<String>, price_employees: Option<String>, price_guests: Option<String>, extras: Vec<String>, dish_type: DishType, } impl Dish { pub fn get_name(&self) -> &str { &self.name } pub fn get_price_students(&self) -> Option<&str> { self.price_students.as_deref() } pub fn get_price_employees(&self) -> Option<&str> { self.price_employees.as_deref() } pub fn get_price_guests(&self) -> Option<&str> { self.price_guests.as_deref() } pub fn get_image_src(&self) -> Option<&str> { self.image_src.as_deref() } pub fn is_vegan(&self) -> bool { self.extras.contains(&"vegan".to_string()) } pub fn is_vegetarian(&self) -> bool { self.extras.contains(&"vegetarisch".to_string()) } pub fn get_extras(&self) -> &[String] { &self.extras } pub fn get_type(&self) -> DishType { self.dish_type } pub fn same_as(&self, other: &Self) -> bool { self.name == other.name && self.price_employees == other.price_employees && self.price_guests == other.price_guests && self.price_students == other.price_students && self.extras.iter().sorted().collect_vec() == self.extras.iter().sorted().collect_vec() } pub fn from_element(element: ElementRef, dish_type: DishType) -> Option<Self> { let html_name_selector = scraper::Selector::parse(".desc h4").ok()?; let name = element .select(&html_name_selector) .next()? .text() .collect::<Vec<_>>() .join("") .trim() .to_string(); let img_selector = scraper::Selector::parse(".img img").ok()?; let img_src = element.select(&img_selector).next().and_then(|el| { el.value() .attr("src") .map(|img_src_path| format!("https://www.studierendenwerk-pb.de/{}", img_src_path)) }); let html_price_selector = scraper::Selector::parse(".desc .price").ok()?; let mut prices = element .select(&html_price_selector) .filter_map(|price| { let price_for = price.first_child().and_then(|strong| { strong.first_child().and_then(|text_element| { text_element .value() .as_text() .map(|text| text.trim().trim_end_matches(':').to_string()) }) }); let price_value = price.last_child().and_then(|text_element| { text_element .value() .as_text() .map(|text| text.trim().to_string()) }); price_for .and_then(|price_for| price_value.map(|price_value| (price_for, price_value))) }) .collect::<Vec<_>>(); let html_extras_selector = scraper::Selector::parse(".desc .buttons > *").ok()?; let extras = element .select(&html_extras_selector) .filter_map(|extra| extra.value().attr("title").map(|title| title.to_string())) .collect::<Vec<_>>(); Some(Self { name, image_src: img_src, price_students: prices .iter_mut() .find(|(price_for, _)| price_for == "Studierende") .map(|(_, price)| std::mem::take(price)), price_employees: prices .iter_mut() .find(|(price_for, _)| price_for == "Bedienstete") .map(|(_, price)| std::mem::take(price)), price_guests: prices .iter_mut() .find(|(price_for, _)| price_for == "Gäste") .map(|(_, price)| std::mem::take(price)), extras, dish_type, }) } } impl PartialOrd for Dish { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { self.name.partial_cmp(&other.name) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DishType { Main, Side, Dessert, } impl Display for DishType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::Main => "main", Self::Side => "side", Self::Dessert => "dessert", }; f.write_str(s) } }