diff --git a/Cargo.lock b/Cargo.lock index 292a0ea..f237141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -226,35 +217,6 @@ dependencies = [ "zerocopy 0.8.14", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "ryu" version = "1.0.18" @@ -325,7 +287,6 @@ version = "0.13.1" dependencies = [ "chrono", "rand", - "regex", "serde", "serde_json", "serde_test", diff --git a/Cargo.toml b/Cargo.toml index b544b64..0ffff0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ rand = ["dep:rand"] [dependencies] chrono = "0.4.39" rand = { version = "0.9.0", optional = true } -regex = "1.11.1" serde = { version = "1.0.217", features = ["derive"], default-features = false } [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index c044fa4..a77255a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,13 +58,11 @@ use std::ops::RemAssign; use std::ops::Sub; use std::ops::SubAssign; use std::str::FromStr; -use std::sync::OnceLock; use std::time::SystemTime; use chrono::format::DelayedFormat; use chrono::format::StrftimeItems; use chrono::DateTime; -use regex::Regex; use serde::de::Visitor; use serde::Deserialize; use serde::Serialize; @@ -1605,42 +1603,38 @@ impl From for Duration { impl FromStr for Duration { type Err = DurationParseError; - fn from_str(seconds: &str) -> Result { - let captures = duration_regex() - .captures(seconds) - .ok_or(DurationParseError::UnrecognizedFormat)?; - let mut duration = Duration::ZERO; - if let Some(h) = captures.name("h") { - duration += Duration::hours( - h.as_str() - .parse::() - .map_err(|_| DurationParseError::UnrecognizedFormat)?, - ); - } - if let Some(m) = captures.name("m") { - duration += Duration::minutes( - m.as_str() - .parse::() - .map_err(|_| DurationParseError::UnrecognizedFormat)?, - ); - } - if let Some(s) = captures.name("s") { - duration += Duration::seconds( - s.as_str() - .parse::() - .map_err(|_| DurationParseError::UnrecognizedFormat)?, - ); - } - if let Some(ms) = captures.name("ms") { - duration += Duration::millis( - ms.as_str() - .parse::() - .map_err(|_| DurationParseError::UnrecognizedFormat)?, - ); + #[expect( + clippy::string_slice, + reason = "all slice indices come from methods that guarantee correctness" + )] + fn from_str(mut s: &str) -> Result { + let without_sign = s.strip_prefix('-'); + let negative = without_sign.is_some(); + s = without_sign.unwrap_or(s); + + let mut duration = Self::ZERO; + while !s.is_empty() { + let without_number = s.trim_start_matches(|c: char| c.is_ascii_digit()); + let Ok(number) = s[..s.len() - without_number.len()].parse::() else { + return Err(DurationParseError::UnrecognizedFormat); + }; + let without_unit = without_number.trim_start_matches(|c: char| !c.is_ascii_digit()); + let unit = &without_number[..without_number.len() - without_unit.len()]; + + duration += match unit { + "h" => Duration::hours(number), + "m" => Duration::minutes(number), + "s" => Duration::seconds(number), + "ms" => Duration::millis(number), + _ => return Err(DurationParseError::UnrecognizedFormat), + }; + s = without_unit; } - if captures.name("sign").is_some() { - duration *= -1; + + if negative { + duration = -duration; } + Ok(duration) } } @@ -1670,17 +1664,6 @@ const fn as_unsigned(x: i64) -> u64 { } } -const REGEX: &str = r"^(?P-)?((?P\d+)h)?((?P\d+)m)?((?P\d+)s)?((?P\d+)ms)?$"; - -#[expect( - clippy::panic, - reason = "This can only happen if the regex is incorrect which would be caught by tests" -)] -fn duration_regex() -> &'static Regex { - static LOCK: OnceLock = OnceLock::new(); - LOCK.get_or_init(|| Regex::new(REGEX).unwrap_or_else(|_| panic!("Invalid regex"))) -} - #[cfg(test)] mod time_test { use serde_test::assert_de_tokens;