diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..65946c8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + +[[package]] +name = "humantime_to_duration" +version = "0.1.0" +dependencies = [ + "regex", + "time", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "regex" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0fd618b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "humantime_to_duration" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +regex = "1.8.1" +time = "0.3.20" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..239ad94 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,196 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use regex::Regex; +use time::Duration; + +/// Parses a relative time string and returns a `Duration` representing the +/// relative time. +/// +/// # Arguments +/// +/// * `s` - A string slice representing the relative time. +/// +/// # Examples +/// +/// ``` +/// use time::Duration; +/// let duration = humantime_to_duration::from_str("+3 days"); +/// assert_eq!(duration, Some(Duration::days(3))); +/// ``` +/// +/// # Supported formats +/// +/// The function supports the following formats for relative time: +/// +/// * `num` `unit` (e.g., "-1 hour", "+3 days") +/// * `unit` (e.g., "hour", "day") +/// * "now" or "today" +/// * "yesterday" +/// * "tomorrow" +/// * use "ago" for the past +/// +/// [num] can be a positive or negative integer. +/// [unit] can be one of the following: "fortnight", "week", "day", "hour", +/// "minute", "min", "second", "sec" and their plural forms. +/// +/// It is also possible to pass "1 hour 2 minutes" or "2 days and 2 hours" +/// +/// # Returns +/// +/// * `Some(Duration)` - If the input string can be parsed as a relative time +/// * `None` - If the input string cannot be parsed as a relative time +/// +/// # Errors +/// +/// This function will return `None` if the input string cannot be parsed as a +/// relative time. +pub fn from_str(s: &str) -> Option { + let time_pattern: Regex = Regex::new( + r"(?x) + (?P[-+]?\d*)\s* + (?Pyears?|months?|fortnights?|weeks?|days?|hours?|h|minutes?|mins?|m|seconds?|secs?|s|yesterday|tomorrow|now|today) + (\s*(?Pand|,)?\s*)?" + ) + .unwrap(); + + let mut total_duration = Duration::ZERO; + let mut is_ago = s.contains(" ago"); + + for capture in time_pattern.captures_iter(s) { + let value_str = capture.name("value").unwrap().as_str(); + let value = if value_str.is_empty() { + 1 + } else { + value_str.parse::().unwrap_or(1) + }; + let unit = capture.name("unit").unwrap().as_str(); + + if capture.name("ago").is_some() { + is_ago = true; + } + + let duration = match unit { + "years" | "year" => Duration::days(value * 365), + "months" | "month" => Duration::days(value * 30), + "fortnights" | "fortnight" => Duration::weeks(value * 2), + "weeks" | "week" => Duration::weeks(value), + "days" | "day" => Duration::days(value), + "hours" | "hour" | "h" => Duration::hours(value), + "minutes" | "minute" | "mins" | "min" | "m" => Duration::minutes(value), + "seconds" | "second" | "secs" | "sec" | "s" => Duration::seconds(value), + "yesterday" => Duration::days(-1), + "tomorrow" => Duration::days(1), + "now" | "today" => Duration::ZERO, + _ => return None, + }; + + total_duration = total_duration.checked_add(if is_ago { -duration } else { duration })?; + } + + if total_duration == Duration::ZERO && !time_pattern.is_match(s) { + None + } else { + Some(total_duration) + } +} + + +#[cfg(test)] +mod tests { + + use super::from_str; + use time::Duration; + + #[test] + fn test_years() { + assert_eq!(from_str("1 year"), Some(Duration::seconds(31536000))); + assert_eq!(from_str("-2 years"), Some(Duration::seconds(-63072000))); + assert_eq!(from_str("2 years ago"), Some(Duration::seconds(-63072000))); + assert_eq!(from_str("year"), Some(Duration::seconds(31536000))); + } + + #[test] + fn test_months() { + assert_eq!(from_str("1 month"), Some(Duration::seconds(2592000))); + assert_eq!(from_str("1 month and 2 weeks"), Some(Duration::seconds(3801600))); + assert_eq!(from_str("1 month and 2 weeks ago"), Some(Duration::seconds(-3801600))); + assert_eq!(from_str("2 months"), Some(Duration::seconds(5184000))); + assert_eq!(from_str("month"), Some(Duration::seconds(2592000))); + } + + #[test] + fn test_fortnights() { + assert_eq!(from_str("1 fortnight"), Some(Duration::seconds(1209600))); + assert_eq!(from_str("3 fortnights"), Some(Duration::seconds(3628800))); + assert_eq!(from_str("fortnight"), Some(Duration::seconds(1209600))); + } + + #[test] + fn test_weeks() { + assert_eq!(from_str("1 week"), Some(Duration::seconds(604800))); + assert_eq!(from_str("1 week 3 days"), Some(Duration::seconds(864000))); + assert_eq!(from_str("1 week 3 days ago"), Some(Duration::seconds(-864000))); + assert_eq!(from_str("-2 weeks"), Some(Duration::seconds(-1209600))); + assert_eq!(from_str("2 weeks ago"), Some(Duration::seconds(-1209600))); + assert_eq!(from_str("week"), Some(Duration::seconds(604800))); + } + + #[test] + fn test_days() { + assert_eq!(from_str("1 day"), Some(Duration::seconds(86400))); + assert_eq!(from_str("2 days ago"), Some(Duration::seconds(-172800))); + assert_eq!(from_str("-2 days"), Some(Duration::seconds(-172800))); + assert_eq!(from_str("day"), Some(Duration::seconds(86400))); + } + + #[test] + fn test_hours() { + assert_eq!(from_str("1 hour"), Some(Duration::seconds(3600))); + assert_eq!(from_str("1 hour ago"), Some(Duration::seconds(-3600))); + assert_eq!(from_str("-2 hours"), Some(Duration::seconds(-7200))); + assert_eq!(from_str("hour"), Some(Duration::seconds(3600))); + } + + #[test] + fn test_minutes() { + assert_eq!(from_str("1 minute"), Some(Duration::seconds(60))); + assert_eq!(from_str("2 minutes"), Some(Duration::seconds(120))); + assert_eq!(from_str("min"), Some(Duration::seconds(60))); + } + + #[test] + fn test_seconds() { + assert_eq!(from_str("1 second"), Some(Duration::seconds(1))); + assert_eq!(from_str("2 seconds"), Some(Duration::seconds(2))); + assert_eq!(from_str("sec"), Some(Duration::seconds(1))); + } + + #[test] + fn test_relative_days() { + assert_eq!(from_str("now"), Some(Duration::seconds(0))); + assert_eq!(from_str("today"), Some(Duration::seconds(0))); + assert_eq!(from_str("yesterday"), Some(Duration::seconds(-86400))); + assert_eq!(from_str("tomorrow"), Some(Duration::seconds(86400))); + } + + #[test] + fn test_no_spaces() { + assert_eq!(from_str("-1hour"), Some(Duration::hours(-1))); + assert_eq!(from_str("+3days"), Some(Duration::days(3))); + assert_eq!(from_str("2weeks"), Some(Duration::weeks(2))); + assert_eq!(from_str("2weeks 1hour"), Some(Duration::seconds(1213200))); + assert_eq!(from_str("2weeks 1hour ago"), Some(Duration::seconds(-1213200))); + assert_eq!(from_str("+4months"), Some(Duration::days(4 * 30))); + assert_eq!(from_str("-2years"), Some(Duration::days(-2 * 365))); + assert_eq!(from_str("15minutes"), Some(Duration::minutes(15))); + assert_eq!(from_str("-30seconds"), Some(Duration::seconds(-30))); + assert_eq!(from_str("30seconds ago"), Some(Duration::seconds(-30))); + } + + #[test] + fn test_invalid_input() { + assert_eq!(from_str("invalid"), None); + assert_eq!(from_str("1 invalid"), None); + } +}