diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b663b7..cca0070 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,14 +14,8 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: check + - uses: dtolnay/rust-toolchain@stable + - run: cargo check test: name: cargo test @@ -31,30 +25,17 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: test + - uses: dtolnay/rust-toolchain@stable + - run: cargo test fmt: name: cargo fmt --all -- --check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: dtolnay/rust-toolchain@stable - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - run: cargo fmt --all -- --check clippy: name: cargo clippy -- -D warnings @@ -64,16 +45,9 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: dtolnay/rust-toolchain@stable - run: rustup component add clippy - - uses: actions-rs/cargo@v1 - with: - command: clippy - args: -- -D warnings + - run: cargo clippy --all-targets -- -D warnings coverage: name: Code Coverage @@ -108,16 +82,9 @@ jobs: outputs CODECOV_FLAGS - name: rust toolchain ~ install - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ steps.vars.outputs.TOOLCHAIN }} - default: true - profile: minimal # minimal component installation (ie, no documentation) + uses: dtolnay/rust-toolchain@nightly - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast + run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast env: CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" @@ -158,7 +125,7 @@ jobs: uses: codecov/codecov-action@v4 # if: steps.vars.outputs.HAS_CODECOV_TOKEN with: - # token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} diff --git a/Cargo.lock b/Cargo.lock index 6af6ced..2acde75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,9 +114,25 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] [[package]] name = "num-traits" @@ -135,9 +151,10 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "parse_datetime" -version = "0.5.0" +version = "0.6.0" dependencies = [ "chrono", + "nom", "regex", ] @@ -161,9 +178,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -173,9 +190,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaecc05d5c4b5f7da074b9a0d1a0867e71fd36e7fc0482d8bcfe8e8fc56290" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -184,9 +201,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.3" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "syn" diff --git a/Cargo.toml b/Cargo.toml index b28ba6f..f7148f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "parse_datetime" description = "parsing human-readable time strings and converting them to a DateTime" -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "MIT" repository = "https://github.com/uutils/parse_datetime" readme = "README.md" [dependencies] -regex = "1.9" +regex = "1.10.4" chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] } +nom = "7.1.3" diff --git a/README.md b/README.md index 31a1147..6f3b031 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -parse_datetime = "0.4.0" +parse_datetime = "0.5.0" ``` Then, import the crate and use the `parse_datetime_at_date` function: @@ -38,13 +38,13 @@ assert_eq!( ); ``` -For DateTime parsing, import the `parse_datetime` module: +For DateTime parsing, import the `parse_datetime` function: ```rs -use parse_datetime::parse_datetime::from_str; +use parse_datetime::parse_datetime; use chrono::{Local, TimeZone}; -let dt = from_str("2021-02-14 06:37:47"); +let dt = parse_datetime("2021-02-14 06:37:47"); assert_eq!(dt.unwrap(), Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap()); ``` diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 5a7c0fd..f5e3e7b 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -148,9 +148,9 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "libfuzzer-sys" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb09950ae85a0a94b27676cccf37da5ff13f27076aa1adbc6545dd0d0e1bd4e" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" dependencies = [ "arbitrary", "cc", @@ -169,6 +169,22 @@ version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -189,6 +205,7 @@ name = "parse_datetime" version = "0.5.0" dependencies = [ "chrono", + "nom", "regex", ] @@ -248,9 +265,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -260,9 +277,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", @@ -271,9 +288,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "syn" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index b5a0de8..d8d56b8 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,8 +8,8 @@ cargo-fuzz = true [dependencies] rand = "0.8.5" -libfuzzer-sys = "0.4" -regex = "1.9.5" +libfuzzer-sys = "0.4.7" +regex = "1.10.4" chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] } [dependencies.parse_datetime] diff --git a/src/lib.rs b/src/lib.rs index 90e191b..4d634f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,10 +14,18 @@ use std::fmt::{self, Display}; // Expose parse_datetime mod parse_relative_time; +mod parse_timestamp; -use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone}; +mod parse_time_only_str; +mod parse_weekday; + +use chrono::{ + DateTime, Datelike, Duration, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone, + Timelike, +}; use parse_relative_time::parse_relative_time; +use parse_timestamp::parse_timestamp; #[derive(Debug, PartialEq)] pub enum ParseDateTimeError { @@ -168,19 +176,38 @@ pub fn parse_datetime_at_date + Clone>( } } + // parse weekday + if let Some(weekday) = parse_weekday::parse_weekday(s.as_ref()) { + let mut beginning_of_day = date + .with_hour(0) + .unwrap() + .with_minute(0) + .unwrap() + .with_second(0) + .unwrap() + .with_nanosecond(0) + .unwrap(); + + while beginning_of_day.weekday() != weekday { + beginning_of_day += Duration::days(1); + } + + let dt = DateTime::::from(beginning_of_day); + + return Ok(dt); + } + // Parse epoch seconds - if s.as_ref().bytes().next() == Some(b'@') { - if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[1..], "%s") { - if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) { - return Ok(dt); - } + if let Ok(timestamp) = parse_timestamp(s.as_ref()) { + if let Some(timestamp_date) = NaiveDateTime::from_timestamp_opt(timestamp, 0) { + return Ok(date.offset().from_utc_datetime(×tamp_date)); } } - let ts = s.as_ref().to_owned() + "0000"; + let ts = s.as_ref().to_owned() + " 0000"; // Parse date only formats - assume midnight local timezone for fmt in [format::ISO_8601, format::ISO_8601_NO_SEP] { - let f = fmt.to_owned() + "%H%M"; + let f = fmt.to_owned() + " %H%M"; if let Ok(parsed) = NaiveDateTime::parse_from_str(&ts, &f) { if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) { return Ok(dt); @@ -209,6 +236,11 @@ pub fn parse_datetime_at_date + Clone>( } } + // parse time only dates + if let Some(date_time) = parse_time_only_str::parse_time_only(date, s.as_ref()) { + return Ok(date_time); + } + // Default parse and failure s.as_ref() .parse() @@ -285,6 +317,31 @@ mod tests { let actual = parse_datetime(dt); assert_eq!(actual.unwrap().timestamp(), TEST_TIME); } + + #[test] + fn test_epoch_seconds_non_utc() { + env::set_var("TZ", "EST"); + let dt = "@1613371067"; + let actual = parse_datetime(dt); + assert_eq!(actual.unwrap().timestamp(), TEST_TIME); + } + } + + #[cfg(test)] + mod formats { + use crate::parse_datetime; + use chrono::{DateTime, Local, TimeZone}; + + #[test] + fn single_digit_month_day() { + let x = Local.with_ymd_and_hms(1987, 5, 7, 0, 0, 0).unwrap(); + let expected = DateTime::fixed_offset(&x); + + assert_eq!(Ok(expected), parse_datetime("1987-05-07")); + assert_eq!(Ok(expected), parse_datetime("1987-5-07")); + assert_eq!(Ok(expected), parse_datetime("1987-05-7")); + assert_eq!(Ok(expected), parse_datetime("1987-5-7")); + } } #[cfg(test)] @@ -348,30 +405,101 @@ mod tests { ]; for relative_time in relative_times { - assert_eq!(parse_datetime(relative_time).is_ok(), true); + assert!(parse_datetime(relative_time).is_ok()); } } } + #[cfg(test)] + mod weekday { + use chrono::{DateTime, Local, TimeZone}; + + use crate::parse_datetime_at_date; + + fn get_formatted_date(date: DateTime, weekday: &str) -> String { + let result = parse_datetime_at_date(date, weekday).unwrap(); + + return result.format("%F %T %f").to_string(); + } + #[test] + fn test_weekday() { + // add some constant hours and minutes and seconds to check its reset + let date = Local.with_ymd_and_hms(2023, 2, 28, 10, 12, 3).unwrap(); + + // 2023-2-28 is tuesday + assert_eq!( + get_formatted_date(date, "tuesday"), + "2023-02-28 00:00:00 000000000" + ); + + // 2023-3-01 is wednesday + assert_eq!( + get_formatted_date(date, "wed"), + "2023-03-01 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "thu"), + "2023-03-02 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "fri"), + "2023-03-03 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "sat"), + "2023-03-04 00:00:00 000000000" + ); + + assert_eq!( + get_formatted_date(date, "sun"), + "2023-03-05 00:00:00 000000000" + ); + } + } + #[cfg(test)] mod timestamp { use crate::parse_datetime; use chrono::{TimeZone, Utc}; #[test] - fn test_positive_offsets() { + fn test_positive_and_negative_offsets() { let offsets: Vec = vec![ 0, 1, 2, 10, 100, 150, 2000, 1234400000, 1334400000, 1692582913, 2092582910, ]; for offset in offsets { + // positive offset let time = Utc.timestamp_opt(offset, 0).unwrap(); let dt = parse_datetime(format!("@{}", offset)); assert_eq!(dt.unwrap(), time); + + // negative offset + let time = Utc.timestamp_opt(-offset, 0).unwrap(); + let dt = parse_datetime(format!("@-{}", offset)); + assert_eq!(dt.unwrap(), time); } } } + #[cfg(test)] + mod timeonly { + use crate::parse_datetime_at_date; + use chrono::{Local, TimeZone}; + use std::env; + #[test] + fn test_time_only() { + env::set_var("TZ", "UTC"); + let test_date = Local.with_ymd_and_hms(2024, 3, 3, 0, 0, 0).unwrap(); + let parsed_time = parse_datetime_at_date(test_date, "9:04:30 PM +0530") + .unwrap() + .timestamp(); + assert_eq!(parsed_time, 1709480070) + } + } /// Used to test example code presented in the README. mod readme_test { use crate::parse_datetime; diff --git a/src/parse_relative_time.rs b/src/parse_relative_time.rs index afa47b5..7bc0840 100644 --- a/src/parse_relative_time.rs +++ b/src/parse_relative_time.rs @@ -1,3 +1,5 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. use crate::ParseDateTimeError; use chrono::{Duration, Local, NaiveDate, Utc}; use regex::Regex; diff --git a/src/parse_time_only_str.rs b/src/parse_time_only_str.rs new file mode 100644 index 0000000..e909216 --- /dev/null +++ b/src/parse_time_only_str.rs @@ -0,0 +1,120 @@ +use chrono::{DateTime, FixedOffset, Local, NaiveTime, TimeZone}; +use regex::Regex; + +mod time_only_formats { + pub const HH_MM: &str = "%R"; + pub const HH_MM_SS: &str = "%T"; + pub const TWELVEHOUR: &str = "%r"; +} + +pub(crate) fn parse_time_only(date: DateTime, s: &str) -> Option> { + let re = + Regex::new(r"^(?