From 1b4fb19af7788b87bddfa2c19a0965030030138c Mon Sep 17 00:00:00 2001 From: "Francisco J. Sanchez" Date: Wed, 13 Dec 2023 04:20:11 +0100 Subject: [PATCH] add fallback units for metadata time --- CHANGELOG.md | 3 +- src/convert/mod.rs | 2 +- src/error.rs | 8 +++ src/metadata.rs | 122 +++++++++++++++++++++++++++++++-------------- 4 files changed, 95 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a26e9e..36235ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,8 @@ - Unknown special metadata keys are now added to the metadata. - Advanced units removal of `%` now supports range values too. - New error for text value in a timer with the advanced units extension. -- Special metadata keys for time, now use the configured time units. +- Special metadata keys for time, now use the configured time units. When no + units are loaded, fallback unit times are used just for this. - Bundled units now includes `secs` and `mins` as aliases to seconds and minutes. - New warning for overriding special recipe total time with composed time and diff --git a/src/convert/mod.rs b/src/convert/mod.rs index 4f05b53..c458710 100644 --- a/src/convert/mod.rs +++ b/src/convert/mod.rs @@ -719,7 +719,7 @@ pub(crate) fn convert_f64(value: f64, from: &Unit, to: &Unit) -> f64 { /// Error when try to convert an unknown unit #[derive(Debug, Error)] #[error("Unknown unit: '{0}'")] -pub struct UnknownUnit(String); +pub struct UnknownUnit(pub String); /// Input value for [`Converter::convert`] #[derive(PartialEq, Clone, Debug)] diff --git a/src/error.rs b/src/error.rs index a87f2cf..f9bde10 100644 --- a/src/error.rs +++ b/src/error.rs @@ -394,6 +394,14 @@ impl PassResult { self.output } + /// Unwraps the inner output + /// + /// # Panics + /// If the output is `None`. + pub fn unwrap_output(self) -> T { + self.output.unwrap() + } + /// Get output, errors and warnings in a tuple pub fn into_tuple(self) -> (Option, SourceReport) { (self.output, self.report) diff --git a/src/metadata.rs b/src/metadata.rs index ee5d032..bb85241 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,6 +1,6 @@ //! Metadata of a recipe -use std::str::FromStr; +use std::{num::ParseFloatError, str::FromStr}; pub use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -21,7 +21,7 @@ macro_rules! regex { pub(crate) use regex; use crate::{ - convert::{ConvertTo, ConvertUnit, ConvertValue, PhysicalQuantity}, + convert::{ConvertError, ConvertTo, ConvertUnit, ConvertValue, PhysicalQuantity, UnknownUnit}, Converter, }; @@ -176,63 +176,107 @@ impl Metadata { /// removed pub fn map_filtered(&self) -> IndexMap { let mut new_map = self.map.clone(); - new_map.retain(|key, _| SpecialKey::from_str(key).is_ok()); + new_map.retain(|key, _| SpecialKey::from_str(key).is_err()); new_map } } /// Returns minutes -fn parse_time(s: &str, converter: &Converter) -> Result { - match parse_time_with_units(s, converter) { - Some(time) => Ok(time), - None => s.parse(), +fn parse_time(s: &str, converter: &Converter) -> Result { + if s.is_empty() { + return Err(ParseTimeError::Empty); } + let r = parse_time_with_units(s, converter); + // if any error, try to fall back to a full float parse + if r.is_err() { + let minutes = s.parse::().map(|m| m.round() as u32); + if let Ok(minutes) = minutes { + return Ok(minutes); + } + } + // otherwise return the result whatever it was + r } -fn parse_time_with_units(s: &str, converter: &Converter) -> Option { - let mut total = 0.0; - let minutes = converter.find_unit("min")?; // TODO maybe make this configurable? It will work for 99% of users... +#[derive(Debug, thiserror::Error)] +pub(crate) enum ParseTimeError { + #[error("A value is missing a unit")] + MissingUnit, + #[error("Could not find minutes in the configuration")] + MinutesNotFound, + #[error(transparent)] + ConvertError(#[from] ConvertError), + #[error(transparent)] + ParseFloatError(#[from] ParseFloatError), + #[error("An empty value is not valid")] + Empty, +} + +fn dynamic_time_units( + value: f64, + unit: &str, + converter: &Converter, +) -> Result { + // TODO maybe make this configurable? It will work for 99% of users... + let minutes = converter + .find_unit("min") + .or_else(|| converter.find_unit("minute")) + .or_else(|| converter.find_unit("minutes")) + .or_else(|| converter.find_unit("m")) + .ok_or(ParseTimeError::MinutesNotFound)?; if minutes.physical_quantity != PhysicalQuantity::Time { - return None; + return Err(ParseTimeError::MinutesNotFound); + } + let (value, _) = converter.convert( + ConvertValue::Number(value), + ConvertUnit::Key(unit), + ConvertTo::from(&minutes), + )?; + match value { + ConvertValue::Number(n) => Ok(n), + _ => unreachable!(), } - let to_minutes = ConvertTo::from(&minutes); +} - let mut parts = s.split_whitespace(); +fn hard_coded_time_units(value: f64, unit: &str) -> Result { + let minutes = match unit { + "s" | "sec" | "secs" | "second" | "seconds" => value / 60.0, + "m" | "min" | "minute" | "minutes" => value, + "h" | "hour" | "hours" => value * 60.0, + "d" | "day" | "days" => value * 24.0 * 60.0, + _ => return Err(ConvertError::UnknownUnit(UnknownUnit(unit.to_string())).into()), + }; + Ok(minutes) +} +fn parse_time_with_units(s: &str, converter: &Converter) -> Result { + let to_minutes = |value, unit| { + if converter.unit_count() == 0 { + hard_coded_time_units(value, unit) + } else { + dynamic_time_units(value, unit, converter) + } + }; + + let mut total = 0.0; + let mut parts = s.split_whitespace(); while let Some(part) = parts.next() { let first_non_digit_pos = part .char_indices() - .find_map(|(pos, c)| (!c.is_numeric()).then_some(pos)); + .find_map(|(pos, c)| (!c.is_numeric() && c != '.').then_some(pos)); let (number, unit) = if let Some(mid) = first_non_digit_pos { // if the part contains a non numeric char, split it in two and it will // be the unit part.split_at(mid) } else { // otherwise, take the next part as the unit - let next = parts.next()?; + let next = parts.next().ok_or(ParseTimeError::MissingUnit)?; (part, next) }; - - let number = number.parse::().ok()?; - let (value, _) = converter - .convert( - ConvertValue::Number(number as f64), - ConvertUnit::Key(unit), - to_minutes, - ) - .ok()?; - match value { - ConvertValue::Number(m) => total += m, - ConvertValue::Range(_) => unreachable!(), - } - } - - let total = total.round() as u32; - if total == 0 { - None - } else { - Some(total) + let number = number.parse::()?; + total += to_minutes(number, unit)?; } + Ok(total.round() as u32) } impl NameAndUrl { @@ -305,6 +349,8 @@ pub(crate) enum MetadataError { ParseIntError(#[from] std::num::ParseIntError), #[error("Duplicate servings: {servings:?}")] DuplicateServings { servings: Vec }, + #[error(transparent)] + ParseTimeError(#[from] ParseTimeError), } /// Checks that a tag is valid @@ -374,8 +420,8 @@ mod tests { #[test] fn test_parse_time_with_units() { let converter = Converter::bundled(); - let t = |s: &str| parse_time_with_units(s, &converter); - assert_eq!(t(""), None); + let t = |s: &str| parse_time_with_units(s, &converter).ok(); + assert_eq!(t(""), Some(0)); assert_eq!(t("1"), None); assert_eq!(t("1 kilometer"), None); assert_eq!(t("1min"), Some(1)); @@ -387,7 +433,7 @@ mod tests { assert_eq!(t("90 minutes"), Some(90)); assert_eq!(t("30 secs 30 secs"), Some(1)); // sum assert_eq!(t("45 secs"), Some(1)); // round up - assert_eq!(t("25 secs"), None); // round down + assert_eq!(t("25 secs"), Some(0)); // round down assert_eq!(t("1 min 25 secs"), Some(1)); // round down assert_eq!(t(" 0 hours 90min 59 sec "), Some(91)); }