├── Cargo.toml ├── dateparser ├── examples │ ├── parse.rs │ ├── str_parse_method.rs │ ├── convert_to_pacific.rs │ ├── parse_with_timezone.rs │ └── parse_with.rs ├── Cargo.toml ├── benches │ └── parse.rs ├── src │ ├── timezone.rs │ ├── lib.rs │ └── datetime.rs └── README.md ├── .gitignore ├── belt ├── src │ ├── main.rs │ ├── opts.rs │ ├── app.rs │ └── config.rs ├── Cargo.toml └── README.md ├── LICENSE ├── .github └── workflows │ ├── ci.yml │ └── build_and_release.yml ├── Makefile ├── README.md └── Cargo.lock /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "belt", 5 | "dateparser", 6 | ] 7 | -------------------------------------------------------------------------------- /dateparser/examples/parse.rs: -------------------------------------------------------------------------------- 1 | use dateparser::parse; 2 | use std::error::Error; 3 | 4 | fn main() -> Result<(), Box> { 5 | let parsed = parse("6:15pm")?; 6 | println!("{:#?}", parsed); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | 9 | # Added by cargo 10 | 11 | /target 12 | 13 | *.swp 14 | -------------------------------------------------------------------------------- /dateparser/examples/str_parse_method.rs: -------------------------------------------------------------------------------- 1 | use dateparser::DateTimeUtc; 2 | use std::error::Error; 3 | 4 | fn main() -> Result<(), Box> { 5 | let parsed = "2021-05-14 18:51 PDT".parse::()?.0; 6 | println!("{:#?}", parsed); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /dateparser/examples/convert_to_pacific.rs: -------------------------------------------------------------------------------- 1 | use chrono_tz::US::Pacific; 2 | use dateparser::DateTimeUtc; 3 | use std::error::Error; 4 | 5 | fn main() -> Result<(), Box> { 6 | let parsed = "Wed, 02 Jun 2021 06:31:39 GMT".parse::()?.0; 7 | println!("{:#?}", parsed.with_timezone(&Pacific)); 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /belt/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod config; 3 | mod opts; 4 | 5 | use crate::{app::App, config::Config, opts::Opts}; 6 | use anyhow::Result; 7 | 8 | fn main() -> Result<()> { 9 | let opts = Opts::new(); 10 | let mut out = std::io::stdout(); 11 | let mut config = Config::new(&opts.app, &mut out)?; 12 | let mut app = App::new(&opts, &mut config); 13 | 14 | app.show_datetime()?; 15 | app.handle_subcommands()?; 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /dateparser/examples/parse_with_timezone.rs: -------------------------------------------------------------------------------- 1 | use chrono::offset::{Local, Utc}; 2 | use chrono_tz::US::Pacific; 3 | use dateparser::parse_with_timezone; 4 | use std::error::Error; 5 | 6 | fn main() -> Result<(), Box> { 7 | let parsed_in_local = parse_with_timezone("6:15pm", &Local)?; 8 | println!("{:#?}", parsed_in_local); 9 | 10 | let parsed_in_utc = parse_with_timezone("6:15pm", &Utc)?; 11 | println!("{:#?}", parsed_in_utc); 12 | 13 | let parsed_in_pacific = parse_with_timezone("6:15pm", &Pacific)?; 14 | println!("{:#?}", parsed_in_pacific); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /belt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "belt" 3 | version = "0.2.1" 4 | authors = ["Rollie Ma "] 5 | edition = "2021" 6 | publish = false 7 | description = "Know your time from a list of selected time zones" 8 | 9 | [dependencies] 10 | anyhow = "1.0.75" 11 | chrono = "0.4.31" 12 | chrono-tz = "0.8.4" 13 | clap = { version = "4.4.8", features = ["derive"] } 14 | colored = "2.0.4" 15 | confy = "0.5.1" 16 | dateparser = { path = "../dateparser" } 17 | directories = "5.0.1" 18 | prettytable-rs = "0.10.0" 19 | serde = { version = "1.0.192", features = ["derive"] } 20 | 21 | [dev-dependencies] 22 | rand = "0.8.5" 23 | regex = "1.10.2" 24 | -------------------------------------------------------------------------------- /dateparser/examples/parse_with.rs: -------------------------------------------------------------------------------- 1 | use chrono::{ 2 | naive::NaiveTime, 3 | offset::{Local, Utc}, 4 | }; 5 | use dateparser::parse_with; 6 | use std::error::Error; 7 | 8 | fn main() -> Result<(), Box> { 9 | let parsed_in_local = parse_with( 10 | "2021-10-09", 11 | &Local, 12 | NaiveTime::from_hms_opt(0, 0, 0).unwrap(), 13 | )?; 14 | println!("{:#?}", parsed_in_local); 15 | 16 | let parsed_in_utc = parse_with( 17 | "2021-10-09", 18 | &Utc, 19 | NaiveTime::from_hms_opt(0, 0, 0).unwrap(), 20 | )?; 21 | println!("{:#?}", parsed_in_utc); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /dateparser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dateparser" 3 | version = "0.2.1" 4 | authors = ["Rollie Ma "] 5 | description = "Parse dates in string formats that are commonly used" 6 | readme = "README.md" 7 | homepage = "https://github.com/waltzofpearls/dateparser" 8 | repository = "https://github.com/waltzofpearls/dateparser" 9 | keywords = ["date", "time", "datetime", "parser", "parse"] 10 | license = "MIT" 11 | edition = "2021" 12 | 13 | [features] 14 | wasm = ["dep:js-sys","dep:wasm-bindgen"] 15 | 16 | 17 | [target.'cfg(target_arch="wasm32")'.dev-dependencies] 18 | wasm-bindgen-test = "0.3" 19 | 20 | [target.'cfg(target_arch="wasm32")'.dependencies] 21 | js-sys = {version="0.3",optional=true} 22 | wasm-bindgen = {version="0.2",optional=true} 23 | 24 | [dependencies] 25 | anyhow = "1.0.75" 26 | chrono = "0.4.31" 27 | regex = "1.10.2" 28 | 29 | [dev-dependencies] 30 | chrono-tz = "0.8.4" 31 | # default features include rayon, which fails on wasm targets 32 | criterion = { version = "0.5.1", features = ["html_reports"], default-features=false } 33 | 34 | [[bench]] 35 | name = "parse" 36 | harness = false 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rollie Ma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /belt/src/opts.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(author, about, version)] 5 | pub struct Opts { 6 | #[arg(name = "TIME")] 7 | pub time: Option, 8 | /// Provide a terse answer, and default to a verbose form 9 | #[arg(short, long)] 10 | pub short: bool, 11 | 12 | /// Name of the config 13 | #[arg(short, long, name = "NAME", default_value = "belt")] 14 | pub app: String, 15 | 16 | #[command(subcommand)] 17 | pub subcommands: Option, 18 | } 19 | 20 | #[derive(Subcommand, Debug)] 21 | pub enum Subcommands { 22 | /// Configure time zones list 23 | Config(OptsConfig), 24 | } 25 | 26 | #[derive(Parser, Debug)] 27 | pub struct OptsConfig { 28 | /// List existing time zones 29 | #[arg(short, long)] 30 | pub list: bool, 31 | /// Reset to default list of time zones 32 | #[arg(short, long)] 33 | pub reset: bool, 34 | /// Add a new time zone to the list 35 | #[arg(short, long, name = "timezone_to_add")] 36 | pub add: Option, 37 | /// Delete a time zone from the list 38 | #[arg(short, long, name = "timezone_to_delete")] 39 | pub delete: Option, 40 | } 41 | 42 | impl Opts { 43 | pub fn new() -> Self { 44 | Self::parse() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /belt/README.md: -------------------------------------------------------------------------------- 1 | # `belt` CLI tool 2 | 3 | Command-line app that can show your time from a list of selected time zones. It uses `dateparser` 4 | rust crate to parse date strings in commonly used formats. 5 | 6 | ## Installation 7 | 8 | MacOS Homebrew or Linuxbrew: 9 | 10 | ```shell 11 | brew tap waltzofpearls/belt 12 | brew install belt 13 | ``` 14 | 15 | ## Run `belt` to parse a given date 16 | 17 | ```shell 18 | $ belt 'MAY 12, 2021 16:44 UTC' 19 | +-------------------+---------------------------+ 20 | | Zone | Date & Time | 21 | +===================+===========================+ 22 | | Local | 2021-05-12 09:44:00 -0700 | 23 | | | 1620837840 | 24 | +-------------------+---------------------------+ 25 | | UTC | 2021-05-12 16:44:00 +0000 | 26 | | | 2021-05-12 16:44 UTC | 27 | +-------------------+---------------------------+ 28 | | America/Vancouver | 2021-05-12 09:44:00 -0700 | 29 | | | 2021-05-12 09:44 PDT | 30 | +-------------------+---------------------------+ 31 | | America/New_York | 2021-05-12 12:44:00 -0400 | 32 | | | 2021-05-12 12:44 EDT | 33 | +-------------------+---------------------------+ 34 | | Europe/London | 2021-05-12 17:44:00 +0100 | 35 | | | 2021-05-12 17:44 BST | 36 | +-------------------+---------------------------+ 37 | ``` 38 | 39 | ## Display parsed date in the short form 40 | 41 | ```shell 42 | # parse a unix epoch timestamp 43 | $ belt 1511648546 --short 44 | 2017-11-25 14:22:26 -0800 45 | 46 | # or show the current local datetime 47 | $ belt --short 48 | 2021-05-15 22:54:34 -0700 49 | ``` 50 | 51 | ## Configure time zone 52 | 53 | ```shell 54 | $ belt config --help 55 | belt-config 56 | Configure time zones list 57 | 58 | USAGE: 59 | belt config [FLAGS] [OPTIONS] 60 | 61 | FLAGS: 62 | -h, --help Prints help information 63 | -l, --list List existing time zones 64 | -r, --reset Reset to default list of time zones 65 | -V, --version Prints version information 66 | 67 | OPTIONS: 68 | -a, --add Add a new time zone to the list 69 | -d, --delete Delete a time zone from the list 70 | ``` 71 | -------------------------------------------------------------------------------- /dateparser/benches/parse.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 2 | use dateparser::parse; 3 | 4 | static SELECTED: [&str; 25] = [ 5 | "1511648546", // unix_timestamp 6 | "2017-11-25T22:34:50Z", // rfc3339 7 | "Wed, 02 Jun 2021 06:31:39 GMT", // rfc2822 8 | "2019-11-29 08:08:05-08", // postgres_timestamp 9 | "2021-04-30 21:14:10", // ymd_hms 10 | "2017-11-25 13:31:15 PST", // ymd_hms_z 11 | "2021-02-21", // ymd 12 | "2021-02-21 PST", // ymd_z 13 | "4:00pm", // hms 14 | "6:00 AM PST", // hms_z 15 | "May 27 02:45:27", // month_md_hms 16 | "May 8, 2009 5:57:51 PM", // month_mdy_hms 17 | "May 02, 2021 15:51 UTC", // month_mdy_hms_z 18 | "2021-Feb-21", // month_ymd 19 | "May 25, 2021", // month_mdy 20 | "14 May 2019 19:11:40.164", // month_dmy_hms 21 | "1 July 2013", // month_dmy 22 | "03/19/2012 10:11:59", // slash_mdy_hms 23 | "08/21/71", // slash_mdy 24 | "2012/03/19 10:11:59", // slash_ymd_hms 25 | "2014/3/31", // slash_ymd 26 | "2014.03.30", // dot_mdy_or_ymd 27 | "171113 14:14:20", // mysql_log_timestamp 28 | "2014年04月08日11时25分18秒", // chinese_ymd_hms 29 | "2014年04月08日", // chinese_ymd 30 | ]; 31 | 32 | fn bench_parse_all(c: &mut Criterion) { 33 | c.bench_with_input( 34 | BenchmarkId::new("parse_all", "accepted_formats"), 35 | &SELECTED, 36 | |b, all| { 37 | b.iter(|| { 38 | for date_str in all.iter() { 39 | let _ = parse(date_str); 40 | } 41 | }) 42 | }, 43 | ); 44 | } 45 | 46 | fn bench_parse_each(c: &mut Criterion) { 47 | let mut group = c.benchmark_group("parse_each"); 48 | for date_str in SELECTED.iter() { 49 | group.bench_with_input(*date_str, *date_str, |b, input| b.iter(|| parse(input))); 50 | } 51 | group.finish(); 52 | } 53 | 54 | criterion_group!(benches, bench_parse_all, bench_parse_each); 55 | criterion_main!(benches); 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: # run tests on every git push except pushing tags 4 | tags-ignore: 5 | - "**" 6 | pull_request: # run tests on every pull request 7 | 8 | jobs: 9 | check: 10 | name: check - ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} 11 | runs-on: ${{ matrix.platform.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: 16 | - os_name: Linux 17 | os: ubuntu-latest 18 | - os_name: macOS 19 | os: macos-latest 20 | - os_name: Windows 21 | os: windows-latest 22 | toolchain: 23 | - stable 24 | - nightly 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: dtolnay/rust-toolchain@stable 28 | with: 29 | toolchain: ${{ matrix.toolchain }} 30 | - run: cargo check --all --all-targets --all-features 31 | 32 | test: 33 | name: test - ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} 34 | runs-on: ${{ matrix.platform.os }} 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | platform: 39 | - os_name: Linux 40 | os: ubuntu-latest 41 | - os_name: macOS 42 | os: macos-latest 43 | - os_name: Windows 44 | os: windows-latest 45 | toolchain: 46 | - stable 47 | - nightly 48 | steps: 49 | - uses: actions/checkout@v3 50 | - uses: dtolnay/rust-toolchain@stable 51 | with: 52 | toolchain: ${{ matrix.toolchain }} 53 | - run: cargo test --workspace --all-features 54 | 55 | wasm-test: 56 | name: test dateparser in wasm with rust ${{ matrix.toolchain }} 57 | runs-on: ubuntu-latest 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | toolchain: 62 | - stable 63 | - nightly 64 | steps: 65 | - uses: actions/checkout@v3 66 | - uses: dtolnay/rust-toolchain@stable 67 | with: 68 | toolchain: ${{ matrix.toolchain }} 69 | - name: install wasm-pack 70 | run: cargo install wasm-pack 71 | - name: test 72 | run: cd dateparser && wasm-pack test --node --features=wasm 73 | 74 | fmt: 75 | name: fmt 76 | runs-on: ubuntu-latest 77 | steps: 78 | - uses: actions/checkout@v3 79 | - uses: dtolnay/rust-toolchain@stable 80 | with: 81 | toolchain: stable 82 | - run: rustup component add rustfmt 83 | - run: cargo fmt --all -- --check 84 | 85 | clippy: 86 | name: clippy 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v3 90 | - uses: dtolnay/rust-toolchain@stable 91 | with: 92 | toolchain: stable 93 | - run: rustup component add clippy 94 | - run: cargo clippy --workspace --tests --all-features -- -D warnings 95 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: 3 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 4 | 5 | .PHONY: build 6 | build: ## Build release with cargo for the current OS 7 | cargo build --release 8 | 9 | .PHONY: lint 10 | lint: ## Run clippy linter 11 | cargo clippy --workspace --tests --all-features -- -D warnings 12 | 13 | .PHONY: test 14 | test: ## Run unit tests 15 | RUST_BACKTRACE=1 cargo test 16 | 17 | .PHONY: cover 18 | cover: ## Generate test coverage report 19 | docker run \ 20 | --security-opt seccomp=unconfined \ 21 | -v ${PWD}:/volume \ 22 | -e "RUST_BACKTRACE=1" \ 23 | xd009642/tarpaulin \ 24 | cargo tarpaulin --color auto --out Html --output-dir ./target 25 | open target/tarpaulin-report.html 26 | 27 | .PHONY: bench 28 | bench: ## Generate benchmark report 29 | cargo bench --bench parse -- --verbose 30 | open target/criterion/report/index.html 31 | 32 | APP = belt 33 | VERSION := $(shell cargo metadata -q | jq -r '.packages[] | select(.name == "$(APP)") | .version') 34 | UNAME_S := $(shell uname -s) 35 | NEXT_VERSION := $(shell echo "$(VERSION)" | awk -F. -v OFS=. '{$$NF += 1 ; print}') 36 | 37 | .PHONY: package 38 | package: ## Make release package based on the current OS 39 | ifdef OS # windows 40 | mkdir -p target/package 41 | tar -a -cvf target/package/$(APP)-$(VERSION)-windows-x86_64-msvc.zip \ 42 | -C $$PWD/target/x86_64-pc-windows-msvc/release $(APP).exe \ 43 | -C $$PWD LICENSE README.md 44 | else ifeq ($(UNAME_S),Darwin) # macOS 45 | mkdir -p target/package 46 | zip -j target/package/$(APP)-$(VERSION)-macos-x86_64.zip \ 47 | target/x86_64-apple-darwin/release/$(APP) LICENSE README.md 48 | else ifeq ($(UNAME_S),Linux) # linux 49 | sudo mkdir -p target/package 50 | sudo tar -z -cvf target/package/$(APP)-$(VERSION)-$(arch)-unknown-linux-$(libc).tar.gz \ 51 | -C $$PWD/target/$(arch)-unknown-linux-$(libc)/release $(APP) \ 52 | -C $$PWD LICENSE README.md 53 | endif 54 | 55 | .PHONY: show-version-files 56 | show-version-files: ## Find all files with the current version 57 | @grep -rn --color \ 58 | --exclude-dir={target,.git} \ 59 | --exclude Cargo.lock \ 60 | --fixed-strings '"$(VERSION)"' . 61 | 62 | .PHONY: bump-version 63 | bump-version: ## Bump version in files that contain the current version 64 | @echo "👉 Bumping version $(VERSION) -> $(NEXT_VERSION)..." 65 | @echo 66 | @echo "🚀 Create a git branch for the version bump:" 67 | git checkout -b bump-version-$(NEXT_VERSION) 68 | @echo 69 | @echo "🚀 Update version in files:" 70 | @for file in $(shell grep -rl --exclude-dir={target,.git} --exclude Cargo.lock --fixed-strings '"$(VERSION)"' .); do \ 71 | echo "✅ In file $$file"; \ 72 | sed -i '' -e 's/$(subst .,\.,$(VERSION))/$(NEXT_VERSION)/g' $$file; \ 73 | git add $$file; \ 74 | done 75 | @echo 76 | @echo "🚀 Update Cargo.lock:" 77 | cargo update 78 | git add Cargo.lock 79 | @echo 80 | @echo "🚀 Bumped version in the following files:" 81 | @make show-version-files 82 | @echo 83 | @echo "🚀 Commit the changes:" 84 | git commit -m "version: bump to $(NEXT_VERSION)" 85 | @echo "🚀 Push the branch to GitHub:" 86 | git push --set-upstream origin bump-version-$(NEXT_VERSION) 87 | @echo 88 | @echo "🎉 Next, create a pull request on GitHub and merge it." 89 | 90 | .PHONY: release 91 | release: ## Make a new tag based on the version from Cargo.toml and push to GitHub 92 | @if [[ "$(shell git tag -l)" == *"v$(VERSION)"* ]]; then \ 93 | echo "Tag v$(VERSION) already exists"; \ 94 | else \ 95 | echo "Tagging v$(VERSION) and pushing to GitHub..."; \ 96 | git tag -a v$(VERSION) -m "Release v$(VERSION)"; \ 97 | git push origin v$(VERSION); \ 98 | fi 99 | 100 | .PHONY: publish 101 | publish: ## Publish to crates.io 102 | cargo publish --manifest-path dateparser/Cargo.toml --token $(token) 103 | -------------------------------------------------------------------------------- /.github/workflows/build_and_release.yml: -------------------------------------------------------------------------------- 1 | name: build and release 2 | on: 3 | push: # run build and release only on new git tags 4 | tags: 5 | - "v*.*.*" # match v*.*.*, i.e. v0.1.5, v20.15.10 6 | 7 | jobs: 8 | build-linux: 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | matrix: 12 | include: 13 | - { arch: "x86_64", libc: "musl" } 14 | - { arch: "i686", libc: "musl" } 15 | - { arch: "aarch64", libc: "musl" } 16 | - { arch: "armv7", libc: "musleabihf" } 17 | - { arch: "arm", libc: "musleabihf" } 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Pull Docker image 21 | run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} 22 | - name: Build in Docker 23 | run: | 24 | docker run --rm -i \ 25 | -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} \ 26 | cargo build --release 27 | - name: Strip binary 28 | run: | 29 | docker run --rm -i \ 30 | -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} \ 31 | musl-strip -s /home/rust/src/target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/belt 32 | - name: Make package 33 | run: make package arch=${{ matrix.arch }} libc=${{ matrix.libc }} 34 | - uses: actions/upload-artifact@v3 35 | with: 36 | name: "linux-${{ matrix.arch }}-${{ matrix.libc }}" 37 | path: "target/package/*-*.*.*-${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}.tar.gz" 38 | retention-days: 5 39 | 40 | build-macos: 41 | runs-on: macos-11 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: Install Rust 45 | uses: dtolnay/rust-toolchain@stable 46 | with: 47 | toolchain: stable 48 | - name: Build 49 | run: cargo build --release --target x86_64-apple-darwin 50 | - name: Make package 51 | run: make package 52 | - uses: actions/upload-artifact@v3 53 | with: 54 | name: macos-x86_64 55 | path: target/package/*-*.*.*-macos-x86_64.zip 56 | retention-days: 5 57 | 58 | build-windows: 59 | runs-on: windows-2022 60 | steps: 61 | - uses: actions/checkout@v3 62 | - name: Install Rust 63 | uses: dtolnay/rust-toolchain@stable 64 | with: 65 | toolchain: stable 66 | - name: Build 67 | run: cargo build --release --target x86_64-pc-windows-msvc 68 | - name: Make package 69 | run: make package 70 | - uses: actions/upload-artifact@v3 71 | with: 72 | name: windows-x86_64-msvc 73 | path: target/package/*-*-windows-x86_64-msvc.zip 74 | retention-days: 5 75 | 76 | release: 77 | needs: [ build-linux, build-macos, build-windows ] 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v3 81 | - name: Download all artifacts 82 | uses: actions/download-artifact@v3 83 | - name: Move to files around 84 | run: | 85 | mkdir -p target/package 86 | [ -d macos-x86_64 ] && mv macos-x86_64/* target/package || true 87 | [ -d windows-x86_64-msvc ] && mv windows-x86_64-msvc/* target/package || true 88 | [ -d linux-x86_64-musl ] && mv linux-x86_64-musl/* target/package || true 89 | [ -d linux-i686-musl ] && mv linux-i686-musl/* target/package || true 90 | [ -d linux-aarch64-musl ] && mv linux-aarch64-musl/* target/package || true 91 | [ -d linux-armv7-musleabihf ] && mv linux-armv7-musleabihf/* target/package || true 92 | [ -d linux-arm-musleabihf ] && mv linux-arm-musleabihf/* target/package || true 93 | - name: Create checksum file 94 | run: shasum -a 256 target/package/*-*.*.*-*.{tar.gz,zip} > target/package/checksums.txt 95 | - name: List files 96 | run: ls -ahl target/package 97 | - name: Release 98 | uses: softprops/action-gh-release@v1 99 | if: startsWith(github.ref, 'refs/tags/') 100 | with: 101 | generate_release_notes: true 102 | fail_on_unmatched_files: true 103 | files: | 104 | target/package/*-*.*.*-aarch64-unknown-linux-musl.tar.gz 105 | target/package/*-*.*.*-arm-unknown-linux-musleabihf.tar.gz 106 | target/package/*-*.*.*-armv7-unknown-linux-musleabihf.tar.gz 107 | target/package/*-*.*.*-i686-unknown-linux-musl.tar.gz 108 | target/package/*-*.*.*-macos-x86_64.zip 109 | target/package/*-*.*.*-windows-x86_64-msvc.zip 110 | target/package/*-*.*.*-x86_64-unknown-linux-musl.tar.gz 111 | target/package/checksums.txt 112 | 113 | cargo-publish: 114 | needs: release 115 | runs-on: ubuntu-latest 116 | steps: 117 | - uses: actions/checkout@v3 118 | - name: Cargo publish 119 | if: startsWith(github.ref, 'refs/tags/') 120 | run: make publish token=${{ secrets.CARGO_REGISTRY_TOKEN }} 121 | -------------------------------------------------------------------------------- /dateparser/src/timezone.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use chrono::offset::FixedOffset; 3 | 4 | /// Tries to parse `[-+]\d\d` continued by `\d\d`. Return FixedOffset if possible. 5 | /// It can parse RFC 2822 legacy timezones. If offset cannot be determined, -0000 will be returned. 6 | /// 7 | /// The additional `colon` may be used to parse a mandatory or optional `:` between hours and minutes, 8 | /// and should return a valid FixedOffset or `Err` when parsing fails. 9 | pub fn parse(s: &str) -> Result { 10 | let offset = if s.contains(':') { 11 | parse_offset_internal(s, colon_or_space, false)? 12 | } else { 13 | parse_offset_2822(s)? 14 | }; 15 | Ok(FixedOffset::east(offset)) 16 | } 17 | 18 | fn parse_offset_2822(s: &str) -> Result { 19 | // tries to parse legacy time zone names 20 | let upto = s 21 | .as_bytes() 22 | .iter() 23 | .position(|&c| !c.is_ascii_alphabetic()) 24 | .unwrap_or(s.len()); 25 | if upto > 0 { 26 | let name = &s[..upto]; 27 | let offset_hours = |o| Ok(o * 3600); 28 | if equals(name, "gmt") || equals(name, "ut") || equals(name, "utc") { 29 | offset_hours(0) 30 | } else if equals(name, "edt") { 31 | offset_hours(-4) 32 | } else if equals(name, "est") || equals(name, "cdt") { 33 | offset_hours(-5) 34 | } else if equals(name, "cst") || equals(name, "mdt") { 35 | offset_hours(-6) 36 | } else if equals(name, "mst") || equals(name, "pdt") { 37 | offset_hours(-7) 38 | } else if equals(name, "pst") { 39 | offset_hours(-8) 40 | } else { 41 | Ok(0) // recommended by RFC 2822: consume but treat it as -0000 42 | } 43 | } else { 44 | let offset = parse_offset_internal(s, |s| Ok(s), false)?; 45 | Ok(offset) 46 | } 47 | } 48 | 49 | fn parse_offset_internal( 50 | mut s: &str, 51 | mut consume_colon: F, 52 | allow_missing_minutes: bool, 53 | ) -> Result 54 | where 55 | F: FnMut(&str) -> Result<&str>, 56 | { 57 | let err_out_of_range = "input is out of range"; 58 | let err_invalid = "input contains invalid characters"; 59 | let err_too_short = "premature end of input"; 60 | 61 | let digits = |s: &str| -> Result<(u8, u8)> { 62 | let b = s.as_bytes(); 63 | if b.len() < 2 { 64 | Err(anyhow!(err_too_short)) 65 | } else { 66 | Ok((b[0], b[1])) 67 | } 68 | }; 69 | let negative = match s.as_bytes().first() { 70 | Some(&b'+') => false, 71 | Some(&b'-') => true, 72 | Some(_) => return Err(anyhow!(err_invalid)), 73 | None => return Err(anyhow!(err_too_short)), 74 | }; 75 | s = &s[1..]; 76 | 77 | // hours (00--99) 78 | let hours = match digits(s)? { 79 | (h1 @ b'0'..=b'9', h2 @ b'0'..=b'9') => i32::from((h1 - b'0') * 10 + (h2 - b'0')), 80 | _ => return Err(anyhow!(err_invalid)), 81 | }; 82 | s = &s[2..]; 83 | 84 | // colons (and possibly other separators) 85 | s = consume_colon(s)?; 86 | 87 | // minutes (00--59) 88 | // if the next two items are digits then we have to add minutes 89 | let minutes = if let Ok(ds) = digits(s) { 90 | match ds { 91 | (m1 @ b'0'..=b'5', m2 @ b'0'..=b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), 92 | (b'6'..=b'9', b'0'..=b'9') => return Err(anyhow!(err_out_of_range)), 93 | _ => return Err(anyhow!(err_invalid)), 94 | } 95 | } else if allow_missing_minutes { 96 | 0 97 | } else { 98 | return Err(anyhow!(err_too_short)); 99 | }; 100 | 101 | let seconds = hours * 3600 + minutes * 60; 102 | Ok(if negative { -seconds } else { seconds }) 103 | } 104 | 105 | /// Returns true when two slices are equal case-insensitively (in ASCII). 106 | /// Assumes that the `pattern` is already converted to lower case. 107 | fn equals(s: &str, pattern: &str) -> bool { 108 | let mut xs = s.as_bytes().iter().map(|&c| match c { 109 | b'A'..=b'Z' => c + 32, 110 | _ => c, 111 | }); 112 | let mut ys = pattern.as_bytes().iter().cloned(); 113 | loop { 114 | match (xs.next(), ys.next()) { 115 | (None, None) => return true, 116 | (None, _) | (_, None) => return false, 117 | (Some(x), Some(y)) if x != y => return false, 118 | _ => (), 119 | } 120 | } 121 | } 122 | 123 | /// Consumes any number (including zero) of colon or spaces. 124 | fn colon_or_space(s: &str) -> Result<&str> { 125 | Ok(s.trim_start_matches(|c: char| c == ':' || c.is_whitespace())) 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | 132 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 133 | #[cfg_attr(not(target_arch = "wasm32"), test)] 134 | fn parse() { 135 | let test_cases = [ 136 | ("-0800", FixedOffset::west(8 * 3600)), 137 | ("+10:00", FixedOffset::east(10 * 3600)), 138 | ("PST", FixedOffset::west(8 * 3600)), 139 | ("PDT", FixedOffset::west(7 * 3600)), 140 | ("UTC", FixedOffset::west(0)), 141 | ("GMT", FixedOffset::west(0)), 142 | ]; 143 | 144 | for &(input, want) in test_cases.iter() { 145 | assert_eq!(super::parse(input).unwrap(), want, "parse/{}", input) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /belt/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::Config, 3 | opts::{Opts, Subcommands}, 4 | }; 5 | use anyhow::{Error, Result}; 6 | use chrono::prelude::*; 7 | use chrono_tz::Tz; 8 | use colored::*; 9 | use dateparser::DateTimeUtc; 10 | use prettytable::{row, Table}; 11 | use std::io; 12 | 13 | pub struct App<'a, T> { 14 | pub opts: &'a Opts, 15 | pub config: &'a mut Config<'a, T>, 16 | } 17 | 18 | impl<'a, T> App<'a, T> 19 | where 20 | T: io::Write, 21 | { 22 | pub fn new(opts: &'a Opts, config: &'a mut Config<'a, T>) -> Self { 23 | Self { opts, config } 24 | } 25 | 26 | pub fn show_datetime(&mut self) -> Result<()> { 27 | if self.opts.subcommands.is_some() { 28 | // skip showing datetime when there is a subcommand 29 | return Ok(()); 30 | } 31 | 32 | let mut to_show = Utc::now(); 33 | if let Some(time) = &self.opts.time { 34 | to_show = time.parse::()?.0; 35 | } 36 | 37 | let local = to_show.with_timezone(&Local); 38 | let ymd_hms_z = "%Y-%m-%d %H:%M:%S %z"; 39 | let ymd_hm_z = "%Y-%m-%d %H:%M %Z"; 40 | 41 | if self.opts.short { 42 | writeln!(self.config.out, "{}", local.format(ymd_hms_z))?; 43 | } else { 44 | let mut table = Table::new(); 45 | table.set_titles(row!["Zone", "Date & Time"]); 46 | table.add_row(row![ 47 | "Local", 48 | format!("{}\n{}", local.format(ymd_hms_z), local.format("%s")) 49 | ]); 50 | for timezone in &self.config.store.timezones { 51 | let tz: Tz = timezone.parse().map_err(Error::msg)?; 52 | let dtz = to_show.with_timezone(&tz); 53 | table.add_row(row![ 54 | timezone, 55 | format!("{}\n{}", dtz.format(ymd_hms_z), dtz.format(ymd_hm_z)) 56 | ]); 57 | } 58 | table.print(&mut self.config.out)?; 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | pub fn handle_subcommands(&mut self) -> Result<()> { 65 | if let Some(subcommands) = &self.opts.subcommands { 66 | match subcommands { 67 | Subcommands::Config(c) => { 68 | if c.list { 69 | let path = self.config.path(); 70 | writeln!(self.config.out, "{}", path.cyan().bold())?; 71 | self.config.list()?; 72 | } else if c.reset { 73 | self.config.reset()?; 74 | self.config.list()?; 75 | } else if let Some(add) = &c.add { 76 | self.config.add(add)?; 77 | self.config.list()?; 78 | } else if let Some(delete) = &c.delete { 79 | self.config.delete(delete)?; 80 | self.config.list()?; 81 | } 82 | } 83 | } 84 | } 85 | Ok(()) 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | use crate::opts::OptsConfig; 93 | use rand::{thread_rng, Rng}; 94 | use regex::Regex; 95 | use std::{thread::sleep, time::Duration}; 96 | 97 | #[test] 98 | fn test_app_show_datetime() { 99 | let mut opts = Opts::new(); 100 | opts.app = "unit-test".to_string(); 101 | let mut buf = vec![0u8]; 102 | let mut config = match Config::new(&opts.app, &mut buf) { 103 | Ok(config) => config, 104 | Err(_) => { 105 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 106 | Config::new(&opts.app, &mut buf).expect("failed to create config") 107 | } 108 | }; 109 | let timezones = config.store.timezones.clone(); 110 | let num_timezones = timezones.len(); 111 | let mut app = App::new(&opts, &mut config); 112 | 113 | app.show_datetime().expect("failed showing time"); 114 | 115 | let printed = String::from_utf8_lossy(&buf); 116 | for tz in timezones { 117 | assert!(printed.contains(&tz)); 118 | } 119 | let re = Regex::new(r"[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9-+]{5}") 120 | .expect("failed to parse regex"); 121 | assert_eq!(re.find_iter(&printed).count(), num_timezones + 1); // num_timezones + local 122 | } 123 | 124 | #[test] 125 | fn test_app_handle_subcommands() { 126 | let mut opts = Opts::new(); 127 | opts.app = "unit-test".to_string(); 128 | let mut buf = vec![0u8]; 129 | let mut config = match Config::new(&opts.app, &mut buf) { 130 | Ok(config) => config, 131 | Err(_) => { 132 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 133 | Config::new(&opts.app, &mut buf).expect("failed to create config") 134 | } 135 | }; 136 | let timezones = config.store.timezones.clone(); 137 | let mut app = App::new(&opts, &mut config); 138 | 139 | let opts = Opts { 140 | subcommands: Some(Subcommands::Config(OptsConfig { 141 | list: true, 142 | reset: false, 143 | add: None, 144 | delete: None, 145 | })), 146 | time: None, 147 | short: false, 148 | app: opts.app.to_owned(), 149 | }; 150 | app.opts = &opts; 151 | app.handle_subcommands() 152 | .expect("failed handling subcommands"); 153 | 154 | let printed = String::from_utf8_lossy(&buf); 155 | for tz in timezones { 156 | assert!(printed.contains(&tz)); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /dateparser/README.md: -------------------------------------------------------------------------------- 1 | # [dateparser](https://crates.io/crates/dateparser) 2 | 3 | [![Build Status][actions-badge]][actions-url] 4 | [![MIT licensed][mit-badge]][mit-url] 5 | [![Crates.io][cratesio-badge]][cratesio-url] 6 | [![Doc.rs][docrs-badge]][docrs-url] 7 | 8 | [actions-badge]: https://github.com/waltzofpearls/dateparser/workflows/ci/badge.svg 9 | [actions-url]: https://github.com/waltzofpearls/dateparser/actions?query=workflow%3Aci+branch%3Amain 10 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 11 | [mit-url]: https://github.com/waltzofpearls/dateparser/blob/main/LICENSE 12 | [cratesio-badge]: https://img.shields.io/crates/v/dateparser.svg 13 | [cratesio-url]: https://crates.io/crates/dateparser 14 | [docrs-badge]: https://docs.rs/dateparser/badge.svg 15 | [docrs-url]: https://docs.rs/crate/dateparser/ 16 | 17 | A rust library for parsing date strings in commonly used formats. Parsed date will be returned as `chrono`'s 18 | `DateTime`. 19 | 20 | ## Examples 21 | 22 | Add to your `Cargo.toml`: 23 | 24 | ```toml 25 | [dependencies] 26 | dateparser = "0.2.1" 27 | ``` 28 | 29 | And then use `dateparser` in your code: 30 | 31 | ```rust 32 | use dateparser::parse; 33 | use std::error::Error; 34 | 35 | fn main() -> Result<(), Box> { 36 | let parsed = parse("6:15pm")?; 37 | println!("{:#?}", parsed); 38 | Ok(()) 39 | } 40 | ``` 41 | 42 | Or use `str`'s `parse` method: 43 | 44 | ```rust 45 | use dateparser::DateTimeUtc; 46 | use std::error::Error; 47 | 48 | fn main() -> Result<(), Box> { 49 | let parsed = "2021-05-14 18:51 PDT".parse::()?.0; 50 | println!("{:#?}", parsed); 51 | Ok(()) 52 | } 53 | ``` 54 | 55 | Convert returned `DateTime` to pacific time zone datetime with `chrono-tz`: 56 | 57 | ```toml 58 | [dependencies] 59 | chrono-tz = "0.6.3" 60 | dateparser = "0.2.1" 61 | ``` 62 | 63 | ```rust 64 | use chrono_tz::US::Pacific; 65 | use dateparser::DateTimeUtc; 66 | use std::error::Error; 67 | 68 | fn main() -> Result<(), Box> { 69 | let parsed = "Wed, 02 Jun 2021 06:31:39 GMT".parse::()?.0; 70 | println!("{:#?}", parsed.with_timezone(&Pacific)); 71 | Ok(()) 72 | } 73 | ``` 74 | 75 | Parse using a custom timezone offset for a datetime string that doesn't come with a specific timezone: 76 | 77 | ```rust 78 | use dateparser::parse_with_timezone; 79 | use chrono::offset::{Local, Utc}; 80 | use chrono_tz::US::Pacific; 81 | use std::error::Error; 82 | 83 | fn main() -> Result<(), Box> { 84 | let parsed_in_local = parse_with_timezone("6:15pm", &Local)?; 85 | println!("{:#?}", parsed_in_local); 86 | 87 | let parsed_in_utc = parse_with_timezone("6:15pm", &Utc)?; 88 | println!("{:#?}", parsed_in_utc); 89 | 90 | let parsed_in_pacific = parse_with_timezone("6:15pm", &Pacific)?; 91 | println!("{:#?}", parsed_in_pacific); 92 | 93 | Ok(()) 94 | } 95 | ``` 96 | 97 | Parse with a custom timezone offset and default time when those are not given in datetime string. 98 | By default, `parse` and `parse_with_timezone` uses `Utc::now().time()` as `default_time`. 99 | 100 | ```rust 101 | use dateparser::parse_with; 102 | use chrono::{ 103 | offset::{Local, Utc}, 104 | naive::NaiveTime, 105 | }; 106 | use std::error::Error; 107 | 108 | fn main() -> Result<(), Box> { 109 | let parsed_in_local = parse_with("2021-10-09", &Local, NaiveTime::from_hms(0, 0, 0))?; 110 | println!("{:#?}", parsed_in_local); 111 | 112 | let parsed_in_utc = parse_with("2021-10-09", &Utc, NaiveTime::from_hms(0, 0, 0))?; 113 | println!("{:#?}", parsed_in_utc); 114 | 115 | Ok(()) 116 | } 117 | ``` 118 | 119 | ## Accepted date formats 120 | 121 | ```rust 122 | // unix timestamp 123 | "1511648546", 124 | "1620021848429", 125 | "1620024872717915000", 126 | // rfc3339 127 | "2021-05-01T01:17:02.604456Z", 128 | "2017-11-25T22:34:50Z", 129 | // rfc2822 130 | "Wed, 02 Jun 2021 06:31:39 GMT", 131 | // postgres timestamp yyyy-mm-dd hh:mm:ss z 132 | "2019-11-29 08:08-08", 133 | "2019-11-29 08:08:05-08", 134 | "2021-05-02 23:31:36.0741-07", 135 | "2021-05-02 23:31:39.12689-07", 136 | "2019-11-29 08:15:47.624504-08", 137 | "2017-07-19 03:21:51+00:00", 138 | // yyyy-mm-dd hh:mm:ss 139 | "2014-04-26 05:24:37 PM", 140 | "2021-04-30 21:14", 141 | "2021-04-30 21:14:10", 142 | "2021-04-30 21:14:10.052282", 143 | "2014-04-26 17:24:37.123", 144 | "2014-04-26 17:24:37.3186369", 145 | "2012-08-03 18:31:59.257000000", 146 | // yyyy-mm-dd hh:mm:ss z 147 | "2017-11-25 13:31:15 PST", 148 | "2017-11-25 13:31 PST", 149 | "2014-12-16 06:20:00 UTC", 150 | "2014-12-16 06:20:00 GMT", 151 | "2014-04-26 13:13:43 +0800", 152 | "2014-04-26 13:13:44 +09:00", 153 | "2012-08-03 18:31:59.257000000 +0000", 154 | "2015-09-30 18:48:56.35272715 UTC", 155 | // yyyy-mm-dd 156 | "2021-02-21", 157 | // yyyy-mm-dd z 158 | "2021-02-21 PST", 159 | "2021-02-21 UTC", 160 | "2020-07-20+08:00", 161 | // hh:mm:ss 162 | "01:06:06", 163 | "4:00pm", 164 | "6:00 AM", 165 | // hh:mm:ss z 166 | "01:06:06 PST", 167 | "4:00pm PST", 168 | "6:00 AM PST", 169 | "6:00pm UTC", 170 | // Mon dd hh:mm:ss 171 | "May 6 at 9:24 PM", 172 | "May 27 02:45:27", 173 | // Mon dd, yyyy, hh:mm:ss 174 | "May 8, 2009 5:57:51 PM", 175 | "September 17, 2012 10:09am", 176 | "September 17, 2012, 10:10:09", 177 | // Mon dd, yyyy hh:mm:ss z 178 | "May 02, 2021 15:51:31 UTC", 179 | "May 02, 2021 15:51 UTC", 180 | "May 26, 2021, 12:49 AM PDT", 181 | "September 17, 2012 at 10:09am PST", 182 | // yyyy-mon-dd 183 | "2021-Feb-21", 184 | // Mon dd, yyyy 185 | "May 25, 2021", 186 | "oct 7, 1970", 187 | "oct 7, 70", 188 | "oct. 7, 1970", 189 | "oct. 7, 70", 190 | "October 7, 1970", 191 | // dd Mon yyyy hh:mm:ss 192 | "12 Feb 2006, 19:17", 193 | "12 Feb 2006 19:17", 194 | "14 May 2019 19:11:40.164", 195 | // dd Mon yyyy 196 | "7 oct 70", 197 | "7 oct 1970", 198 | "03 February 2013", 199 | "1 July 2013", 200 | // mm/dd/yyyy hh:mm:ss 201 | "4/8/2014 22:05", 202 | "04/08/2014 22:05", 203 | "4/8/14 22:05", 204 | "04/2/2014 03:00:51", 205 | "8/8/1965 12:00:00 AM", 206 | "8/8/1965 01:00:01 PM", 207 | "8/8/1965 01:00 PM", 208 | "8/8/1965 1:00 PM", 209 | "8/8/1965 12:00 AM", 210 | "4/02/2014 03:00:51", 211 | "03/19/2012 10:11:59", 212 | "03/19/2012 10:11:59.3186369", 213 | // mm/dd/yyyy 214 | "3/31/2014", 215 | "03/31/2014", 216 | "08/21/71", 217 | "8/1/71", 218 | // yyyy/mm/dd hh:mm:ss 219 | "2014/4/8 22:05", 220 | "2014/04/08 22:05", 221 | "2014/04/2 03:00:51", 222 | "2014/4/02 03:00:51", 223 | "2012/03/19 10:11:59", 224 | "2012/03/19 10:11:59.3186369", 225 | // yyyy/mm/dd 226 | "2014/3/31", 227 | "2014/03/31", 228 | // mm.dd.yyyy 229 | "3.31.2014", 230 | "03.31.2014", 231 | "08.21.71", 232 | // yyyy.mm.dd 233 | "2014.03.30", 234 | "2014.03", 235 | // yymmdd hh:mm:ss mysql log 236 | "171113 14:14:20", 237 | // chinese yyyy mm dd hh mm ss 238 | "2014年04月08日11时25分18秒", 239 | // chinese yyyy mm dd 240 | "2014年04月08日", 241 | ``` 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [dateparser](https://crates.io/crates/dateparser) 2 | 3 | [![Build Status][actions-badge]][actions-url] 4 | [![MIT licensed][mit-badge]][mit-url] 5 | [![Crates.io][cratesio-badge]][cratesio-url] 6 | [![Doc.rs][docrs-badge]][docrs-url] 7 | 8 | [actions-badge]: https://github.com/waltzofpearls/dateparser/workflows/ci/badge.svg 9 | [actions-url]: https://github.com/waltzofpearls/dateparser/actions?query=workflow%3Aci+branch%3Amain 10 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 11 | [mit-url]: https://github.com/waltzofpearls/dateparser/blob/main/LICENSE 12 | [cratesio-badge]: https://img.shields.io/crates/v/dateparser.svg 13 | [cratesio-url]: https://crates.io/crates/dateparser 14 | [docrs-badge]: https://docs.rs/dateparser/badge.svg 15 | [docrs-url]: https://docs.rs/crate/dateparser/ 16 | 17 | Parse dates in commonly used string formats with Rust. 18 | 19 | This repo contains 2 cargo workspaces: 20 | 21 | - [dateparser](./dateparser): Rust crate for parsing date strings in commonly used formats. 22 | - [belt](./belt): Command-line tool that can display a given time in a list of selected time zones. 23 | It also serves as an example showcasing how you could use dateparser in your project. 24 | 25 | ## [`dateparser`](./dateparser) crate 26 | 27 | ```rust 28 | use dateparser::parse; 29 | use std::error::Error; 30 | 31 | fn main() -> Result<(), Box> { 32 | let parsed = parse("6:15pm")?; 33 | println!("{:#?}", parsed); 34 | Ok(()) 35 | } 36 | ``` 37 | 38 | Will parse the input `6:15pm` and print parsed date and time in UTC time zone as `2023-03-26T01:15:00Z`. 39 | More about this crate on [Docs.rs][1] and in [examples][2] folder 40 | 41 | [1]: https://docs.rs/dateparser/latest/dateparser 42 | [2]: ./dateparser/examples 43 | 44 | #### Accepted date formats 45 | 46 | ```rust 47 | // unix timestamp 48 | "1511648546", 49 | "1620021848429", 50 | "1620024872717915000", 51 | // rfc3339 52 | "2021-05-01T01:17:02.604456Z", 53 | "2017-11-25T22:34:50Z", 54 | // rfc2822 55 | "Wed, 02 Jun 2021 06:31:39 GMT", 56 | // postgres timestamp yyyy-mm-dd hh:mm:ss z 57 | "2019-11-29 08:08-08", 58 | "2019-11-29 08:08:05-08", 59 | "2021-05-02 23:31:36.0741-07", 60 | "2021-05-02 23:31:39.12689-07", 61 | "2019-11-29 08:15:47.624504-08", 62 | "2017-07-19 03:21:51+00:00", 63 | // yyyy-mm-dd hh:mm:ss 64 | "2014-04-26 05:24:37 PM", 65 | "2021-04-30 21:14", 66 | "2021-04-30 21:14:10", 67 | "2021-04-30 21:14:10.052282", 68 | "2014-04-26 17:24:37.123", 69 | "2014-04-26 17:24:37.3186369", 70 | "2012-08-03 18:31:59.257000000", 71 | // yyyy-mm-dd hh:mm:ss z 72 | "2017-11-25 13:31:15 PST", 73 | "2017-11-25 13:31 PST", 74 | "2014-12-16 06:20:00 UTC", 75 | "2014-12-16 06:20:00 GMT", 76 | "2014-04-26 13:13:43 +0800", 77 | "2014-04-26 13:13:44 +09:00", 78 | "2012-08-03 18:31:59.257000000 +0000", 79 | "2015-09-30 18:48:56.35272715 UTC", 80 | // yyyy-mm-dd 81 | "2021-02-21", 82 | // yyyy-mm-dd z 83 | "2021-02-21 PST", 84 | "2021-02-21 UTC", 85 | "2020-07-20+08:00", 86 | // hh:mm:ss 87 | "01:06:06", 88 | "4:00pm", 89 | "6:00 AM", 90 | // hh:mm:ss z 91 | "01:06:06 PST", 92 | "4:00pm PST", 93 | "6:00 AM PST", 94 | "6:00pm UTC", 95 | // Mon dd hh:mm:ss 96 | "May 6 at 9:24 PM", 97 | "May 27 02:45:27", 98 | // Mon dd, yyyy, hh:mm:ss 99 | "May 8, 2009 5:57:51 PM", 100 | "September 17, 2012 10:09am", 101 | "September 17, 2012, 10:10:09", 102 | // Mon dd, yyyy hh:mm:ss z 103 | "May 02, 2021 15:51:31 UTC", 104 | "May 02, 2021 15:51 UTC", 105 | "May 26, 2021, 12:49 AM PDT", 106 | "September 17, 2012 at 10:09am PST", 107 | // yyyy-mon-dd 108 | "2021-Feb-21", 109 | // Mon dd, yyyy 110 | "May 25, 2021", 111 | "oct 7, 1970", 112 | "oct 7, 70", 113 | "oct. 7, 1970", 114 | "oct. 7, 70", 115 | "October 7, 1970", 116 | // dd Mon yyyy hh:mm:ss 117 | "12 Feb 2006, 19:17", 118 | "12 Feb 2006 19:17", 119 | "14 May 2019 19:11:40.164", 120 | // dd Mon yyyy 121 | "7 oct 70", 122 | "7 oct 1970", 123 | "03 February 2013", 124 | "1 July 2013", 125 | // mm/dd/yyyy hh:mm:ss 126 | "4/8/2014 22:05", 127 | "04/08/2014 22:05", 128 | "4/8/14 22:05", 129 | "04/2/2014 03:00:51", 130 | "8/8/1965 12:00:00 AM", 131 | "8/8/1965 01:00:01 PM", 132 | "8/8/1965 01:00 PM", 133 | "8/8/1965 1:00 PM", 134 | "8/8/1965 12:00 AM", 135 | "4/02/2014 03:00:51", 136 | "03/19/2012 10:11:59", 137 | "03/19/2012 10:11:59.3186369", 138 | // mm/dd/yyyy 139 | "3/31/2014", 140 | "03/31/2014", 141 | "08/21/71", 142 | "8/1/71", 143 | // yyyy/mm/dd hh:mm:ss 144 | "2014/4/8 22:05", 145 | "2014/04/08 22:05", 146 | "2014/04/2 03:00:51", 147 | "2014/4/02 03:00:51", 148 | "2012/03/19 10:11:59", 149 | "2012/03/19 10:11:59.3186369", 150 | // yyyy/mm/dd 151 | "2014/3/31", 152 | "2014/03/31", 153 | // mm.dd.yyyy 154 | "3.31.2014", 155 | "03.31.2014", 156 | "08.21.71", 157 | // yyyy.mm.dd 158 | "2014.03.30", 159 | "2014.03", 160 | // yymmdd hh:mm:ss mysql log 161 | "171113 14:14:20", 162 | // chinese yyyy mm dd hh mm ss 163 | "2014年04月08日11时25分18秒", 164 | // chinese yyyy mm dd 165 | "2014年04月08日", 166 | ``` 167 | 168 | ## [`belt`](./belt) CLI tool 169 | 170 | Run `belt` to parse a given date: 171 | 172 | ```shell 173 | $> belt 'MAY 12, 2021 16:44 UTC' 174 | +-------------------+---------------------------+ 175 | | Zone | Date & Time | 176 | +===================+===========================+ 177 | | Local | 2021-05-12 09:44:00 -0700 | 178 | | | 1620837840 | 179 | +-------------------+---------------------------+ 180 | | UTC | 2021-05-12 16:44:00 +0000 | 181 | | | 2021-05-12 16:44 UTC | 182 | +-------------------+---------------------------+ 183 | | America/Vancouver | 2021-05-12 09:44:00 -0700 | 184 | | | 2021-05-12 09:44 PDT | 185 | +-------------------+---------------------------+ 186 | | America/New_York | 2021-05-12 12:44:00 -0400 | 187 | | | 2021-05-12 12:44 EDT | 188 | +-------------------+---------------------------+ 189 | | Europe/London | 2021-05-12 17:44:00 +0100 | 190 | | | 2021-05-12 17:44 BST | 191 | +-------------------+---------------------------+ 192 | ``` 193 | 194 | #### Installation 195 | 196 | MacOS Homebrew or Linuxbrew: 197 | 198 | ```shell 199 | brew tap waltzofpearls/belt 200 | brew install belt 201 | ``` 202 | 203 | ## How to make a new release 204 | 205 | List files that need to be updated with new version number: 206 | 207 | ```shell 208 | make show-version-files 209 | ``` 210 | 211 | It will output something like this: 212 | 213 | ```shell 214 | ./dateparser/Cargo.toml:3:version = "0.1.5" 215 | ./dateparser/README.md:26:dateparser = "0.1.5" 216 | ./dateparser/README.md:60:dateparser = "0.1.5" 217 | ./belt/Cargo.toml:3:version = "0.1.5" 218 | ``` 219 | 220 | Next, automatically bump the version with `make bump-version` or manually update verion numbers in 221 | those listed files. When auto incrementing version with `make bump-version`, it will only bump the 222 | patch version, for example, 0.1.5 will become 0.1.6. Automatic version bump will create a git branch, 223 | commit and push the changes. You will need to create a pull request from GitHub to merge those changes 224 | from the git branch that's automatically created. 225 | 226 | **NOTE**: if those files with version numbers are manually edited, then you will need to run `cargo update` 227 | to update `dateparser` and `belt` versions in the `Cargo.lock` file, and then git commit and push those 228 | changes to a git branch, and create a pull request from that branch. 229 | 230 | Once the pull request is merged and those files are updated, run the following command to tag a new 231 | version with git and push the new tag to GitHub. This will trigger a build and release workflow run 232 | in GitHub Actions: 233 | 234 | ```shell 235 | make release 236 | ``` 237 | -------------------------------------------------------------------------------- /belt/src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use chrono::prelude::*; 3 | use chrono_tz::{OffsetComponents, OffsetName, Tz}; 4 | use colored::*; 5 | use directories::ProjectDirs; 6 | use prettytable::{row, Table}; 7 | use serde::{Deserialize, Serialize}; 8 | use std::io; 9 | 10 | pub struct Config<'a, T> { 11 | pub store: Store, 12 | pub out: &'a mut T, 13 | pub app: String, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | pub struct Store { 18 | pub timezones: Vec, 19 | } 20 | 21 | impl ::std::default::Default for Store { 22 | fn default() -> Self { 23 | Self { 24 | timezones: vec![ 25 | "UTC".to_string(), 26 | "America/Vancouver".to_string(), 27 | "America/New_York".to_string(), 28 | "Europe/London".to_string(), 29 | ], 30 | } 31 | } 32 | } 33 | 34 | impl<'a, T> Config<'a, T> 35 | where 36 | T: io::Write, 37 | { 38 | pub fn new(app: &str, out: &'a mut T) -> Result { 39 | let store: Store = confy::load(app, None)?; 40 | Ok(Self { 41 | store, 42 | out, 43 | app: app.to_string(), 44 | }) 45 | } 46 | 47 | pub fn path(&self) -> String { 48 | ProjectDirs::from("rs", "", &self.app) 49 | .and_then(|project| project.config_dir().to_str().map(|s: &str| s.to_string())) 50 | .map(|s| format!("{}/{}.toml", s, self.app)) 51 | .unwrap_or_default() 52 | } 53 | 54 | pub fn list(&mut self) -> Result<()> { 55 | let now_utc = Local::now().naive_utc(); 56 | let mut table = Table::new(); 57 | table.set_titles(row![l -> "Zone", l -> "Abbr.", r -> "Offset"]); 58 | for timezone in &self.store.timezones { 59 | let tz: Tz = timezone.parse().map_err(Error::msg)?; 60 | let offset = tz.offset_from_utc_datetime(&now_utc); 61 | table.add_row(row![ 62 | l -> timezone, 63 | l -> offset.abbreviation(), 64 | r -> match offset.base_utc_offset().num_hours() { 65 | 0 => "0 hour ".to_string(), 66 | hours => format!("{} hours", hours), 67 | } 68 | ]); 69 | } 70 | table.print(self.out)?; 71 | Ok(()) 72 | } 73 | 74 | pub fn add(&mut self, to_add: &str) -> Result<()> { 75 | let result = to_add.parse::().and_then(|_| { 76 | self.store.timezones.push(to_add.to_string()); 77 | confy::store(&self.app, None, &self.store).map_err(|err| format!("{}", err)) 78 | }); 79 | 80 | match result { 81 | Ok(_) => writeln!( 82 | self.out, 83 | "{}", 84 | format!("Added '{}' to config.", to_add).green().bold() 85 | )?, 86 | Err(err) => writeln!( 87 | self.out, 88 | "{}", 89 | format!("Could not add time zone: {}.", err).red().bold() 90 | )?, 91 | }; 92 | Ok(()) 93 | } 94 | 95 | pub fn delete(&mut self, to_delete: &str) -> Result<()> { 96 | self.store.timezones.retain(|tz| tz != to_delete); 97 | match confy::store(&self.app, None, &self.store) { 98 | Ok(_) => writeln!( 99 | self.out, 100 | "{}", 101 | format!("Deleted '{}' from config.", to_delete) 102 | .green() 103 | .bold() 104 | )?, 105 | Err(err) => writeln!( 106 | self.out, 107 | "{}", 108 | format!("Could not delete time zone: {}.", err).red().bold() 109 | )?, 110 | }; 111 | Ok(()) 112 | } 113 | 114 | pub fn reset(&mut self) -> Result<()> { 115 | self.store.timezones = Store::default().timezones; 116 | match confy::store(&self.app, None, &self.store) { 117 | Ok(_) => writeln!( 118 | self.out, 119 | "{}", 120 | "Config has been reset to default.".green().bold() 121 | )?, 122 | Err(err) => writeln!( 123 | self.out, 124 | "{}", 125 | format!("Could not reset time zones: {}", err).red().bold() 126 | )?, 127 | }; 128 | Ok(()) 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | use rand::{thread_rng, Rng}; 136 | use std::{thread::sleep, time::Duration}; 137 | 138 | #[test] 139 | fn test_config_path() { 140 | let mut buf = vec![0u8]; 141 | let app = "unit-test"; 142 | let config = match Config::new(app, &mut buf) { 143 | Ok(config) => config, 144 | Err(_) => { 145 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 146 | Config::new(app, &mut buf).expect("failed to create config") 147 | } 148 | }; 149 | let path = config.path(); 150 | if !path.contains(app) { 151 | panic!("path [{}] does not contain [unit-test]", path); 152 | } 153 | } 154 | 155 | #[test] 156 | fn test_config_list() { 157 | let mut buf = vec![0u8]; 158 | let app = "unit-test"; 159 | let mut config = match Config::new(app, &mut buf) { 160 | Ok(config) => config, 161 | Err(_) => { 162 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 163 | Config::new(app, &mut buf).expect("failed to create config") 164 | } 165 | }; 166 | config.reset().expect("failed to reset config store"); 167 | config.out.clear(); 168 | 169 | config.list().expect("failed to list configured timezons"); 170 | let listed = String::from_utf8_lossy(&buf); 171 | for tz in Store::default().timezones { 172 | assert!(listed.contains(&tz)); 173 | } 174 | } 175 | 176 | #[test] 177 | fn test_config_add() { 178 | let mut buf = vec![0u8]; 179 | let app = "unit-test"; 180 | let mut config = match Config::new(app, &mut buf) { 181 | Ok(config) => config, 182 | Err(_) => { 183 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 184 | Config::new(app, &mut buf).expect("failed to create config") 185 | } 186 | }; 187 | config.reset().expect("failed to reset config store"); 188 | config 189 | .add("Europe/Berlin") 190 | .expect("failed to add Europe/Berlin"); 191 | config.out.clear(); 192 | 193 | config.list().expect("failed to list configured timezons"); 194 | let listed = String::from_utf8_lossy(&buf); 195 | assert!(listed.contains("Europe/Berlin")); 196 | } 197 | 198 | #[test] 199 | fn test_config_delete() { 200 | let mut buf = vec![0u8]; 201 | let app = "unit-test"; 202 | let mut config = match Config::new(app, &mut buf) { 203 | Ok(config) => config, 204 | Err(_) => { 205 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 206 | Config::new(app, &mut buf).expect("failed to create config") 207 | } 208 | }; 209 | config.reset().expect("failed to reset config store"); 210 | config.delete("UTC").expect("failed to delete UTC"); 211 | config.out.clear(); 212 | 213 | config.list().expect("failed to list configured timezons"); 214 | let listed = String::from_utf8_lossy(&buf); 215 | assert!(!listed.contains("UTC")); 216 | } 217 | 218 | #[test] 219 | fn test_config_reset() { 220 | let mut buf = vec![0u8]; 221 | let app = "unit-test"; 222 | let mut config = match Config::new(app, &mut buf) { 223 | Ok(config) => config, 224 | Err(_) => { 225 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 226 | Config::new(app, &mut buf).expect("failed to create config") 227 | } 228 | }; 229 | config.reset().expect("failed to reset config store"); 230 | config 231 | .add("Europe/Berlin") 232 | .expect("failed to add Europe/Berlin"); 233 | config.delete("UTC").expect("failed to delete UTC"); 234 | config.reset().expect("failed to reset config store"); 235 | config.out.clear(); 236 | 237 | config.list().expect("failed to list configured timezons"); 238 | let listed = String::from_utf8_lossy(&buf); 239 | for tz in Store::default().timezones { 240 | assert!(listed.contains(&tz)); 241 | } 242 | assert!(!listed.contains("Europe/Berlin")); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anes" 31 | version = "0.1.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle" 51 | version = "1.0.4" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 54 | 55 | [[package]] 56 | name = "anstyle-parse" 57 | version = "0.2.2" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 60 | dependencies = [ 61 | "utf8parse", 62 | ] 63 | 64 | [[package]] 65 | name = "anstyle-query" 66 | version = "1.0.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 69 | dependencies = [ 70 | "windows-sys", 71 | ] 72 | 73 | [[package]] 74 | name = "anstyle-wincon" 75 | version = "3.0.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 78 | dependencies = [ 79 | "anstyle", 80 | "windows-sys", 81 | ] 82 | 83 | [[package]] 84 | name = "anyhow" 85 | version = "1.0.75" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 88 | 89 | [[package]] 90 | name = "autocfg" 91 | version = "1.1.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 94 | 95 | [[package]] 96 | name = "belt" 97 | version = "0.2.1" 98 | dependencies = [ 99 | "anyhow", 100 | "chrono", 101 | "chrono-tz", 102 | "clap", 103 | "colored", 104 | "confy", 105 | "dateparser", 106 | "directories 5.0.1", 107 | "prettytable-rs", 108 | "rand", 109 | "regex", 110 | "serde", 111 | ] 112 | 113 | [[package]] 114 | name = "bitflags" 115 | version = "1.3.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 118 | 119 | [[package]] 120 | name = "bitflags" 121 | version = "2.4.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 124 | 125 | [[package]] 126 | name = "bumpalo" 127 | version = "3.14.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 130 | 131 | [[package]] 132 | name = "cast" 133 | version = "0.3.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 136 | 137 | [[package]] 138 | name = "cc" 139 | version = "1.0.84" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" 142 | dependencies = [ 143 | "libc", 144 | ] 145 | 146 | [[package]] 147 | name = "cfg-if" 148 | version = "1.0.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 151 | 152 | [[package]] 153 | name = "chrono" 154 | version = "0.4.31" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 157 | dependencies = [ 158 | "android-tzdata", 159 | "iana-time-zone", 160 | "js-sys", 161 | "num-traits", 162 | "wasm-bindgen", 163 | "windows-targets", 164 | ] 165 | 166 | [[package]] 167 | name = "chrono-tz" 168 | version = "0.8.4" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "e23185c0e21df6ed832a12e2bda87c7d1def6842881fb634a8511ced741b0d76" 171 | dependencies = [ 172 | "chrono", 173 | "chrono-tz-build", 174 | "phf", 175 | ] 176 | 177 | [[package]] 178 | name = "chrono-tz-build" 179 | version = "0.2.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" 182 | dependencies = [ 183 | "parse-zoneinfo", 184 | "phf", 185 | "phf_codegen", 186 | ] 187 | 188 | [[package]] 189 | name = "ciborium" 190 | version = "0.2.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" 193 | dependencies = [ 194 | "ciborium-io", 195 | "ciborium-ll", 196 | "serde", 197 | ] 198 | 199 | [[package]] 200 | name = "ciborium-io" 201 | version = "0.2.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" 204 | 205 | [[package]] 206 | name = "ciborium-ll" 207 | version = "0.2.1" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" 210 | dependencies = [ 211 | "ciborium-io", 212 | "half", 213 | ] 214 | 215 | [[package]] 216 | name = "clap" 217 | version = "4.4.8" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" 220 | dependencies = [ 221 | "clap_builder", 222 | "clap_derive", 223 | ] 224 | 225 | [[package]] 226 | name = "clap_builder" 227 | version = "4.4.8" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" 230 | dependencies = [ 231 | "anstream", 232 | "anstyle", 233 | "clap_lex", 234 | "strsim", 235 | ] 236 | 237 | [[package]] 238 | name = "clap_derive" 239 | version = "4.4.7" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 242 | dependencies = [ 243 | "heck", 244 | "proc-macro2", 245 | "quote", 246 | "syn", 247 | ] 248 | 249 | [[package]] 250 | name = "clap_lex" 251 | version = "0.6.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 254 | 255 | [[package]] 256 | name = "colorchoice" 257 | version = "1.0.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 260 | 261 | [[package]] 262 | name = "colored" 263 | version = "2.0.4" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" 266 | dependencies = [ 267 | "is-terminal", 268 | "lazy_static", 269 | "windows-sys", 270 | ] 271 | 272 | [[package]] 273 | name = "confy" 274 | version = "0.5.1" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" 277 | dependencies = [ 278 | "directories 4.0.1", 279 | "serde", 280 | "thiserror", 281 | "toml", 282 | ] 283 | 284 | [[package]] 285 | name = "console_error_panic_hook" 286 | version = "0.1.7" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 289 | dependencies = [ 290 | "cfg-if", 291 | "wasm-bindgen", 292 | ] 293 | 294 | [[package]] 295 | name = "core-foundation-sys" 296 | version = "0.8.4" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 299 | 300 | [[package]] 301 | name = "criterion" 302 | version = "0.5.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 305 | dependencies = [ 306 | "anes", 307 | "cast", 308 | "ciborium", 309 | "clap", 310 | "criterion-plot", 311 | "is-terminal", 312 | "itertools", 313 | "num-traits", 314 | "once_cell", 315 | "oorandom", 316 | "regex", 317 | "serde", 318 | "serde_derive", 319 | "serde_json", 320 | "tinytemplate", 321 | "walkdir", 322 | ] 323 | 324 | [[package]] 325 | name = "criterion-plot" 326 | version = "0.5.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 329 | dependencies = [ 330 | "cast", 331 | "itertools", 332 | ] 333 | 334 | [[package]] 335 | name = "csv" 336 | version = "1.3.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" 339 | dependencies = [ 340 | "csv-core", 341 | "itoa", 342 | "ryu", 343 | "serde", 344 | ] 345 | 346 | [[package]] 347 | name = "csv-core" 348 | version = "0.1.11" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 351 | dependencies = [ 352 | "memchr", 353 | ] 354 | 355 | [[package]] 356 | name = "dateparser" 357 | version = "0.2.1" 358 | dependencies = [ 359 | "anyhow", 360 | "chrono", 361 | "chrono-tz", 362 | "criterion", 363 | "js-sys", 364 | "regex", 365 | "wasm-bindgen", 366 | "wasm-bindgen-test", 367 | ] 368 | 369 | [[package]] 370 | name = "directories" 371 | version = "4.0.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" 374 | dependencies = [ 375 | "dirs-sys 0.3.7", 376 | ] 377 | 378 | [[package]] 379 | name = "directories" 380 | version = "5.0.1" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 383 | dependencies = [ 384 | "dirs-sys 0.4.1", 385 | ] 386 | 387 | [[package]] 388 | name = "dirs-next" 389 | version = "2.0.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 392 | dependencies = [ 393 | "cfg-if", 394 | "dirs-sys-next", 395 | ] 396 | 397 | [[package]] 398 | name = "dirs-sys" 399 | version = "0.3.7" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 402 | dependencies = [ 403 | "libc", 404 | "redox_users", 405 | "winapi", 406 | ] 407 | 408 | [[package]] 409 | name = "dirs-sys" 410 | version = "0.4.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 413 | dependencies = [ 414 | "libc", 415 | "option-ext", 416 | "redox_users", 417 | "windows-sys", 418 | ] 419 | 420 | [[package]] 421 | name = "dirs-sys-next" 422 | version = "0.1.2" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 425 | dependencies = [ 426 | "libc", 427 | "redox_users", 428 | "winapi", 429 | ] 430 | 431 | [[package]] 432 | name = "either" 433 | version = "1.9.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 436 | 437 | [[package]] 438 | name = "encode_unicode" 439 | version = "1.0.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 442 | 443 | [[package]] 444 | name = "errno" 445 | version = "0.3.6" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" 448 | dependencies = [ 449 | "libc", 450 | "windows-sys", 451 | ] 452 | 453 | [[package]] 454 | name = "getrandom" 455 | version = "0.2.11" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 458 | dependencies = [ 459 | "cfg-if", 460 | "libc", 461 | "wasi", 462 | ] 463 | 464 | [[package]] 465 | name = "half" 466 | version = "1.8.2" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 469 | 470 | [[package]] 471 | name = "heck" 472 | version = "0.4.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 475 | 476 | [[package]] 477 | name = "hermit-abi" 478 | version = "0.3.3" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 481 | 482 | [[package]] 483 | name = "iana-time-zone" 484 | version = "0.1.58" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 487 | dependencies = [ 488 | "android_system_properties", 489 | "core-foundation-sys", 490 | "iana-time-zone-haiku", 491 | "js-sys", 492 | "wasm-bindgen", 493 | "windows-core", 494 | ] 495 | 496 | [[package]] 497 | name = "iana-time-zone-haiku" 498 | version = "0.1.2" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 501 | dependencies = [ 502 | "cc", 503 | ] 504 | 505 | [[package]] 506 | name = "is-terminal" 507 | version = "0.4.9" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 510 | dependencies = [ 511 | "hermit-abi", 512 | "rustix", 513 | "windows-sys", 514 | ] 515 | 516 | [[package]] 517 | name = "itertools" 518 | version = "0.10.5" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 521 | dependencies = [ 522 | "either", 523 | ] 524 | 525 | [[package]] 526 | name = "itoa" 527 | version = "1.0.9" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 530 | 531 | [[package]] 532 | name = "js-sys" 533 | version = "0.3.65" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 536 | dependencies = [ 537 | "wasm-bindgen", 538 | ] 539 | 540 | [[package]] 541 | name = "lazy_static" 542 | version = "1.4.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 545 | 546 | [[package]] 547 | name = "libc" 548 | version = "0.2.150" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 551 | 552 | [[package]] 553 | name = "libredox" 554 | version = "0.0.1" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 557 | dependencies = [ 558 | "bitflags 2.4.1", 559 | "libc", 560 | "redox_syscall", 561 | ] 562 | 563 | [[package]] 564 | name = "linux-raw-sys" 565 | version = "0.4.11" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" 568 | 569 | [[package]] 570 | name = "log" 571 | version = "0.4.20" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 574 | 575 | [[package]] 576 | name = "memchr" 577 | version = "2.6.4" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 580 | 581 | [[package]] 582 | name = "num-traits" 583 | version = "0.2.17" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 586 | dependencies = [ 587 | "autocfg", 588 | ] 589 | 590 | [[package]] 591 | name = "once_cell" 592 | version = "1.18.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 595 | 596 | [[package]] 597 | name = "oorandom" 598 | version = "11.1.3" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 601 | 602 | [[package]] 603 | name = "option-ext" 604 | version = "0.2.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 607 | 608 | [[package]] 609 | name = "parse-zoneinfo" 610 | version = "0.3.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" 613 | dependencies = [ 614 | "regex", 615 | ] 616 | 617 | [[package]] 618 | name = "phf" 619 | version = "0.11.2" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 622 | dependencies = [ 623 | "phf_shared", 624 | ] 625 | 626 | [[package]] 627 | name = "phf_codegen" 628 | version = "0.11.2" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" 631 | dependencies = [ 632 | "phf_generator", 633 | "phf_shared", 634 | ] 635 | 636 | [[package]] 637 | name = "phf_generator" 638 | version = "0.11.2" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 641 | dependencies = [ 642 | "phf_shared", 643 | "rand", 644 | ] 645 | 646 | [[package]] 647 | name = "phf_shared" 648 | version = "0.11.2" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 651 | dependencies = [ 652 | "siphasher", 653 | ] 654 | 655 | [[package]] 656 | name = "ppv-lite86" 657 | version = "0.2.17" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 660 | 661 | [[package]] 662 | name = "prettytable-rs" 663 | version = "0.10.0" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" 666 | dependencies = [ 667 | "csv", 668 | "encode_unicode", 669 | "is-terminal", 670 | "lazy_static", 671 | "term", 672 | "unicode-width", 673 | ] 674 | 675 | [[package]] 676 | name = "proc-macro2" 677 | version = "1.0.69" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 680 | dependencies = [ 681 | "unicode-ident", 682 | ] 683 | 684 | [[package]] 685 | name = "quote" 686 | version = "1.0.33" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 689 | dependencies = [ 690 | "proc-macro2", 691 | ] 692 | 693 | [[package]] 694 | name = "rand" 695 | version = "0.8.5" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 698 | dependencies = [ 699 | "libc", 700 | "rand_chacha", 701 | "rand_core", 702 | ] 703 | 704 | [[package]] 705 | name = "rand_chacha" 706 | version = "0.3.1" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 709 | dependencies = [ 710 | "ppv-lite86", 711 | "rand_core", 712 | ] 713 | 714 | [[package]] 715 | name = "rand_core" 716 | version = "0.6.4" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 719 | dependencies = [ 720 | "getrandom", 721 | ] 722 | 723 | [[package]] 724 | name = "redox_syscall" 725 | version = "0.4.1" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 728 | dependencies = [ 729 | "bitflags 1.3.2", 730 | ] 731 | 732 | [[package]] 733 | name = "redox_users" 734 | version = "0.4.4" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 737 | dependencies = [ 738 | "getrandom", 739 | "libredox", 740 | "thiserror", 741 | ] 742 | 743 | [[package]] 744 | name = "regex" 745 | version = "1.10.2" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 748 | dependencies = [ 749 | "aho-corasick", 750 | "memchr", 751 | "regex-automata", 752 | "regex-syntax", 753 | ] 754 | 755 | [[package]] 756 | name = "regex-automata" 757 | version = "0.4.3" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 760 | dependencies = [ 761 | "aho-corasick", 762 | "memchr", 763 | "regex-syntax", 764 | ] 765 | 766 | [[package]] 767 | name = "regex-syntax" 768 | version = "0.8.2" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 771 | 772 | [[package]] 773 | name = "rustix" 774 | version = "0.38.21" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" 777 | dependencies = [ 778 | "bitflags 2.4.1", 779 | "errno", 780 | "libc", 781 | "linux-raw-sys", 782 | "windows-sys", 783 | ] 784 | 785 | [[package]] 786 | name = "rustversion" 787 | version = "1.0.14" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 790 | 791 | [[package]] 792 | name = "ryu" 793 | version = "1.0.15" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 796 | 797 | [[package]] 798 | name = "same-file" 799 | version = "1.0.6" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 802 | dependencies = [ 803 | "winapi-util", 804 | ] 805 | 806 | [[package]] 807 | name = "scoped-tls" 808 | version = "1.0.1" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 811 | 812 | [[package]] 813 | name = "serde" 814 | version = "1.0.192" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" 817 | dependencies = [ 818 | "serde_derive", 819 | ] 820 | 821 | [[package]] 822 | name = "serde_derive" 823 | version = "1.0.192" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" 826 | dependencies = [ 827 | "proc-macro2", 828 | "quote", 829 | "syn", 830 | ] 831 | 832 | [[package]] 833 | name = "serde_json" 834 | version = "1.0.108" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 837 | dependencies = [ 838 | "itoa", 839 | "ryu", 840 | "serde", 841 | ] 842 | 843 | [[package]] 844 | name = "siphasher" 845 | version = "0.3.11" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 848 | 849 | [[package]] 850 | name = "strsim" 851 | version = "0.10.0" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 854 | 855 | [[package]] 856 | name = "syn" 857 | version = "2.0.39" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 860 | dependencies = [ 861 | "proc-macro2", 862 | "quote", 863 | "unicode-ident", 864 | ] 865 | 866 | [[package]] 867 | name = "term" 868 | version = "0.7.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 871 | dependencies = [ 872 | "dirs-next", 873 | "rustversion", 874 | "winapi", 875 | ] 876 | 877 | [[package]] 878 | name = "thiserror" 879 | version = "1.0.50" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 882 | dependencies = [ 883 | "thiserror-impl", 884 | ] 885 | 886 | [[package]] 887 | name = "thiserror-impl" 888 | version = "1.0.50" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 891 | dependencies = [ 892 | "proc-macro2", 893 | "quote", 894 | "syn", 895 | ] 896 | 897 | [[package]] 898 | name = "tinytemplate" 899 | version = "1.2.1" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 902 | dependencies = [ 903 | "serde", 904 | "serde_json", 905 | ] 906 | 907 | [[package]] 908 | name = "toml" 909 | version = "0.5.11" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 912 | dependencies = [ 913 | "serde", 914 | ] 915 | 916 | [[package]] 917 | name = "unicode-ident" 918 | version = "1.0.12" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 921 | 922 | [[package]] 923 | name = "unicode-width" 924 | version = "0.1.11" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 927 | 928 | [[package]] 929 | name = "utf8parse" 930 | version = "0.2.1" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 933 | 934 | [[package]] 935 | name = "walkdir" 936 | version = "2.4.0" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 939 | dependencies = [ 940 | "same-file", 941 | "winapi-util", 942 | ] 943 | 944 | [[package]] 945 | name = "wasi" 946 | version = "0.11.0+wasi-snapshot-preview1" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 949 | 950 | [[package]] 951 | name = "wasm-bindgen" 952 | version = "0.2.88" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 955 | dependencies = [ 956 | "cfg-if", 957 | "wasm-bindgen-macro", 958 | ] 959 | 960 | [[package]] 961 | name = "wasm-bindgen-backend" 962 | version = "0.2.88" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 965 | dependencies = [ 966 | "bumpalo", 967 | "log", 968 | "once_cell", 969 | "proc-macro2", 970 | "quote", 971 | "syn", 972 | "wasm-bindgen-shared", 973 | ] 974 | 975 | [[package]] 976 | name = "wasm-bindgen-futures" 977 | version = "0.4.38" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" 980 | dependencies = [ 981 | "cfg-if", 982 | "js-sys", 983 | "wasm-bindgen", 984 | "web-sys", 985 | ] 986 | 987 | [[package]] 988 | name = "wasm-bindgen-macro" 989 | version = "0.2.88" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 992 | dependencies = [ 993 | "quote", 994 | "wasm-bindgen-macro-support", 995 | ] 996 | 997 | [[package]] 998 | name = "wasm-bindgen-macro-support" 999 | version = "0.2.88" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 1002 | dependencies = [ 1003 | "proc-macro2", 1004 | "quote", 1005 | "syn", 1006 | "wasm-bindgen-backend", 1007 | "wasm-bindgen-shared", 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "wasm-bindgen-shared" 1012 | version = "0.2.88" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 1015 | 1016 | [[package]] 1017 | name = "wasm-bindgen-test" 1018 | version = "0.3.38" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d" 1021 | dependencies = [ 1022 | "console_error_panic_hook", 1023 | "js-sys", 1024 | "scoped-tls", 1025 | "wasm-bindgen", 1026 | "wasm-bindgen-futures", 1027 | "wasm-bindgen-test-macro", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "wasm-bindgen-test-macro" 1032 | version = "0.3.38" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735" 1035 | dependencies = [ 1036 | "proc-macro2", 1037 | "quote", 1038 | "syn", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "web-sys" 1043 | version = "0.3.65" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" 1046 | dependencies = [ 1047 | "js-sys", 1048 | "wasm-bindgen", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "winapi" 1053 | version = "0.3.9" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1056 | dependencies = [ 1057 | "winapi-i686-pc-windows-gnu", 1058 | "winapi-x86_64-pc-windows-gnu", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "winapi-i686-pc-windows-gnu" 1063 | version = "0.4.0" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1066 | 1067 | [[package]] 1068 | name = "winapi-util" 1069 | version = "0.1.6" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1072 | dependencies = [ 1073 | "winapi", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "winapi-x86_64-pc-windows-gnu" 1078 | version = "0.4.0" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1081 | 1082 | [[package]] 1083 | name = "windows-core" 1084 | version = "0.51.1" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 1087 | dependencies = [ 1088 | "windows-targets", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "windows-sys" 1093 | version = "0.48.0" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1096 | dependencies = [ 1097 | "windows-targets", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "windows-targets" 1102 | version = "0.48.5" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1105 | dependencies = [ 1106 | "windows_aarch64_gnullvm", 1107 | "windows_aarch64_msvc", 1108 | "windows_i686_gnu", 1109 | "windows_i686_msvc", 1110 | "windows_x86_64_gnu", 1111 | "windows_x86_64_gnullvm", 1112 | "windows_x86_64_msvc", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "windows_aarch64_gnullvm" 1117 | version = "0.48.5" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1120 | 1121 | [[package]] 1122 | name = "windows_aarch64_msvc" 1123 | version = "0.48.5" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1126 | 1127 | [[package]] 1128 | name = "windows_i686_gnu" 1129 | version = "0.48.5" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1132 | 1133 | [[package]] 1134 | name = "windows_i686_msvc" 1135 | version = "0.48.5" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1138 | 1139 | [[package]] 1140 | name = "windows_x86_64_gnu" 1141 | version = "0.48.5" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1144 | 1145 | [[package]] 1146 | name = "windows_x86_64_gnullvm" 1147 | version = "0.48.5" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1150 | 1151 | [[package]] 1152 | name = "windows_x86_64_msvc" 1153 | version = "0.48.5" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1156 | -------------------------------------------------------------------------------- /dateparser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | //! A rust library for parsing date strings in commonly used formats. Parsed date will be returned 3 | //! as `chrono`'s `DateTime`. 4 | //! 5 | //! # Quick Start 6 | //! 7 | //! ``` 8 | //! use chrono::prelude::*; 9 | //! use dateparser::parse; 10 | //! use std::error::Error; 11 | //! 12 | //! fn main() -> Result<(), Box> { 13 | //! assert_eq!( 14 | //! parse("6:15pm UTC")?, 15 | //! Utc::now().date().and_time( 16 | //! NaiveTime::from_hms(18, 15, 0), 17 | //! ).unwrap(), 18 | //! ); 19 | //! Ok(()) 20 | //! } 21 | //! ``` 22 | //! 23 | //! Use `str`'s `parse` method: 24 | //! 25 | //! ``` 26 | //! use chrono::prelude::*; 27 | //! use dateparser::DateTimeUtc; 28 | //! use std::error::Error; 29 | //! 30 | //! fn main() -> Result<(), Box> { 31 | //! assert_eq!( 32 | //! "2021-05-14 18:51 PDT".parse::()?.0, 33 | //! Utc.ymd(2021, 5, 15).and_hms(1, 51, 0), 34 | //! ); 35 | //! Ok(()) 36 | //! } 37 | //! ``` 38 | //! 39 | //! Parse using a custom timezone offset for a datetime string that doesn't come with a specific 40 | //! timezone: 41 | //! 42 | //! ``` 43 | //! use dateparser::parse_with_timezone; 44 | //! use chrono::offset::Utc; 45 | //! use std::error::Error; 46 | //! 47 | //! fn main() -> Result<(), Box> { 48 | //! let parsed_in_utc = parse_with_timezone("6:15pm", &Utc)?; 49 | //! assert_eq!( 50 | //! parsed_in_utc, 51 | //! Utc::now().date().and_hms(18, 15, 0), 52 | //! ); 53 | //! Ok(()) 54 | //! } 55 | //! ``` 56 | //! 57 | //! ## Accepted date formats 58 | //! 59 | //! ``` 60 | //! use dateparser::DateTimeUtc; 61 | //! 62 | //! let accepted = vec![ 63 | //! // unix timestamp 64 | //! "1511648546", 65 | //! "1620021848429", 66 | //! "1620024872717915000", 67 | //! // rfc3339 68 | //! "2021-05-01T01:17:02.604456Z", 69 | //! "2017-11-25T22:34:50Z", 70 | //! // rfc2822 71 | //! "Wed, 02 Jun 2021 06:31:39 GMT", 72 | //! // postgres timestamp yyyy-mm-dd hh:mm:ss z 73 | //! "2019-11-29 08:08-08", 74 | //! "2019-11-29 08:08:05-08", 75 | //! "2021-05-02 23:31:36.0741-07", 76 | //! "2021-05-02 23:31:39.12689-07", 77 | //! "2019-11-29 08:15:47.624504-08", 78 | //! "2017-07-19 03:21:51+00:00", 79 | //! // yyyy-mm-dd hh:mm:ss 80 | //! "2014-04-26 05:24:37 PM", 81 | //! "2021-04-30 21:14", 82 | //! "2021-04-30 21:14:10", 83 | //! "2021-04-30 21:14:10.052282", 84 | //! "2014-04-26 17:24:37.123", 85 | //! "2014-04-26 17:24:37.3186369", 86 | //! "2012-08-03 18:31:59.257000000", 87 | //! // yyyy-mm-dd hh:mm:ss z 88 | //! "2017-11-25 13:31:15 PST", 89 | //! "2017-11-25 13:31 PST", 90 | //! "2014-12-16 06:20:00 UTC", 91 | //! "2014-12-16 06:20:00 GMT", 92 | //! "2014-04-26 13:13:43 +0800", 93 | //! "2014-04-26 13:13:44 +09:00", 94 | //! "2012-08-03 18:31:59.257000000 +0000", 95 | //! "2015-09-30 18:48:56.35272715 UTC", 96 | //! // yyyy-mm-dd 97 | //! "2021-02-21", 98 | //! // yyyy-mm-dd z 99 | //! "2021-02-21 PST", 100 | //! "2021-02-21 UTC", 101 | //! "2020-07-20+08:00", 102 | //! // hh:mm:ss 103 | //! "01:06:06", 104 | //! "4:00pm", 105 | //! "6:00 AM", 106 | //! // hh:mm:ss z 107 | //! "01:06:06 PST", 108 | //! "4:00pm PST", 109 | //! "6:00 AM PST", 110 | //! "6:00pm UTC", 111 | //! // Mon dd hh:mm:ss 112 | //! "May 6 at 9:24 PM", 113 | //! "May 27 02:45:27", 114 | //! // Mon dd, yyyy, hh:mm:ss 115 | //! "May 8, 2009 5:57:51 PM", 116 | //! "September 17, 2012 10:09am", 117 | //! "September 17, 2012, 10:10:09", 118 | //! // Mon dd, yyyy hh:mm:ss z 119 | //! "May 02, 2021 15:51:31 UTC", 120 | //! "May 02, 2021 15:51 UTC", 121 | //! "May 26, 2021, 12:49 AM PDT", 122 | //! "September 17, 2012 at 10:09am PST", 123 | //! // yyyy-mon-dd 124 | //! "2021-Feb-21", 125 | //! // Mon dd, yyyy 126 | //! "May 25, 2021", 127 | //! "oct 7, 1970", 128 | //! "oct 7, 70", 129 | //! "oct. 7, 1970", 130 | //! "oct. 7, 70", 131 | //! "October 7, 1970", 132 | //! // dd Mon yyyy hh:mm:ss 133 | //! "12 Feb 2006, 19:17", 134 | //! "12 Feb 2006 19:17", 135 | //! "14 May 2019 19:11:40.164", 136 | //! // dd Mon yyyy 137 | //! "7 oct 70", 138 | //! "7 oct 1970", 139 | //! "03 February 2013", 140 | //! "1 July 2013", 141 | //! // mm/dd/yyyy hh:mm:ss 142 | //! "4/8/2014 22:05", 143 | //! "04/08/2014 22:05", 144 | //! "4/8/14 22:05", 145 | //! "04/2/2014 03:00:51", 146 | //! "8/8/1965 12:00:00 AM", 147 | //! "8/8/1965 01:00:01 PM", 148 | //! "8/8/1965 01:00 PM", 149 | //! "8/8/1965 1:00 PM", 150 | //! "8/8/1965 12:00 AM", 151 | //! "4/02/2014 03:00:51", 152 | //! "03/19/2012 10:11:59", 153 | //! "03/19/2012 10:11:59.3186369", 154 | //! // mm/dd/yyyy 155 | //! "3/31/2014", 156 | //! "03/31/2014", 157 | //! "08/21/71", 158 | //! "8/1/71", 159 | //! // yyyy/mm/dd hh:mm:ss 160 | //! "2014/4/8 22:05", 161 | //! "2014/04/08 22:05", 162 | //! "2014/04/2 03:00:51", 163 | //! "2014/4/02 03:00:51", 164 | //! "2012/03/19 10:11:59", 165 | //! "2012/03/19 10:11:59.3186369", 166 | //! // yyyy/mm/dd 167 | //! "2014/3/31", 168 | //! "2014/03/31", 169 | //! // mm.dd.yyyy 170 | //! "3.31.2014", 171 | //! "03.31.2014", 172 | //! "08.21.71", 173 | //! // yyyy.mm.dd 174 | //! "2014.03.29", 175 | //! "2014.03", 176 | //! // yymmdd hh:mm:ss mysql log 177 | //! "171113 14:14:20", 178 | //! // chinese yyyy mm dd hh mm ss 179 | //! "2014年04月08日11时25分18秒", 180 | //! // chinese yyyy mm dd 181 | //! "2014年04月08日", 182 | //! ]; 183 | //! 184 | //! for date_str in accepted { 185 | //! let result = date_str.parse::(); 186 | //! assert!(result.is_ok()) 187 | //! } 188 | //! ``` 189 | 190 | /// Datetime string parser 191 | /// 192 | /// ``` 193 | /// use chrono::prelude::*; 194 | /// use dateparser::datetime::Parse; 195 | /// use std::error::Error; 196 | /// 197 | /// fn main() -> Result<(), Box> { 198 | /// let parse_with_local = Parse::new(&Local, None); 199 | /// assert_eq!( 200 | /// parse_with_local.parse("2021-06-05 06:19 PM")?, 201 | /// Local.ymd(2021, 6, 5).and_hms(18, 19, 0).with_timezone(&Utc), 202 | /// ); 203 | /// 204 | /// let parse_with_utc = Parse::new(&Utc, None); 205 | /// assert_eq!( 206 | /// parse_with_utc.parse("2021-06-05 06:19 PM")?, 207 | /// Utc.ymd(2021, 6, 5).and_hms(18, 19, 0), 208 | /// ); 209 | /// 210 | /// Ok(()) 211 | /// } 212 | /// ``` 213 | pub mod datetime; 214 | 215 | /// Timezone offset string parser 216 | /// 217 | /// ``` 218 | /// use chrono::prelude::*; 219 | /// use dateparser::timezone::parse; 220 | /// use std::error::Error; 221 | /// 222 | /// fn main() -> Result<(), Box> { 223 | /// assert_eq!(parse("-0800")?, FixedOffset::west(8 * 3600)); 224 | /// assert_eq!(parse("+10:00")?, FixedOffset::east(10 * 3600)); 225 | /// assert_eq!(parse("PST")?, FixedOffset::west(8 * 3600)); 226 | /// assert_eq!(parse("PDT")?, FixedOffset::west(7 * 3600)); 227 | /// assert_eq!(parse("UTC")?, FixedOffset::west(0)); 228 | /// assert_eq!(parse("GMT")?, FixedOffset::west(0)); 229 | /// 230 | /// Ok(()) 231 | /// } 232 | /// ``` 233 | pub mod timezone; 234 | 235 | use crate::datetime::Parse; 236 | use anyhow::{Error, Result}; 237 | use chrono::prelude::*; 238 | 239 | /// DateTimeUtc is an alias for `chrono`'s `DateTime`. It implements `std::str::FromStr`'s 240 | /// `from_str` method, and it makes `str`'s `parse` method to understand the accepted date formats 241 | /// from this crate. 242 | /// 243 | /// ``` 244 | /// use dateparser::DateTimeUtc; 245 | /// 246 | /// // parsed is DateTimeUTC and parsed.0 is chrono's DateTime 247 | /// match "May 02, 2021 15:51:31 UTC".parse::() { 248 | /// Ok(parsed) => println!("PARSED into UTC datetime {:?}", parsed.0), 249 | /// Err(err) => println!("ERROR from parsing datetime string: {}", err) 250 | /// } 251 | /// ``` 252 | #[derive(Clone, Debug)] 253 | pub struct DateTimeUtc(pub DateTime); 254 | 255 | impl std::str::FromStr for DateTimeUtc { 256 | type Err = Error; 257 | 258 | fn from_str(s: &str) -> Result { 259 | parse(s).map(DateTimeUtc) 260 | } 261 | } 262 | 263 | /// This function tries to recognize the input datetime string with a list of accepted formats. 264 | /// When timezone is not provided, this function assumes it's a [`chrono::Local`] datetime. For 265 | /// custom timezone, use [`parse_with_timezone()`] instead.If all options are exhausted, 266 | /// [`parse()`] will return an error to let the caller know that no formats were matched. 267 | /// 268 | /// ``` 269 | /// use dateparser::parse; 270 | /// use chrono::offset::{Local, Utc}; 271 | /// use chrono_tz::US::Pacific; 272 | /// 273 | /// let parsed = parse("6:15pm").unwrap(); 274 | /// 275 | /// assert_eq!( 276 | /// parsed, 277 | /// Local::now().date().and_hms(18, 15, 0).with_timezone(&Utc), 278 | /// ); 279 | /// 280 | /// assert_eq!( 281 | /// parsed.with_timezone(&Pacific), 282 | /// Local::now().date().and_hms(18, 15, 0).with_timezone(&Utc).with_timezone(&Pacific), 283 | /// ); 284 | /// ``` 285 | pub fn parse(input: &str) -> Result> { 286 | Parse::new(&Local, None).parse(input) 287 | } 288 | 289 | /// Similar to [`parse()`], this function takes a datetime string and a custom [`chrono::TimeZone`], 290 | /// and tries to parse the datetime string. When timezone is not given in the string, this function 291 | /// will assume and parse the datetime by the custom timezone provided in this function's arguments. 292 | /// 293 | /// ``` 294 | /// use dateparser::parse_with_timezone; 295 | /// use chrono::offset::{Local, Utc}; 296 | /// use chrono_tz::US::Pacific; 297 | /// 298 | /// let parsed_in_local = parse_with_timezone("6:15pm", &Local).unwrap(); 299 | /// assert_eq!( 300 | /// parsed_in_local, 301 | /// Local::now().date().and_hms(18, 15, 0).with_timezone(&Utc), 302 | /// ); 303 | /// 304 | /// let parsed_in_utc = parse_with_timezone("6:15pm", &Utc).unwrap(); 305 | /// assert_eq!( 306 | /// parsed_in_utc, 307 | /// Utc::now().date().and_hms(18, 15, 0), 308 | /// ); 309 | /// 310 | /// let parsed_in_pacific = parse_with_timezone("6:15pm", &Pacific).unwrap(); 311 | /// assert_eq!( 312 | /// parsed_in_pacific, 313 | /// Utc::now().with_timezone(&Pacific).date().and_hms(18, 15, 0).with_timezone(&Utc), 314 | /// ); 315 | /// ``` 316 | pub fn parse_with_timezone(input: &str, tz: &Tz2) -> Result> { 317 | Parse::new(tz, None).parse(input) 318 | } 319 | 320 | /// Similar to [`parse()`] and [`parse_with_timezone()`], this function takes a datetime string, a 321 | /// custom [`chrono::TimeZone`] and a default naive time. In addition to assuming timezone when 322 | /// it's not given in datetime string, this function also use provided default naive time in parsed 323 | /// [`chrono::DateTime`]. 324 | /// 325 | /// ``` 326 | /// use dateparser::parse_with; 327 | /// use chrono::prelude::*; 328 | /// 329 | /// let utc_now = Utc::now().time().trunc_subsecs(0); 330 | /// let local_now = Local::now().time().trunc_subsecs(0); 331 | /// let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 332 | /// let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 333 | /// 334 | /// let parsed_with_local_now = parse_with("2021-10-09", &Local, local_now); 335 | /// let parsed_with_local_midnight = parse_with("2021-10-09", &Local, midnight_naive); 336 | /// let parsed_with_local_before_midnight = parse_with("2021-10-09", &Local, before_midnight_naive); 337 | /// let parsed_with_utc_now = parse_with("2021-10-09", &Utc, utc_now); 338 | /// let parsed_with_utc_midnight = parse_with("2021-10-09", &Utc, midnight_naive); 339 | /// 340 | /// assert_eq!( 341 | /// parsed_with_local_now.unwrap(), 342 | /// Local.ymd(2021, 10, 9).and_time(local_now).unwrap().with_timezone(&Utc), 343 | /// "parsed_with_local_now" 344 | /// ); 345 | /// assert_eq!( 346 | /// parsed_with_local_midnight.unwrap(), 347 | /// Local.ymd(2021, 10, 9).and_time(midnight_naive).unwrap().with_timezone(&Utc), 348 | /// "parsed_with_local_midnight" 349 | /// ); 350 | /// assert_eq!( 351 | /// parsed_with_local_before_midnight.unwrap(), 352 | /// Local.ymd(2021, 10, 9).and_time(before_midnight_naive).unwrap().with_timezone(&Utc), 353 | /// "parsed_with_local_before_midnight" 354 | /// ); 355 | /// assert_eq!( 356 | /// parsed_with_utc_now.unwrap(), 357 | /// Utc.ymd(2021, 10, 9).and_time(utc_now).unwrap(), 358 | /// "parsed_with_utc_now" 359 | /// ); 360 | /// assert_eq!( 361 | /// parsed_with_utc_midnight.unwrap(), 362 | /// Utc.ymd(2021, 10, 9).and_hms(0, 0, 0), 363 | /// "parsed_with_utc_midnight" 364 | /// ); 365 | /// ``` 366 | pub fn parse_with( 367 | input: &str, 368 | tz: &Tz2, 369 | default_time: NaiveTime, 370 | ) -> Result> { 371 | Parse::new(tz, Some(default_time)).parse(input) 372 | } 373 | 374 | #[cfg(test)] 375 | mod tests { 376 | use super::*; 377 | 378 | #[derive(Clone, Copy)] 379 | enum Trunc { 380 | Seconds, 381 | None, 382 | } 383 | 384 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 385 | #[cfg_attr(not(target_arch = "wasm32"), test)] 386 | fn parse_in_local() { 387 | let test_cases = vec![ 388 | ( 389 | "unix_timestamp", 390 | "1511648546", 391 | Utc.ymd(2017, 11, 25).and_hms(22, 22, 26), 392 | Trunc::None, 393 | ), 394 | ( 395 | "rfc3339", 396 | "2017-11-25T22:34:50Z", 397 | Utc.ymd(2017, 11, 25).and_hms(22, 34, 50), 398 | Trunc::None, 399 | ), 400 | ( 401 | "rfc2822", 402 | "Wed, 02 Jun 2021 06:31:39 GMT", 403 | Utc.ymd(2021, 6, 2).and_hms(6, 31, 39), 404 | Trunc::None, 405 | ), 406 | ( 407 | "postgres_timestamp", 408 | "2019-11-29 08:08:05-08", 409 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 5), 410 | Trunc::None, 411 | ), 412 | ( 413 | "ymd_hms", 414 | "2021-04-30 21:14:10", 415 | Local 416 | .ymd(2021, 4, 30) 417 | .and_hms(21, 14, 10) 418 | .with_timezone(&Utc), 419 | Trunc::None, 420 | ), 421 | ( 422 | "ymd_hms_z", 423 | "2017-11-25 13:31:15 PST", 424 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 15), 425 | Trunc::None, 426 | ), 427 | ( 428 | "ymd", 429 | "2021-02-21", 430 | Local 431 | .ymd(2021, 2, 21) 432 | .and_time(Local::now().time()) 433 | .unwrap() 434 | .with_timezone(&Utc), 435 | Trunc::Seconds, 436 | ), 437 | ( 438 | "ymd_z", 439 | "2021-02-21 PST", 440 | FixedOffset::west(8 * 3600) 441 | .ymd(2021, 2, 21) 442 | .and_time( 443 | Utc::now() 444 | .with_timezone(&FixedOffset::west(8 * 3600)) 445 | .time(), 446 | ) 447 | .unwrap() 448 | .with_timezone(&Utc), 449 | Trunc::Seconds, 450 | ), 451 | ( 452 | "hms", 453 | "4:00pm", 454 | Local::now() 455 | .date() 456 | .and_time(NaiveTime::from_hms(16, 0, 0)) 457 | .unwrap() 458 | .with_timezone(&Utc), 459 | Trunc::None, 460 | ), 461 | ( 462 | "hms_z", 463 | "6:00 AM PST", 464 | Utc::now() 465 | .with_timezone(&FixedOffset::west(8 * 3600)) 466 | .date() 467 | .and_time(NaiveTime::from_hms(6, 0, 0)) 468 | .unwrap() 469 | .with_timezone(&Utc), 470 | Trunc::None, 471 | ), 472 | ( 473 | "month_ymd", 474 | "2021-Feb-21", 475 | Local 476 | .ymd(2021, 2, 21) 477 | .and_time(Local::now().time()) 478 | .unwrap() 479 | .with_timezone(&Utc), 480 | Trunc::Seconds, 481 | ), 482 | ( 483 | "month_md_hms", 484 | "May 27 02:45:27", 485 | Local 486 | .ymd(Local::now().year(), 5, 27) 487 | .and_hms(2, 45, 27) 488 | .with_timezone(&Utc), 489 | Trunc::None, 490 | ), 491 | ( 492 | "month_mdy_hms", 493 | "May 8, 2009 5:57:51 PM", 494 | Local 495 | .ymd(2009, 5, 8) 496 | .and_hms(17, 57, 51) 497 | .with_timezone(&Utc), 498 | Trunc::None, 499 | ), 500 | ( 501 | "month_mdy_hms_z", 502 | "May 02, 2021 15:51 UTC", 503 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 0), 504 | Trunc::None, 505 | ), 506 | ( 507 | "month_mdy", 508 | "May 25, 2021", 509 | Local 510 | .ymd(2021, 5, 25) 511 | .and_time(Local::now().time()) 512 | .unwrap() 513 | .with_timezone(&Utc), 514 | Trunc::Seconds, 515 | ), 516 | ( 517 | "month_dmy_hms", 518 | "14 May 2019 19:11:40.164", 519 | Local 520 | .ymd(2019, 5, 14) 521 | .and_hms_milli(19, 11, 40, 164) 522 | .with_timezone(&Utc), 523 | Trunc::None, 524 | ), 525 | ( 526 | "month_dmy", 527 | "1 July 2013", 528 | Local 529 | .ymd(2013, 7, 1) 530 | .and_time(Local::now().time()) 531 | .unwrap() 532 | .with_timezone(&Utc), 533 | Trunc::Seconds, 534 | ), 535 | ( 536 | "slash_mdy_hms", 537 | "03/19/2012 10:11:59", 538 | Local 539 | .ymd(2012, 3, 19) 540 | .and_hms(10, 11, 59) 541 | .with_timezone(&Utc), 542 | Trunc::None, 543 | ), 544 | ( 545 | "slash_mdy", 546 | "08/21/71", 547 | Local 548 | .ymd(1971, 8, 21) 549 | .and_time(Local::now().time()) 550 | .unwrap() 551 | .with_timezone(&Utc), 552 | Trunc::Seconds, 553 | ), 554 | ( 555 | "slash_ymd_hms", 556 | "2012/03/19 10:11:59", 557 | Local 558 | .ymd(2012, 3, 19) 559 | .and_hms(10, 11, 59) 560 | .with_timezone(&Utc), 561 | Trunc::None, 562 | ), 563 | ( 564 | "slash_ymd", 565 | "2014/3/31", 566 | Local 567 | .ymd(2014, 3, 31) 568 | .and_time(Local::now().time()) 569 | .unwrap() 570 | .with_timezone(&Utc), 571 | Trunc::Seconds, 572 | ), 573 | ( 574 | "dot_mdy_or_ymd", 575 | "2014.03.29", 576 | Local 577 | .ymd(2014, 3, 29) 578 | .and_time(Local::now().time()) 579 | .unwrap() 580 | .with_timezone(&Utc), 581 | Trunc::Seconds, 582 | ), 583 | ( 584 | "mysql_log_timestamp", 585 | "171113 14:14:20", 586 | Local 587 | .ymd(2017, 11, 13) 588 | .and_hms(14, 14, 20) 589 | .with_timezone(&Utc), 590 | Trunc::None, 591 | ), 592 | ( 593 | "chinese_ymd_hms", 594 | "2014年04月08日11时25分18秒", 595 | Local 596 | .ymd(2014, 4, 8) 597 | .and_hms(11, 25, 18) 598 | .with_timezone(&Utc), 599 | Trunc::None, 600 | ), 601 | ( 602 | "chinese_ymd", 603 | "2014年04月08日", 604 | Local 605 | .ymd(2014, 4, 8) 606 | .and_time(Local::now().time()) 607 | .unwrap() 608 | .with_timezone(&Utc), 609 | Trunc::Seconds, 610 | ), 611 | ]; 612 | 613 | for &(test, input, want, trunc) in test_cases.iter() { 614 | match trunc { 615 | Trunc::None => { 616 | assert_eq!( 617 | super::parse(input).unwrap(), 618 | want, 619 | "parse_in_local/{}/{}", 620 | test, 621 | input 622 | ) 623 | } 624 | Trunc::Seconds => assert_eq!( 625 | super::parse(input) 626 | .unwrap() 627 | .trunc_subsecs(0) 628 | .with_second(0) 629 | .unwrap(), 630 | want.trunc_subsecs(0).with_second(0).unwrap(), 631 | "parse_in_local/{}/{}", 632 | test, 633 | input 634 | ), 635 | }; 636 | } 637 | } 638 | 639 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 640 | #[cfg_attr(not(target_arch = "wasm32"), test)] 641 | fn parse_with_timezone_in_utc() { 642 | let test_cases = vec![ 643 | ( 644 | "unix_timestamp", 645 | "1511648546", 646 | Utc.ymd(2017, 11, 25).and_hms(22, 22, 26), 647 | Trunc::None, 648 | ), 649 | ( 650 | "rfc3339", 651 | "2017-11-25T22:34:50Z", 652 | Utc.ymd(2017, 11, 25).and_hms(22, 34, 50), 653 | Trunc::None, 654 | ), 655 | ( 656 | "rfc2822", 657 | "Wed, 02 Jun 2021 06:31:39 GMT", 658 | Utc.ymd(2021, 6, 2).and_hms(6, 31, 39), 659 | Trunc::None, 660 | ), 661 | ( 662 | "postgres_timestamp", 663 | "2019-11-29 08:08:05-08", 664 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 5), 665 | Trunc::None, 666 | ), 667 | ( 668 | "ymd_hms", 669 | "2021-04-30 21:14:10", 670 | Utc.ymd(2021, 4, 30).and_hms(21, 14, 10), 671 | Trunc::None, 672 | ), 673 | ( 674 | "ymd_hms_z", 675 | "2017-11-25 13:31:15 PST", 676 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 15), 677 | Trunc::None, 678 | ), 679 | ( 680 | "ymd", 681 | "2021-02-21", 682 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(), 683 | Trunc::Seconds, 684 | ), 685 | ( 686 | "ymd_z", 687 | "2021-02-21 PST", 688 | FixedOffset::west(8 * 3600) 689 | .ymd(2021, 2, 21) 690 | .and_time( 691 | Utc::now() 692 | .with_timezone(&FixedOffset::west(8 * 3600)) 693 | .time(), 694 | ) 695 | .unwrap() 696 | .with_timezone(&Utc), 697 | Trunc::Seconds, 698 | ), 699 | ( 700 | "hms", 701 | "4:00pm", 702 | Utc::now() 703 | .date() 704 | .and_time(NaiveTime::from_hms(16, 0, 0)) 705 | .unwrap(), 706 | Trunc::None, 707 | ), 708 | ( 709 | "hms_z", 710 | "6:00 AM PST", 711 | FixedOffset::west(8 * 3600) 712 | .from_local_date( 713 | &Utc::now() 714 | .with_timezone(&FixedOffset::west(8 * 3600)) 715 | .date() 716 | .naive_local(), 717 | ) 718 | .and_time(NaiveTime::from_hms(6, 0, 0)) 719 | .unwrap() 720 | .with_timezone(&Utc), 721 | Trunc::None, 722 | ), 723 | ( 724 | "month_ymd", 725 | "2021-Feb-21", 726 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(), 727 | Trunc::Seconds, 728 | ), 729 | ( 730 | "month_md_hms", 731 | "May 27 02:45:27", 732 | Utc.ymd(Utc::now().year(), 5, 27).and_hms(2, 45, 27), 733 | Trunc::None, 734 | ), 735 | ( 736 | "month_mdy_hms", 737 | "May 8, 2009 5:57:51 PM", 738 | Utc.ymd(2009, 5, 8).and_hms(17, 57, 51), 739 | Trunc::None, 740 | ), 741 | ( 742 | "month_mdy_hms_z", 743 | "May 02, 2021 15:51 UTC", 744 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 0), 745 | Trunc::None, 746 | ), 747 | ( 748 | "month_mdy", 749 | "May 25, 2021", 750 | Utc.ymd(2021, 5, 25).and_time(Utc::now().time()).unwrap(), 751 | Trunc::Seconds, 752 | ), 753 | ( 754 | "month_dmy_hms", 755 | "14 May 2019 19:11:40.164", 756 | Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164), 757 | Trunc::None, 758 | ), 759 | ( 760 | "month_dmy", 761 | "1 July 2013", 762 | Utc.ymd(2013, 7, 1).and_time(Utc::now().time()).unwrap(), 763 | Trunc::Seconds, 764 | ), 765 | ( 766 | "slash_mdy_hms", 767 | "03/19/2012 10:11:59", 768 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 769 | Trunc::None, 770 | ), 771 | ( 772 | "slash_mdy", 773 | "08/21/71", 774 | Utc.ymd(1971, 8, 21).and_time(Utc::now().time()).unwrap(), 775 | Trunc::Seconds, 776 | ), 777 | ( 778 | "slash_ymd_hms", 779 | "2012/03/19 10:11:59", 780 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 781 | Trunc::None, 782 | ), 783 | ( 784 | "slash_ymd", 785 | "2014/3/31", 786 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()).unwrap(), 787 | Trunc::Seconds, 788 | ), 789 | ( 790 | "dot_mdy_or_ymd", 791 | "2014.03.29", 792 | Utc.ymd(2014, 3, 29).and_time(Utc::now().time()).unwrap(), 793 | Trunc::Seconds, 794 | ), 795 | ( 796 | "mysql_log_timestamp", 797 | "171113 14:14:20", 798 | Utc.ymd(2017, 11, 13).and_hms(14, 14, 20), 799 | Trunc::None, 800 | ), 801 | ( 802 | "chinese_ymd_hms", 803 | "2014年04月08日11时25分18秒", 804 | Utc.ymd(2014, 4, 8).and_hms(11, 25, 18), 805 | Trunc::None, 806 | ), 807 | ( 808 | "chinese_ymd", 809 | "2014年04月08日", 810 | Utc.ymd(2014, 4, 8).and_time(Utc::now().time()).unwrap(), 811 | Trunc::Seconds, 812 | ), 813 | ]; 814 | 815 | for &(test, input, want, trunc) in test_cases.iter() { 816 | match trunc { 817 | Trunc::None => { 818 | assert_eq!( 819 | super::parse_with_timezone(input, &Utc).unwrap(), 820 | want, 821 | "parse_with_timezone_in_utc/{}/{}", 822 | test, 823 | input 824 | ) 825 | } 826 | Trunc::Seconds => assert_eq!( 827 | super::parse_with_timezone(input, &Utc) 828 | .unwrap() 829 | .trunc_subsecs(0) 830 | .with_second(0) 831 | .unwrap(), 832 | want.trunc_subsecs(0).with_second(0).unwrap(), 833 | "parse_with_timezone_in_utc/{}/{}", 834 | test, 835 | input 836 | ), 837 | }; 838 | } 839 | } 840 | 841 | // test parse_with() with various timezones and times 842 | 843 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 844 | #[cfg_attr(not(target_arch = "wasm32"), test)] 845 | fn parse_with_edt() { 846 | // Eastern Daylight Time (EDT) is from (as of 2023) 2nd Sun in Mar to 1st Sun in Nov 847 | // It is UTC -4 848 | 849 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 850 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 851 | let us_edt = &FixedOffset::west_opt(4 * 3600).unwrap(); 852 | 853 | let edt_test_cases = vec![ 854 | ("ymd", "2023-04-21"), 855 | ("ymd_z", "2023-04-21 EDT"), 856 | ("month_ymd", "2023-Apr-21"), 857 | ("month_mdy", "April 21, 2023"), 858 | ("month_dmy", "21 April 2023"), 859 | ("slash_mdy", "04/21/23"), 860 | ("slash_ymd", "2023/4/21"), 861 | ("dot_mdy_or_ymd", "2023.04.21"), 862 | ("chinese_ymd", "2023年04月21日"), 863 | ]; 864 | 865 | // test us_edt at midnight 866 | let us_edt_midnight_as_utc = Utc.ymd(2023, 4, 21).and_hms(4, 0, 0); 867 | 868 | for &(test, input) in edt_test_cases.iter() { 869 | assert_eq!( 870 | super::parse_with(input, us_edt, midnight_naive).unwrap(), 871 | us_edt_midnight_as_utc, 872 | "parse_with/{test}/{input}", 873 | ) 874 | } 875 | 876 | // test us_edt at 23:59:59 - UTC will be one day ahead 877 | let us_edt_before_midnight_as_utc = Utc.ymd(2023, 4, 22).and_hms(3, 59, 59); 878 | for &(test, input) in edt_test_cases.iter() { 879 | assert_eq!( 880 | super::parse_with(input, us_edt, before_midnight_naive).unwrap(), 881 | us_edt_before_midnight_as_utc, 882 | "parse_with/{test}/{input}", 883 | ) 884 | } 885 | } 886 | 887 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 888 | #[cfg_attr(not(target_arch = "wasm32"), test)] 889 | fn parse_with_est() { 890 | // Eastern Standard Time (EST) is from (as of 2023) 1st Sun in Nov to 2nd Sun in Mar 891 | // It is UTC -5 892 | 893 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 894 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 895 | let us_est = &FixedOffset::west(5 * 3600); 896 | 897 | let est_test_cases = vec![ 898 | ("ymd", "2023-12-21"), 899 | ("ymd_z", "2023-12-21 EST"), 900 | ("month_ymd", "2023-Dec-21"), 901 | ("month_mdy", "December 21, 2023"), 902 | ("month_dmy", "21 December 2023"), 903 | ("slash_mdy", "12/21/23"), 904 | ("slash_ymd", "2023/12/21"), 905 | ("dot_mdy_or_ymd", "2023.12.21"), 906 | ("chinese_ymd", "2023年12月21日"), 907 | ]; 908 | 909 | // test us_est at midnight 910 | let us_est_midnight_as_utc = Utc.ymd(2023, 12, 21).and_hms(5, 0, 0); 911 | 912 | for &(test, input) in est_test_cases.iter() { 913 | assert_eq!( 914 | super::parse_with(input, us_est, midnight_naive).unwrap(), 915 | us_est_midnight_as_utc, 916 | "parse_with/{test}/{input}", 917 | ) 918 | } 919 | 920 | // test us_est at 23:59:59 - UTC will be one day ahead 921 | let us_est_before_midnight_as_utc = Utc.ymd(2023, 12, 22).and_hms(4, 59, 59); 922 | for &(test, input) in est_test_cases.iter() { 923 | assert_eq!( 924 | super::parse_with(input, us_est, before_midnight_naive).unwrap(), 925 | us_est_before_midnight_as_utc, 926 | "parse_with/{test}/{input}", 927 | ) 928 | } 929 | } 930 | 931 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 932 | #[cfg_attr(not(target_arch = "wasm32"), test)] 933 | fn parse_with_utc() { 934 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 935 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 936 | let utc_test_cases = vec![ 937 | ("ymd", "2023-12-21"), 938 | ("ymd_z", "2023-12-21 UTC"), 939 | ("month_ymd", "2023-Dec-21"), 940 | ("month_mdy", "December 21, 2023"), 941 | ("month_dmy", "21 December 2023"), 942 | ("slash_mdy", "12/21/23"), 943 | ("slash_ymd", "2023/12/21"), 944 | ("dot_mdy_or_ymd", "2023.12.21"), 945 | ("chinese_ymd", "2023年12月21日"), 946 | ]; 947 | // test utc at midnight 948 | let utc_midnight = Utc.ymd(2023, 12, 21).and_hms(0, 0, 0); 949 | 950 | for &(test, input) in utc_test_cases.iter() { 951 | assert_eq!( 952 | super::parse_with(input, &Utc, midnight_naive).unwrap(), 953 | utc_midnight, 954 | "parse_with/{test}/{input}", 955 | ) 956 | } 957 | 958 | // test utc at 23:59:59 959 | let utc_before_midnight = Utc.ymd(2023, 12, 21).and_hms(23, 59, 59); 960 | for &(test, input) in utc_test_cases.iter() { 961 | assert_eq!( 962 | super::parse_with(input, &Utc, before_midnight_naive).unwrap(), 963 | utc_before_midnight, 964 | "parse_with/{test}/{input}", 965 | ) 966 | } 967 | } 968 | 969 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 970 | #[cfg_attr(not(target_arch = "wasm32"), test)] 971 | fn parse_with_local() { 972 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 973 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 974 | let local_test_cases = [ 975 | ("ymd", "2023-12-21"), 976 | ("month_ymd", "2023-Dec-21"), 977 | ("month_mdy", "December 21, 2023"), 978 | ("month_dmy", "21 December 2023"), 979 | ("slash_mdy", "12/21/23"), 980 | ("slash_ymd", "2023/12/21"), 981 | ("dot_mdy_or_ymd", "2023.12.21"), 982 | ("chinese_ymd", "2023年12月21日"), 983 | ]; 984 | 985 | // test local at midnight 986 | let local_midnight_as_utc = Local.ymd(2023, 12, 21).and_hms(0, 0, 0).with_timezone(&Utc); 987 | 988 | for &(test, input) in local_test_cases.iter() { 989 | assert_eq!( 990 | super::parse_with(input, &Local, midnight_naive).unwrap(), 991 | local_midnight_as_utc, 992 | "parse_with/{test}/{input}", 993 | ) 994 | } 995 | 996 | // test local at 23:59:59 997 | let local_before_midnight_as_utc = Local 998 | .ymd(2023, 12, 21) 999 | .and_hms(23, 59, 59) 1000 | .with_timezone(&Utc); 1001 | 1002 | for &(test, input) in local_test_cases.iter() { 1003 | assert_eq!( 1004 | super::parse_with(input, &Local, before_midnight_naive).unwrap(), 1005 | local_before_midnight_as_utc, 1006 | "parse_with/{test}/{input}", 1007 | ) 1008 | } 1009 | } 1010 | } 1011 | -------------------------------------------------------------------------------- /dateparser/src/datetime.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | use crate::timezone; 3 | use anyhow::{anyhow, Result}; 4 | use chrono::prelude::*; 5 | 6 | macro_rules! new_regex { 7 | ($name:ident = $regex:literal) => { 8 | std::thread_local! { 9 | #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))] 10 | static $name: ::regex::Regex = regex::Regex::new($regex).unwrap(); 11 | #[cfg(all(feature = "wasm", target_arch = "wasm32"))] 12 | static $name: ::js_sys::RegExp = js_sys::RegExp::new($regex,"d"); 13 | } 14 | }; 15 | } 16 | 17 | trait RegexEx { 18 | fn is_match(&'static self, input: &str) -> bool; 19 | fn with_tz(&'static self, input: &str, then: impl Fn(&str) -> Option) -> Option; 20 | } 21 | 22 | #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))] 23 | impl RegexEx for std::thread::LocalKey<::regex::Regex> { 24 | #[inline] 25 | fn is_match(&'static self, input: &str) -> bool { 26 | self.with(|r| r.is_match(input)) 27 | } 28 | fn with_tz(&'static self, input: &str, then: impl Fn(&str) -> Option) -> Option { 29 | self.with(|r| { 30 | if !r.is_match(input) { 31 | return None; 32 | } 33 | if let Some(caps) = r.captures(input) { 34 | if let Some(m) = caps.name("tz") { 35 | return then(m.as_str().trim()); 36 | } 37 | } 38 | None 39 | }) 40 | } 41 | } 42 | 43 | #[cfg(all(feature = "wasm", target_arch = "wasm32"))] 44 | impl RegexEx for std::thread::LocalKey<::js_sys::RegExp> { 45 | fn is_match(&'static self, input: &str) -> bool { 46 | self.with(|regex| regex.exec(input).is_some()) 47 | } 48 | fn with_tz(&'static self, input: &str, then: impl Fn(&str) -> Option) -> Option { 49 | std::thread_local! { 50 | static INDICES : wasm_bindgen::JsValue = wasm_bindgen::JsValue::from_str("indices"); 51 | static GROUPS : wasm_bindgen::JsValue = wasm_bindgen::JsValue::from_str("groups"); 52 | static TZ : wasm_bindgen::JsValue = wasm_bindgen::JsValue::from_str("tz"); 53 | } 54 | self.with(|regex| { 55 | let res = regex.exec(input)?; 56 | let indices = INDICES.with(|idx| js_sys::Reflect::get(&res, idx)).ok()?; 57 | let groups = GROUPS 58 | .with(|grp| js_sys::Reflect::get(&indices, grp)) 59 | .ok()?; 60 | let tz = TZ.with(|tz| js_sys::Reflect::get(&groups, tz)).ok()?; 61 | let start = js_sys::Reflect::get_u32(&tz, 0).ok()?.as_f64()? as usize; 62 | let end = js_sys::Reflect::get_u32(&tz, 1).ok()?.as_f64()? as usize; 63 | let substr = &input[start..end].trim(); 64 | then(substr) 65 | }) 66 | } 67 | } 68 | 69 | /// Parse struct has methods implemented parsers for accepted formats. 70 | pub struct Parse<'z, Tz2> { 71 | tz: &'z Tz2, 72 | default_time: Option, 73 | } 74 | 75 | impl<'z, Tz2> Parse<'z, Tz2> 76 | where 77 | Tz2: TimeZone, 78 | { 79 | /// Create a new instrance of [`Parse`] with a custom parsing timezone that handles the 80 | /// datetime string without time offset. 81 | pub fn new(tz: &'z Tz2, default_time: Option) -> Self { 82 | Self { tz, default_time } 83 | } 84 | 85 | /// This method tries to parse the input datetime string with a list of accepted formats. See 86 | /// more exmaples from [`Parse`], [`crate::parse()`] and [`crate::parse_with_timezone()`]. 87 | pub fn parse(&self, input: &str) -> Result> { 88 | self.unix_timestamp(input) 89 | .or_else(|| self.rfc2822(input)) 90 | .or_else(|| self.ymd_family(input)) 91 | .or_else(|| self.hms_family(input)) 92 | .or_else(|| self.month_ymd(input)) 93 | .or_else(|| self.month_mdy_family(input)) 94 | .or_else(|| self.month_dmy_family(input)) 95 | .or_else(|| self.slash_mdy_family(input)) 96 | .or_else(|| self.hyphen_mdy_family(input)) 97 | .or_else(|| self.slash_ymd_family(input)) 98 | .or_else(|| self.dot_mdy_or_ymd(input)) 99 | .or_else(|| self.mysql_log_timestamp(input)) 100 | .or_else(|| self.chinese_ymd_family(input)) 101 | .unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input))) 102 | } 103 | 104 | fn ymd_family(&self, input: &str) -> Option>> { 105 | new_regex!(RE = r"^[0-9]{4}-[0-9]{2}"); 106 | if !RE.is_match(input) { 107 | return None; 108 | } 109 | self.rfc3339(input) 110 | .or_else(|| self.postgres_timestamp(input)) 111 | .or_else(|| self.ymd_hms(input)) 112 | .or_else(|| self.ymd_hms_z(input)) 113 | .or_else(|| self.ymd(input)) 114 | .or_else(|| self.ymd_z(input)) 115 | } 116 | 117 | fn hms_family(&self, input: &str) -> Option>> { 118 | new_regex!(RE = r"^[0-9]{1,2}:[0-9]{2}"); 119 | if !RE.is_match(input) { 120 | return None; 121 | } 122 | self.hms(input).or_else(|| self.hms_z(input)) 123 | } 124 | 125 | fn month_mdy_family(&self, input: &str) -> Option>> { 126 | new_regex!(RE = r"^[a-zA-Z]{3,9}\.?\s+[0-9]{1,2}"); 127 | if !RE.is_match(input) { 128 | return None; 129 | } 130 | self.month_md_hms(input) 131 | .or_else(|| self.month_mdy_hms(input)) 132 | .or_else(|| self.month_mdy_hms_z(input)) 133 | .or_else(|| self.month_mdy(input)) 134 | } 135 | 136 | fn month_dmy_family(&self, input: &str) -> Option>> { 137 | new_regex!(RE = r"^[0-9]{1,2}\s+[a-zA-Z]{3,9}"); 138 | if !RE.is_match(input) { 139 | return None; 140 | } 141 | self.month_dmy_hms(input).or_else(|| self.month_dmy(input)) 142 | } 143 | 144 | fn slash_mdy_family(&self, input: &str) -> Option>> { 145 | new_regex!(RE = r"^[0-9]{1,2}/[0-9]{1,2}"); 146 | if !RE.is_match(input) { 147 | return None; 148 | } 149 | self.slash_mdy_hms(input).or_else(|| self.slash_mdy(input)) 150 | } 151 | 152 | fn hyphen_mdy_family(&self, input: &str) -> Option>> { 153 | new_regex!(RE = r"^[0-9]{1,2}-[0-9]{1,2}"); 154 | if !RE.is_match(input) { 155 | return None; 156 | } 157 | self.hyphen_mdy_hms(input) 158 | .or_else(|| self.hyphen_mdy(input)) 159 | } 160 | 161 | fn slash_ymd_family(&self, input: &str) -> Option>> { 162 | new_regex!(RE = r"^[0-9]{4}/[0-9]{1,2}"); 163 | if !RE.is_match(input) { 164 | return None; 165 | } 166 | self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input)) 167 | } 168 | 169 | fn chinese_ymd_family(&self, input: &str) -> Option>> { 170 | new_regex!(RE = r"^[0-9]{4}年[0-9]{2}月"); 171 | if !RE.is_match(input) { 172 | return None; 173 | } 174 | self.chinese_ymd_hms(input) 175 | .or_else(|| self.chinese_ymd(input)) 176 | } 177 | 178 | // unix timestamp 179 | // - 1511648546 180 | // - 1620021848429 181 | // - 1620024872717915000 182 | fn unix_timestamp(&self, input: &str) -> Option>> { 183 | new_regex!(RE = r"^[0-9]{10,19}$"); 184 | if !RE.is_match(input) { 185 | return None; 186 | } 187 | 188 | input 189 | .parse::() 190 | .ok() 191 | .and_then(|timestamp| { 192 | match input.len() { 193 | 10 => Some(Utc.timestamp(timestamp, 0)), 194 | 13 => Some(Utc.timestamp_millis(timestamp)), 195 | 19 => Some(Utc.timestamp_nanos(timestamp)), 196 | _ => None, 197 | } 198 | .map(|datetime| datetime.with_timezone(&Utc)) 199 | }) 200 | .map(Ok) 201 | } 202 | 203 | // rfc3339 204 | // - 2021-05-01T01:17:02.604456Z 205 | // - 2017-11-25T22:34:50Z 206 | fn rfc3339(&self, input: &str) -> Option>> { 207 | DateTime::parse_from_rfc3339(input) 208 | .ok() 209 | .map(|parsed| parsed.with_timezone(&Utc)) 210 | .map(Ok) 211 | } 212 | 213 | // rfc2822 214 | // - Wed, 02 Jun 2021 06:31:39 GMT 215 | fn rfc2822(&self, input: &str) -> Option>> { 216 | DateTime::parse_from_rfc2822(input) 217 | .ok() 218 | .map(|parsed| parsed.with_timezone(&Utc)) 219 | .map(Ok) 220 | } 221 | 222 | // postgres timestamp yyyy-mm-dd hh:mm:ss z 223 | // - 2019-11-29 08:08-08 224 | // - 2019-11-29 08:08:05-08 225 | // - 2021-05-02 23:31:36.0741-07 226 | // - 2021-05-02 23:31:39.12689-07 227 | // - 2019-11-29 08:15:47.624504-08 228 | // - 2017-07-19 03:21:51+00:00 229 | fn postgres_timestamp(&self, input: &str) -> Option>> { 230 | new_regex!( 231 | RE = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?[+-:0-9]{3,6}$" 232 | ); 233 | if !RE.is_match(input) { 234 | return None; 235 | } 236 | 237 | DateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S%#z") 238 | .or_else(|_| DateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S%.f%#z")) 239 | .or_else(|_| DateTime::parse_from_str(input, "%Y-%m-%d %H:%M%#z")) 240 | .ok() 241 | .map(|parsed| parsed.with_timezone(&Utc)) 242 | .map(Ok) 243 | } 244 | 245 | // yyyy-mm-dd hh:mm:ss 246 | // - 2014-04-26 05:24:37 PM 247 | // - 2021-04-30 21:14 248 | // - 2021-04-30 21:14:10 249 | // - 2021-04-30 21:14:10.052282 250 | // - 2014-04-26 17:24:37.123 251 | // - 2014-04-26 17:24:37.3186369 252 | // - 2012-08-03 18:31:59.257000000 253 | fn ymd_hms(&self, input: &str) -> Option>> { 254 | new_regex!( 255 | RE = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$" 256 | ); 257 | if !RE.is_match(input) { 258 | return None; 259 | } 260 | 261 | self.tz 262 | .datetime_from_str(input, "%Y-%m-%d %H:%M:%S") 263 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %H:%M")) 264 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %H:%M:%S%.f")) 265 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %I:%M:%S %P")) 266 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %I:%M %P")) 267 | .ok() 268 | .map(|parsed| parsed.with_timezone(&Utc)) 269 | .map(Ok) 270 | } 271 | 272 | // yyyy-mm-dd hh:mm:ss z 273 | // - 2017-11-25 13:31:15 PST 274 | // - 2017-11-25 13:31 PST 275 | // - 2014-12-16 06:20:00 UTC 276 | // - 2014-12-16 06:20:00 GMT 277 | // - 2014-04-26 13:13:43 +0800 278 | // - 2014-04-26 13:13:44 +09:00 279 | // - 2012-08-03 18:31:59.257000000 +0000 280 | // - 2015-09-30 18:48:56.35272715 UTC 281 | fn ymd_hms_z(&self, input: &str) -> Option>> { 282 | new_regex!( 283 | RE = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?(?\s*[+-:a-zA-Z0-9]{3,6})$" 284 | ); 285 | 286 | RE.with_tz(input, |matched_tz| { 287 | let parse_from_str = NaiveDateTime::parse_from_str; 288 | match timezone::parse(matched_tz) { 289 | Ok(offset) => parse_from_str(input, "%Y-%m-%d %H:%M:%S %Z") 290 | .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M %Z")) 291 | .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M:%S%.f %Z")) 292 | .ok() 293 | .and_then(|parsed| offset.from_local_datetime(&parsed).single()) 294 | .map(|datetime| datetime.with_timezone(&Utc)) 295 | .map(Ok), 296 | Err(err) => Some(Err(err)), 297 | } 298 | }) 299 | } 300 | 301 | // yyyy-mm-dd 302 | // - 2021-02-21 303 | fn ymd(&self, input: &str) -> Option>> { 304 | new_regex!(RE = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"); 305 | 306 | if !RE.is_match(input) { 307 | return None; 308 | } 309 | 310 | // set time to use 311 | let time = match self.default_time { 312 | Some(v) => v, 313 | None => Utc::now().with_timezone(self.tz).time(), 314 | }; 315 | 316 | NaiveDate::parse_from_str(input, "%Y-%m-%d") 317 | .ok() 318 | .map(|parsed| parsed.and_time(time)) 319 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 320 | .map(|at_tz| at_tz.with_timezone(&Utc)) 321 | .map(Ok) 322 | } 323 | 324 | // yyyy-mm-dd z 325 | // - 2021-02-21 PST 326 | // - 2021-02-21 UTC 327 | // - 2020-07-20+08:00 (yyyy-mm-dd-07:00) 328 | fn ymd_z(&self, input: &str) -> Option>> { 329 | new_regex!(RE = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}(?\s*[+-:a-zA-Z0-9]{3,6})$"); 330 | 331 | RE.with_tz(input, |matched_tz| { 332 | match timezone::parse(matched_tz) { 333 | Ok(offset) => { 334 | // set time to use 335 | let time = match self.default_time { 336 | Some(v) => v, 337 | None => Utc::now().with_timezone(&offset).time(), 338 | }; 339 | NaiveDate::parse_from_str(input, "%Y-%m-%d %Z") 340 | .ok() 341 | .map(|parsed| parsed.and_time(time)) 342 | .and_then(|datetime| offset.from_local_datetime(&datetime).single()) 343 | .map(|at_tz| at_tz.with_timezone(&Utc)) 344 | .map(Ok) 345 | } 346 | Err(err) => Some(Err(err)), 347 | } 348 | }) 349 | } 350 | 351 | // hh:mm:ss 352 | // - 01:06:06 353 | // - 4:00pm 354 | // - 6:00 AM 355 | fn hms(&self, input: &str) -> Option>> { 356 | new_regex!(RE = r"^[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?$"); 357 | if !RE.is_match(input) { 358 | return None; 359 | } 360 | 361 | let now = Utc::now().with_timezone(self.tz); 362 | NaiveTime::parse_from_str(input, "%H:%M:%S") 363 | .or_else(|_| NaiveTime::parse_from_str(input, "%H:%M")) 364 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M:%S %P")) 365 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M %P")) 366 | .ok() 367 | .and_then(|parsed| now.date().and_time(parsed)) 368 | .map(|datetime| datetime.with_timezone(&Utc)) 369 | .map(Ok) 370 | } 371 | 372 | // hh:mm:ss z 373 | // - 01:06:06 PST 374 | // - 4:00pm PST 375 | // - 6:00 AM PST 376 | // - 6:00pm UTC 377 | fn hms_z(&self, input: &str) -> Option>> { 378 | new_regex!( 379 | RE = r"^[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?(?\s+[+-:a-zA-Z0-9]{3,6})$" 380 | ); 381 | RE.with_tz(input, |matched_tz| match timezone::parse(matched_tz) { 382 | Ok(offset) => { 383 | let now = Utc::now().with_timezone(&offset); 384 | NaiveTime::parse_from_str(input, "%H:%M:%S %Z") 385 | .or_else(|_| NaiveTime::parse_from_str(input, "%H:%M %Z")) 386 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M:%S %P %Z")) 387 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M %P %Z")) 388 | .ok() 389 | .map(|parsed| now.date().naive_local().and_time(parsed)) 390 | .and_then(|datetime| offset.from_local_datetime(&datetime).single()) 391 | .map(|at_tz| at_tz.with_timezone(&Utc)) 392 | .map(Ok) 393 | } 394 | Err(err) => Some(Err(err)), 395 | }) 396 | } 397 | 398 | // yyyy-mon-dd 399 | // - 2021-Feb-21 400 | fn month_ymd(&self, input: &str) -> Option>> { 401 | new_regex!(RE = r"^[0-9]{4}-[a-zA-Z]{3,9}-[0-9]{2}$"); 402 | if !RE.is_match(input) { 403 | return None; 404 | } 405 | 406 | // set time to use 407 | let time = match self.default_time { 408 | Some(v) => v, 409 | None => Utc::now().with_timezone(self.tz).time(), 410 | }; 411 | 412 | NaiveDate::parse_from_str(input, "%Y-%m-%d") 413 | .or_else(|_| NaiveDate::parse_from_str(input, "%Y-%b-%d")) 414 | .ok() 415 | .map(|parsed| parsed.and_time(time)) 416 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 417 | .map(|at_tz| at_tz.with_timezone(&Utc)) 418 | .map(Ok) 419 | } 420 | 421 | // Mon dd hh:mm:ss 422 | // - May 6 at 9:24 PM 423 | // - May 27 02:45:27 424 | fn month_md_hms(&self, input: &str) -> Option>> { 425 | new_regex!( 426 | RE = r"^[a-zA-Z]{3}\s+[0-9]{1,2}\s*(at)?\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?$" 427 | ); 428 | if !RE.is_match(input) { 429 | return None; 430 | } 431 | 432 | let now = Utc::now().with_timezone(self.tz); 433 | let with_year = format!("{} {}", now.year(), input); 434 | self.tz 435 | .datetime_from_str(&with_year, "%Y %b %d at %I:%M %P") 436 | .or_else(|_| self.tz.datetime_from_str(&with_year, "%Y %b %d %H:%M:%S")) 437 | .ok() 438 | .map(|parsed| parsed.with_timezone(&Utc)) 439 | .map(Ok) 440 | } 441 | 442 | // Mon dd, yyyy, hh:mm:ss 443 | // - May 8, 2009 5:57:51 PM 444 | // - September 17, 2012 10:09am 445 | // - September 17, 2012, 10:10:09 446 | fn month_mdy_hms(&self, input: &str) -> Option>> { 447 | new_regex!( 448 | RE = r"^[a-zA-Z]{3,9}\.?\s+[0-9]{1,2},\s+[0-9]{2,4},?\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?$" 449 | ); 450 | if !RE.is_match(input) { 451 | return None; 452 | } 453 | 454 | let dt = input.replace(", ", " ").replace(". ", " "); 455 | self.tz 456 | .datetime_from_str(&dt, "%B %d %Y %H:%M:%S") 457 | .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %H:%M")) 458 | .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M:%S %P")) 459 | .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M %P")) 460 | .ok() 461 | .map(|at_tz| at_tz.with_timezone(&Utc)) 462 | .map(Ok) 463 | } 464 | 465 | // Mon dd, yyyy hh:mm:ss z 466 | // - May 02, 2021 15:51:31 UTC 467 | // - May 02, 2021 15:51 UTC 468 | // - May 26, 2021, 12:49 AM PDT 469 | // - September 17, 2012 at 10:09am PST 470 | fn month_mdy_hms_z(&self, input: &str) -> Option>> { 471 | new_regex!( 472 | RE = r"^[a-zA-Z]{3,9}\s+[0-9]{1,2},?\s+[0-9]{4}\s*,?(at)?\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?(?\s+[+-:a-zA-Z0-9]{3,6})$" 473 | ); 474 | 475 | RE.with_tz(input, |matched_tz| { 476 | let parse_from_str = NaiveDateTime::parse_from_str; 477 | match timezone::parse(matched_tz) { 478 | Ok(offset) => { 479 | let dt = input.replace(',', "").replace("at", ""); 480 | parse_from_str(&dt, "%B %d %Y %H:%M:%S %Z") 481 | .or_else(|_| parse_from_str(&dt, "%B %d %Y %H:%M %Z")) 482 | .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M:%S %P %Z")) 483 | .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M %P %Z")) 484 | .ok() 485 | .and_then(|parsed| offset.from_local_datetime(&parsed).single()) 486 | .map(|datetime| datetime.with_timezone(&Utc)) 487 | .map(Ok) 488 | } 489 | Err(err) => Some(Err(err)), 490 | } 491 | }) 492 | } 493 | 494 | // Mon dd, yyyy 495 | // - May 25, 2021 496 | // - oct 7, 1970 497 | // - oct 7, 70 498 | // - oct. 7, 1970 499 | // - oct. 7, 70 500 | // - October 7, 1970 501 | fn month_mdy(&self, input: &str) -> Option>> { 502 | new_regex!(RE = r"^[a-zA-Z]{3,9}\.?\s+[0-9]{1,2},\s+[0-9]{2,4}$"); 503 | if !RE.is_match(input) { 504 | return None; 505 | } 506 | 507 | // set time to use 508 | let time = match self.default_time { 509 | Some(v) => v, 510 | None => Utc::now().with_timezone(self.tz).time(), 511 | }; 512 | 513 | let dt = input.replace(", ", " ").replace(". ", " "); 514 | NaiveDate::parse_from_str(&dt, "%B %d %y") 515 | .or_else(|_| NaiveDate::parse_from_str(&dt, "%B %d %Y")) 516 | .ok() 517 | .map(|parsed| parsed.and_time(time)) 518 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 519 | .map(|at_tz| at_tz.with_timezone(&Utc)) 520 | .map(Ok) 521 | } 522 | 523 | // dd Mon yyyy hh:mm:ss 524 | // - 12 Feb 2006, 19:17 525 | // - 12 Feb 2006 19:17 526 | // - 14 May 2019 19:11:40.164 527 | fn month_dmy_hms(&self, input: &str) -> Option>> { 528 | new_regex!( 529 | RE = r"^[0-9]{1,2}\s+[a-zA-Z]{3,9}\s+[0-9]{2,4},?\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?$" 530 | ); 531 | if !RE.is_match(input) { 532 | return None; 533 | } 534 | 535 | let dt = input.replace(", ", " "); 536 | self.tz 537 | .datetime_from_str(&dt, "%d %B %Y %H:%M:%S") 538 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M")) 539 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M:%S%.f")) 540 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M:%S %P")) 541 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M %P")) 542 | .ok() 543 | .map(|at_tz| at_tz.with_timezone(&Utc)) 544 | .map(Ok) 545 | } 546 | 547 | // dd Mon yyyy 548 | // - 7 oct 70 549 | // - 7 oct 1970 550 | // - 03 February 2013 551 | // - 1 July 2013 552 | fn month_dmy(&self, input: &str) -> Option>> { 553 | new_regex!(RE = r"^[0-9]{1,2}\s+[a-zA-Z]{3,9}\s+[0-9]{2,4}$"); 554 | if !RE.is_match(input) { 555 | return None; 556 | } 557 | 558 | // set time to use 559 | let time = match self.default_time { 560 | Some(v) => v, 561 | None => Utc::now().with_timezone(self.tz).time(), 562 | }; 563 | 564 | NaiveDate::parse_from_str(input, "%d %B %y") 565 | .or_else(|_| NaiveDate::parse_from_str(input, "%d %B %Y")) 566 | .ok() 567 | .map(|parsed| parsed.and_time(time)) 568 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 569 | .map(|at_tz| at_tz.with_timezone(&Utc)) 570 | .map(Ok) 571 | } 572 | 573 | // mm/dd/yyyy hh:mm:ss 574 | // - 4/8/2014 22:05 575 | // - 04/08/2014 22:05 576 | // - 4/8/14 22:05 577 | // - 04/2/2014 03:00:51 578 | // - 8/8/1965 12:00:00 AM 579 | // - 8/8/1965 01:00:01 PM 580 | // - 8/8/1965 01:00 PM 581 | // - 8/8/1965 1:00 PM 582 | // - 8/8/1965 12:00 AM 583 | // - 4/02/2014 03:00:51 584 | // - 03/19/2012 10:11:59 585 | // - 03/19/2012 10:11:59.3186369 586 | fn slash_mdy_hms(&self, input: &str) -> Option>> { 587 | new_regex!( 588 | RE = r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$" 589 | ); 590 | if !RE.is_match(input) { 591 | return None; 592 | } 593 | 594 | self.tz 595 | .datetime_from_str(input, "%m/%d/%y %H:%M:%S") 596 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %H:%M")) 597 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %H:%M:%S%.f")) 598 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %I:%M:%S %P")) 599 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %I:%M %P")) 600 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M:%S")) 601 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M")) 602 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M:%S%.f")) 603 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %I:%M:%S %P")) 604 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %I:%M %P")) 605 | .ok() 606 | .map(|at_tz| at_tz.with_timezone(&Utc)) 607 | .map(Ok) 608 | } 609 | 610 | // mm/dd/yyyy 611 | // - 3/31/2014 612 | // - 03/31/2014 613 | // - 08/21/71 614 | // - 8/1/71 615 | fn slash_mdy(&self, input: &str) -> Option>> { 616 | new_regex!(RE = r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$"); 617 | if !RE.is_match(input) { 618 | return None; 619 | } 620 | 621 | // set time to use 622 | let time = match self.default_time { 623 | Some(v) => v, 624 | None => Utc::now().with_timezone(self.tz).time(), 625 | }; 626 | 627 | NaiveDate::parse_from_str(input, "%m/%d/%y") 628 | .or_else(|_| NaiveDate::parse_from_str(input, "%m/%d/%Y")) 629 | .ok() 630 | .map(|parsed| parsed.and_time(time)) 631 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 632 | .map(|at_tz| at_tz.with_timezone(&Utc)) 633 | .map(Ok) 634 | } 635 | 636 | // yyyy/mm/dd hh:mm:ss 637 | // - 2014/4/8 22:05 638 | // - 2014/04/08 22:05 639 | // - 2014/04/2 03:00:51 640 | // - 2014/4/02 03:00:51 641 | // - 2012/03/19 10:11:59 642 | // - 2012/03/19 10:11:59.3186369 643 | fn slash_ymd_hms(&self, input: &str) -> Option>> { 644 | new_regex!( 645 | RE = r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$" 646 | ); 647 | if !RE.is_match(input) { 648 | return None; 649 | } 650 | 651 | self.tz 652 | .datetime_from_str(input, "%Y/%m/%d %H:%M:%S") 653 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M")) 654 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M:%S%.f")) 655 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M:%S %P")) 656 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M %P")) 657 | .ok() 658 | .map(|at_tz| at_tz.with_timezone(&Utc)) 659 | .map(Ok) 660 | } 661 | 662 | // yyyy/mm/dd 663 | // - 2014/3/31 664 | // - 2014/03/31 665 | fn slash_ymd(&self, input: &str) -> Option>> { 666 | new_regex!(RE = r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$"); 667 | if !RE.is_match(input) { 668 | return None; 669 | } 670 | 671 | // set time to use 672 | let time = match self.default_time { 673 | Some(v) => v, 674 | None => Utc::now().with_timezone(self.tz).time(), 675 | }; 676 | 677 | NaiveDate::parse_from_str(input, "%Y/%m/%d") 678 | .ok() 679 | .map(|parsed| parsed.and_time(time)) 680 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 681 | .map(|at_tz| at_tz.with_timezone(&Utc)) 682 | .map(Ok) 683 | } 684 | 685 | // mm-dd-yyyy 686 | // - 3-31-2014 687 | // - 03-3-2014 688 | // - 08-21-71 689 | // - 8-1-71 690 | fn hyphen_mdy(&self, input: &str) -> Option>> { 691 | new_regex!(RE = r"^[0-9]{1,2}-[0-9]{1,2}-[0-9]{2,4}$"); 692 | if !RE.is_match(input) { 693 | return None; 694 | } 695 | 696 | // set time to use 697 | let time = match self.default_time { 698 | Some(v) => v, 699 | None => Utc::now().with_timezone(self.tz).time(), 700 | }; 701 | 702 | NaiveDate::parse_from_str(input, "%m-%d-%y") 703 | .or_else(|_| NaiveDate::parse_from_str(input, "%m-%d-%Y")) 704 | .ok() 705 | .map(|parsed| parsed.and_time(time)) 706 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 707 | .map(|at_tz| at_tz.with_timezone(&Utc)) 708 | .map(Ok) 709 | } 710 | 711 | // mm-dd-yyyy hh:mm:ss 712 | // - 4-8-2014 22:05 713 | // - 04-08-2014 22:05 714 | // - 4-8-14 22:05 715 | // - 04-2-2014 03:00:51 716 | // - 8-8-1965 12:00:00 AM 717 | // - 8-8-1965 01:00:01 PM 718 | // - 8-8-1965 01:00 PM 719 | // - 8-8-1965 1:00 PM 720 | // - 8-8-1965 12:00 AM 721 | // - 4-02-2014 03:00:51 722 | // - 03-19-2012 10:11:59 723 | // - 03-19-2012 10:11:59.3186369 724 | fn hyphen_mdy_hms(&self, input: &str) -> Option>> { 725 | new_regex!( 726 | RE = r"^[0-9]{1,2}-[0-9]{1,2}-[0-9]{2,4}\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$" 727 | ); 728 | if !RE.is_match(input) { 729 | return None; 730 | } 731 | 732 | self.tz 733 | .datetime_from_str(input, "%m-%d-%y %H:%M:%S") 734 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %H:%M")) 735 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %H:%M:%S%.f")) 736 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %I:%M:%S %P")) 737 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %I:%M %P")) 738 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %H:%M:%S")) 739 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %H:%M")) 740 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %H:%M:%S%.f")) 741 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %I:%M:%S %P")) 742 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %I:%M %P")) 743 | .ok() 744 | .map(|at_tz| at_tz.with_timezone(&Utc)) 745 | .map(Ok) 746 | } 747 | 748 | // mm.dd.yyyy 749 | // - 3.31.2014 750 | // - 03.31.2014 751 | // - 08.21.71 752 | // yyyy.mm.dd 753 | // - 2014.03.30 754 | // - 2014.03 755 | fn dot_mdy_or_ymd(&self, input: &str) -> Option>> { 756 | new_regex!(RE = r"[0-9]{1,4}.[0-9]{1,4}[0-9]{1,4}"); 757 | if !RE.is_match(input) { 758 | return None; 759 | } 760 | 761 | // set time to use 762 | let time = match self.default_time { 763 | Some(v) => v, 764 | None => Utc::now().with_timezone(self.tz).time(), 765 | }; 766 | 767 | NaiveDate::parse_from_str(input, "%m.%d.%y") 768 | .or_else(|_| NaiveDate::parse_from_str(input, "%m.%d.%Y")) 769 | .or_else(|_| NaiveDate::parse_from_str(input, "%Y.%m.%d")) 770 | .or_else(|_| { 771 | NaiveDate::parse_from_str(&format!("{}.{}", input, Utc::now().day()), "%Y.%m.%d") 772 | }) 773 | .ok() 774 | .map(|parsed| parsed.and_time(time)) 775 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 776 | .map(|at_tz| at_tz.with_timezone(&Utc)) 777 | .map(Ok) 778 | } 779 | 780 | // yymmdd hh:mm:ss mysql log 781 | // - 171113 14:14:20 782 | fn mysql_log_timestamp(&self, input: &str) -> Option>> { 783 | new_regex!(RE = r"[0-9]{6}\s+[0-9]{2}:[0-9]{2}:[0-9]{2}"); 784 | if !RE.is_match(input) { 785 | return None; 786 | } 787 | 788 | self.tz 789 | .datetime_from_str(input, "%y%m%d %H:%M:%S") 790 | .ok() 791 | .map(|at_tz| at_tz.with_timezone(&Utc)) 792 | .map(Ok) 793 | } 794 | 795 | // chinese yyyy mm dd hh mm ss 796 | // - 2014年04月08日11时25分18秒 797 | fn chinese_ymd_hms(&self, input: &str) -> Option>> { 798 | new_regex!(RE = r"^[0-9]{4}年[0-9]{2}月[0-9]{2}日[0-9]{2}时[0-9]{2}分[0-9]{2}秒$"); 799 | if !RE.is_match(input) { 800 | return None; 801 | } 802 | 803 | self.tz 804 | .datetime_from_str(input, "%Y年%m月%d日%H时%M分%S秒") 805 | .ok() 806 | .map(|at_tz| at_tz.with_timezone(&Utc)) 807 | .map(Ok) 808 | } 809 | 810 | // chinese yyyy mm dd 811 | // - 2014年04月08日 812 | fn chinese_ymd(&self, input: &str) -> Option>> { 813 | new_regex!(RE = r"^[0-9]{4}年[0-9]{2}月[0-9]{2}日$"); 814 | if !RE.is_match(input) { 815 | return None; 816 | } 817 | 818 | // set time to use 819 | let time = match self.default_time { 820 | Some(v) => v, 821 | None => Utc::now().with_timezone(self.tz).time(), 822 | }; 823 | 824 | NaiveDate::parse_from_str(input, "%Y年%m月%d日") 825 | .ok() 826 | .map(|parsed| parsed.and_time(time)) 827 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 828 | .map(|at_tz| at_tz.with_timezone(&Utc)) 829 | .map(Ok) 830 | } 831 | } 832 | 833 | #[cfg(test)] 834 | mod tests { 835 | use super::*; 836 | 837 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 838 | #[cfg_attr(not(target_arch = "wasm32"), test)] 839 | fn unix_timestamp() { 840 | let parse = Parse::new(&Utc, None); 841 | 842 | let test_cases = [ 843 | ("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), 844 | ("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), 845 | ("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), 846 | ("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)), 847 | ( 848 | "1620021848429", 849 | Utc.ymd(2021, 5, 3).and_hms_milli(6, 4, 8, 429), 850 | ), 851 | ( 852 | "1620024872717915000", 853 | Utc.ymd(2021, 5, 3).and_hms_nano(6, 54, 32, 717915000), 854 | ), 855 | ]; 856 | 857 | for &(input, want) in test_cases.iter() { 858 | assert_eq!( 859 | parse.unix_timestamp(input).unwrap().unwrap(), 860 | want, 861 | "unix_timestamp/{}", 862 | input 863 | ) 864 | } 865 | assert!(parse.unix_timestamp("15116").is_none()); 866 | assert!(parse 867 | .unix_timestamp("16200248727179150001620024872717915000") 868 | .is_none()); 869 | assert!(parse.unix_timestamp("not-a-ts").is_none()); 870 | } 871 | 872 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 873 | #[cfg_attr(not(target_arch = "wasm32"), test)] 874 | fn rfc3339() { 875 | let parse = Parse::new(&Utc, None); 876 | 877 | let test_cases = [ 878 | ( 879 | "2021-05-01T01:17:02.604456Z", 880 | Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000), 881 | ), 882 | ( 883 | "2017-11-25T22:34:50Z", 884 | Utc.ymd(2017, 11, 25).and_hms(22, 34, 50), 885 | ), 886 | ]; 887 | 888 | for &(input, want) in test_cases.iter() { 889 | assert_eq!( 890 | parse.rfc3339(input).unwrap().unwrap(), 891 | want, 892 | "rfc3339/{}", 893 | input 894 | ) 895 | } 896 | assert!(parse.rfc3339("2017-11-25 22:34:50").is_none()); 897 | assert!(parse.rfc3339("not-date-time").is_none()); 898 | } 899 | 900 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 901 | #[cfg_attr(not(target_arch = "wasm32"), test)] 902 | fn rfc2822() { 903 | let parse = Parse::new(&Utc, None); 904 | 905 | let test_cases = [ 906 | ( 907 | "Wed, 02 Jun 2021 06:31:39 GMT", 908 | Utc.ymd(2021, 6, 2).and_hms(6, 31, 39), 909 | ), 910 | ( 911 | "Wed, 02 Jun 2021 06:31:39 PDT", 912 | Utc.ymd(2021, 6, 2).and_hms(13, 31, 39), 913 | ), 914 | ]; 915 | 916 | for &(input, want) in test_cases.iter() { 917 | assert_eq!( 918 | parse.rfc2822(input).unwrap().unwrap(), 919 | want, 920 | "rfc2822/{}", 921 | input 922 | ) 923 | } 924 | assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none()); 925 | assert!(parse.rfc2822("not-date-time").is_none()); 926 | } 927 | 928 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 929 | #[cfg_attr(not(target_arch = "wasm32"), test)] 930 | fn postgres_timestamp() { 931 | let parse = Parse::new(&Utc, None); 932 | 933 | let test_cases = [ 934 | ( 935 | "2019-11-29 08:08-08", 936 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 0), 937 | ), 938 | ( 939 | "2019-11-29 08:08:05-08", 940 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 5), 941 | ), 942 | ( 943 | "2021-05-02 23:31:36.0741-07", 944 | Utc.ymd(2021, 5, 3).and_hms_micro(6, 31, 36, 74100), 945 | ), 946 | ( 947 | "2021-05-02 23:31:39.12689-07", 948 | Utc.ymd(2021, 5, 3).and_hms_micro(6, 31, 39, 126890), 949 | ), 950 | ( 951 | "2019-11-29 08:15:47.624504-08", 952 | Utc.ymd(2019, 11, 29).and_hms_micro(16, 15, 47, 624504), 953 | ), 954 | ( 955 | "2017-07-19 03:21:51+00:00", 956 | Utc.ymd(2017, 7, 19).and_hms(3, 21, 51), 957 | ), 958 | ]; 959 | 960 | for &(input, want) in test_cases.iter() { 961 | assert_eq!( 962 | parse.postgres_timestamp(input).unwrap().unwrap(), 963 | want, 964 | "postgres_timestamp/{}", 965 | input 966 | ) 967 | } 968 | assert!(parse.postgres_timestamp("not-date-time").is_none()); 969 | } 970 | 971 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 972 | #[cfg_attr(not(target_arch = "wasm32"), test)] 973 | fn ymd_hms() { 974 | let parse = Parse::new(&Utc, None); 975 | 976 | let test_cases = [ 977 | ("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)), 978 | ( 979 | "2021-04-30 21:14:10", 980 | Utc.ymd(2021, 4, 30).and_hms(21, 14, 10), 981 | ), 982 | ( 983 | "2021-04-30 21:14:10.052282", 984 | Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282), 985 | ), 986 | ( 987 | "2014-04-26 05:24:37 PM", 988 | Utc.ymd(2014, 4, 26).and_hms(17, 24, 37), 989 | ), 990 | ( 991 | "2014-04-26 17:24:37.123", 992 | Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123), 993 | ), 994 | ( 995 | "2014-04-26 17:24:37.3186369", 996 | Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900), 997 | ), 998 | ( 999 | "2012-08-03 18:31:59.257000000", 1000 | Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000), 1001 | ), 1002 | ]; 1003 | 1004 | for &(input, want) in test_cases.iter() { 1005 | assert_eq!( 1006 | parse.ymd_hms(input).unwrap().unwrap(), 1007 | want, 1008 | "ymd_hms/{}", 1009 | input 1010 | ) 1011 | } 1012 | assert!(parse.ymd_hms("not-date-time").is_none()); 1013 | } 1014 | 1015 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1016 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1017 | fn ymd_hms_z() { 1018 | let parse = Parse::new(&Utc, None); 1019 | 1020 | let test_cases = [ 1021 | ( 1022 | "2017-11-25 13:31:15 PST", 1023 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 15), 1024 | ), 1025 | ( 1026 | "2017-11-25 13:31 PST", 1027 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 0), 1028 | ), 1029 | ( 1030 | "2014-12-16 06:20:00 UTC", 1031 | Utc.ymd(2014, 12, 16).and_hms(6, 20, 0), 1032 | ), 1033 | ( 1034 | "2014-12-16 06:20:00 GMT", 1035 | Utc.ymd(2014, 12, 16).and_hms(6, 20, 0), 1036 | ), 1037 | ( 1038 | "2014-04-26 13:13:43 +0800", 1039 | Utc.ymd(2014, 4, 26).and_hms(5, 13, 43), 1040 | ), 1041 | ( 1042 | "2014-04-26 13:13:44 +09:00", 1043 | Utc.ymd(2014, 4, 26).and_hms(4, 13, 44), 1044 | ), 1045 | ( 1046 | "2012-08-03 18:31:59.257000000 +0000", 1047 | Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000), 1048 | ), 1049 | ( 1050 | "2015-09-30 18:48:56.35272715 UTC", 1051 | Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150), 1052 | ), 1053 | ]; 1054 | 1055 | for &(input, want) in test_cases.iter() { 1056 | assert_eq!( 1057 | parse.ymd_hms_z(input).unwrap().unwrap(), 1058 | want, 1059 | "ymd_hms_z/{}", 1060 | input 1061 | ) 1062 | } 1063 | assert!(parse.ymd_hms_z("not-date-time").is_none()); 1064 | } 1065 | 1066 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1067 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1068 | fn ymd() { 1069 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1070 | 1071 | let test_cases = [( 1072 | "2021-02-21", 1073 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()), 1074 | )]; 1075 | 1076 | for &(input, want) in test_cases.iter() { 1077 | assert_eq!( 1078 | parse 1079 | .ymd(input) 1080 | .unwrap() 1081 | .unwrap() 1082 | .trunc_subsecs(0) 1083 | .with_second(0) 1084 | .unwrap(), 1085 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1086 | "ymd/{}", 1087 | input 1088 | ) 1089 | } 1090 | assert!(parse.ymd("not-date-time").is_none()); 1091 | } 1092 | 1093 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1094 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1095 | fn ymd_z() { 1096 | let parse = Parse::new(&Utc, None); 1097 | let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600)); 1098 | let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600)); 1099 | 1100 | let test_cases = [ 1101 | ( 1102 | "2021-02-21 PST", 1103 | FixedOffset::west(8 * 3600) 1104 | .ymd(2021, 2, 21) 1105 | .and_time(now_at_pst.time()) 1106 | .map(|dt| dt.with_timezone(&Utc)), 1107 | ), 1108 | ( 1109 | "2021-02-21 UTC", 1110 | FixedOffset::west(0) 1111 | .ymd(2021, 2, 21) 1112 | .and_time(Utc::now().time()) 1113 | .map(|dt| dt.with_timezone(&Utc)), 1114 | ), 1115 | ( 1116 | "2020-07-20+08:00", 1117 | FixedOffset::east(8 * 3600) 1118 | .ymd(2020, 7, 20) 1119 | .and_time(now_at_cst.time()) 1120 | .map(|dt| dt.with_timezone(&Utc)), 1121 | ), 1122 | ]; 1123 | 1124 | for &(input, want) in test_cases.iter() { 1125 | assert_eq!( 1126 | parse 1127 | .ymd_z(input) 1128 | .unwrap() 1129 | .unwrap() 1130 | .trunc_subsecs(0) 1131 | .with_second(0) 1132 | .unwrap(), 1133 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1134 | "ymd_z/{}", 1135 | input 1136 | ) 1137 | } 1138 | assert!(parse.ymd_z("not-date-time").is_none()); 1139 | } 1140 | 1141 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1142 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1143 | fn hms() { 1144 | let parse = Parse::new(&Utc, None); 1145 | 1146 | let test_cases = [ 1147 | ( 1148 | "01:06:06", 1149 | Utc::now().date().and_time(NaiveTime::from_hms(1, 6, 6)), 1150 | ), 1151 | ( 1152 | "4:00pm", 1153 | Utc::now().date().and_time(NaiveTime::from_hms(16, 0, 0)), 1154 | ), 1155 | ( 1156 | "6:00 AM", 1157 | Utc::now().date().and_time(NaiveTime::from_hms(6, 0, 0)), 1158 | ), 1159 | ]; 1160 | 1161 | for &(input, want) in test_cases.iter() { 1162 | assert_eq!( 1163 | parse.hms(input).unwrap().unwrap(), 1164 | want.unwrap(), 1165 | "hms/{}", 1166 | input 1167 | ) 1168 | } 1169 | assert!(parse.hms("not-date-time").is_none()); 1170 | } 1171 | 1172 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1173 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1174 | fn hms_z() { 1175 | let parse = Parse::new(&Utc, None); 1176 | let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600)); 1177 | 1178 | let test_cases = [ 1179 | ( 1180 | "01:06:06 PST", 1181 | FixedOffset::west(8 * 3600) 1182 | .from_local_date(&now_at_pst.date().naive_local()) 1183 | .and_time(NaiveTime::from_hms(1, 6, 6)) 1184 | .map(|dt| dt.with_timezone(&Utc)), 1185 | ), 1186 | ( 1187 | "4:00pm PST", 1188 | FixedOffset::west(8 * 3600) 1189 | .from_local_date(&now_at_pst.date().naive_local()) 1190 | .and_time(NaiveTime::from_hms(16, 0, 0)) 1191 | .map(|dt| dt.with_timezone(&Utc)), 1192 | ), 1193 | ( 1194 | "6:00 AM PST", 1195 | FixedOffset::west(8 * 3600) 1196 | .from_local_date(&now_at_pst.date().naive_local()) 1197 | .and_time(NaiveTime::from_hms(6, 0, 0)) 1198 | .map(|dt| dt.with_timezone(&Utc)), 1199 | ), 1200 | ( 1201 | "6:00pm UTC", 1202 | FixedOffset::west(0) 1203 | .from_local_date(&Utc::now().date().naive_local()) 1204 | .and_time(NaiveTime::from_hms(18, 0, 0)) 1205 | .map(|dt| dt.with_timezone(&Utc)), 1206 | ), 1207 | ]; 1208 | 1209 | for &(input, want) in test_cases.iter() { 1210 | assert_eq!( 1211 | parse.hms_z(input).unwrap().unwrap(), 1212 | want.unwrap(), 1213 | "hms_z/{}", 1214 | input 1215 | ) 1216 | } 1217 | assert!(parse.hms_z("not-date-time").is_none()); 1218 | } 1219 | 1220 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1221 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1222 | fn month_ymd() { 1223 | let parse = Parse::new(&Utc, None); 1224 | 1225 | let test_cases = [( 1226 | "2021-Feb-21", 1227 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()), 1228 | )]; 1229 | 1230 | for &(input, want) in test_cases.iter() { 1231 | assert_eq!( 1232 | parse 1233 | .month_ymd(input) 1234 | .unwrap() 1235 | .unwrap() 1236 | .trunc_subsecs(0) 1237 | .with_second(0) 1238 | .unwrap(), 1239 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1240 | "month_ymd/{}", 1241 | input 1242 | ) 1243 | } 1244 | assert!(parse.month_ymd("not-date-time").is_none()); 1245 | } 1246 | 1247 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1248 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1249 | fn month_md_hms() { 1250 | let parse = Parse::new(&Utc, None); 1251 | 1252 | let test_cases = [ 1253 | ( 1254 | "May 6 at 9:24 PM", 1255 | Utc.ymd(Utc::now().year(), 5, 6).and_hms(21, 24, 0), 1256 | ), 1257 | ( 1258 | "May 27 02:45:27", 1259 | Utc.ymd(Utc::now().year(), 5, 27).and_hms(2, 45, 27), 1260 | ), 1261 | ]; 1262 | 1263 | for &(input, want) in test_cases.iter() { 1264 | assert_eq!( 1265 | parse.month_md_hms(input).unwrap().unwrap(), 1266 | want, 1267 | "month_md_hms/{}", 1268 | input 1269 | ) 1270 | } 1271 | assert!(parse.month_md_hms("not-date-time").is_none()); 1272 | } 1273 | 1274 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1275 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1276 | fn month_mdy_hms() { 1277 | let parse = Parse::new(&Utc, None); 1278 | 1279 | let test_cases = [ 1280 | ( 1281 | "May 8, 2009 5:57:51 PM", 1282 | Utc.ymd(2009, 5, 8).and_hms(17, 57, 51), 1283 | ), 1284 | ( 1285 | "September 17, 2012 10:09am", 1286 | Utc.ymd(2012, 9, 17).and_hms(10, 9, 0), 1287 | ), 1288 | ( 1289 | "September 17, 2012, 10:10:09", 1290 | Utc.ymd(2012, 9, 17).and_hms(10, 10, 9), 1291 | ), 1292 | ]; 1293 | 1294 | for &(input, want) in test_cases.iter() { 1295 | assert_eq!( 1296 | parse.month_mdy_hms(input).unwrap().unwrap(), 1297 | want, 1298 | "month_mdy_hms/{}", 1299 | input 1300 | ) 1301 | } 1302 | assert!(parse.month_mdy_hms("not-date-time").is_none()); 1303 | } 1304 | 1305 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1306 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1307 | fn month_mdy_hms_z() { 1308 | let parse = Parse::new(&Utc, None); 1309 | 1310 | let test_cases = [ 1311 | ( 1312 | "May 02, 2021 15:51:31 UTC", 1313 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 31), 1314 | ), 1315 | ( 1316 | "May 02, 2021 15:51 UTC", 1317 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 0), 1318 | ), 1319 | ( 1320 | "May 26, 2021, 12:49 AM PDT", 1321 | Utc.ymd(2021, 5, 26).and_hms(7, 49, 0), 1322 | ), 1323 | ( 1324 | "September 17, 2012 at 10:09am PST", 1325 | Utc.ymd(2012, 9, 17).and_hms(18, 9, 0), 1326 | ), 1327 | ]; 1328 | 1329 | for &(input, want) in test_cases.iter() { 1330 | assert_eq!( 1331 | parse.month_mdy_hms_z(input).unwrap().unwrap(), 1332 | want, 1333 | "month_mdy_hms_z/{}", 1334 | input 1335 | ) 1336 | } 1337 | assert!(parse.month_mdy_hms_z("not-date-time").is_none()); 1338 | } 1339 | 1340 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1341 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1342 | fn month_mdy() { 1343 | let parse = Parse::new(&Utc, None); 1344 | 1345 | let test_cases = [ 1346 | ( 1347 | "May 25, 2021", 1348 | Utc.ymd(2021, 5, 25).and_time(Utc::now().time()), 1349 | ), 1350 | ( 1351 | "oct 7, 1970", 1352 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1353 | ), 1354 | ( 1355 | "oct 7, 70", 1356 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1357 | ), 1358 | ( 1359 | "oct. 7, 1970", 1360 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1361 | ), 1362 | ( 1363 | "oct. 7, 70", 1364 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1365 | ), 1366 | ( 1367 | "October 7, 1970", 1368 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1369 | ), 1370 | ]; 1371 | 1372 | for &(input, want) in test_cases.iter() { 1373 | assert_eq!( 1374 | parse 1375 | .month_mdy(input) 1376 | .unwrap() 1377 | .unwrap() 1378 | .trunc_subsecs(0) 1379 | .with_second(0) 1380 | .unwrap(), 1381 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1382 | "month_mdy/{}", 1383 | input 1384 | ) 1385 | } 1386 | assert!(parse.month_mdy("not-date-time").is_none()); 1387 | } 1388 | 1389 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1390 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1391 | fn month_dmy_hms() { 1392 | let parse = Parse::new(&Utc, None); 1393 | 1394 | let test_cases = [ 1395 | ( 1396 | "12 Feb 2006, 19:17", 1397 | Utc.ymd(2006, 2, 12).and_hms(19, 17, 0), 1398 | ), 1399 | ("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)), 1400 | ( 1401 | "14 May 2019 19:11:40.164", 1402 | Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164), 1403 | ), 1404 | ]; 1405 | 1406 | for &(input, want) in test_cases.iter() { 1407 | assert_eq!( 1408 | parse.month_dmy_hms(input).unwrap().unwrap(), 1409 | want, 1410 | "month_dmy_hms/{}", 1411 | input 1412 | ) 1413 | } 1414 | assert!(parse.month_dmy_hms("not-date-time").is_none()); 1415 | } 1416 | 1417 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1418 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1419 | fn month_dmy() { 1420 | let parse = Parse::new(&Utc, None); 1421 | 1422 | let test_cases = [ 1423 | ("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())), 1424 | ( 1425 | "7 oct 1970", 1426 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1427 | ), 1428 | ( 1429 | "03 February 2013", 1430 | Utc.ymd(2013, 2, 3).and_time(Utc::now().time()), 1431 | ), 1432 | ( 1433 | "1 July 2013", 1434 | Utc.ymd(2013, 7, 1).and_time(Utc::now().time()), 1435 | ), 1436 | ]; 1437 | 1438 | for &(input, want) in test_cases.iter() { 1439 | assert_eq!( 1440 | parse 1441 | .month_dmy(input) 1442 | .unwrap() 1443 | .unwrap() 1444 | .trunc_subsecs(0) 1445 | .with_second(0) 1446 | .unwrap(), 1447 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1448 | "month_dmy/{}", 1449 | input 1450 | ) 1451 | } 1452 | assert!(parse.month_dmy("not-date-time").is_none()); 1453 | } 1454 | 1455 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1456 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1457 | fn slash_mdy_hms() { 1458 | let parse = Parse::new(&Utc, None); 1459 | 1460 | let test_cases = vec![ 1461 | ("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1462 | ("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1463 | ("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1464 | ("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1465 | ("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1466 | ( 1467 | "8/8/1965 01:00:01 PM", 1468 | Utc.ymd(1965, 8, 8).and_hms(13, 0, 1), 1469 | ), 1470 | ("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1471 | ("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1472 | ("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1473 | ("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1474 | ( 1475 | "03/19/2012 10:11:59", 1476 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 1477 | ), 1478 | ( 1479 | "03/19/2012 10:11:59.3186369", 1480 | Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900), 1481 | ), 1482 | ]; 1483 | 1484 | for &(input, want) in test_cases.iter() { 1485 | assert_eq!( 1486 | parse.slash_mdy_hms(input).unwrap().unwrap(), 1487 | want, 1488 | "slash_mdy_hms/{}", 1489 | input 1490 | ) 1491 | } 1492 | assert!(parse.slash_mdy_hms("not-date-time").is_none()); 1493 | } 1494 | 1495 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1496 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1497 | fn slash_mdy() { 1498 | let parse = Parse::new(&Utc, None); 1499 | 1500 | let test_cases = [ 1501 | ( 1502 | "3/31/2014", 1503 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1504 | ), 1505 | ( 1506 | "03/31/2014", 1507 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1508 | ), 1509 | ("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())), 1510 | ("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())), 1511 | ]; 1512 | 1513 | for &(input, want) in test_cases.iter() { 1514 | assert_eq!( 1515 | parse 1516 | .slash_mdy(input) 1517 | .unwrap() 1518 | .unwrap() 1519 | .trunc_subsecs(0) 1520 | .with_second(0) 1521 | .unwrap(), 1522 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1523 | "slash_mdy/{}", 1524 | input 1525 | ) 1526 | } 1527 | assert!(parse.slash_mdy("not-date-time").is_none()); 1528 | } 1529 | 1530 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1531 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1532 | fn hyphen_mdy() { 1533 | let parse = Parse::new(&Utc, None); 1534 | 1535 | let test_cases = [ 1536 | ( 1537 | "3-31-2014", 1538 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1539 | ), 1540 | ( 1541 | "03-31-2014", 1542 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1543 | ), 1544 | ("08-21-71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())), 1545 | ("8-1-71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())), 1546 | ]; 1547 | 1548 | for &(input, want) in test_cases.iter() { 1549 | assert_eq!( 1550 | parse 1551 | .hyphen_mdy(input) 1552 | .unwrap() 1553 | .unwrap() 1554 | .trunc_subsecs(0) 1555 | .with_second(0) 1556 | .unwrap(), 1557 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1558 | "hyphen_mdy/{}", 1559 | input 1560 | ) 1561 | } 1562 | assert!(parse.hyphen_mdy("not-date-time").is_none()); 1563 | } 1564 | 1565 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1566 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1567 | fn hyphen_mdy_hms() { 1568 | let parse = Parse::new(&Utc, None); 1569 | 1570 | let test_cases = vec![ 1571 | ("4-8-2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1572 | ("04-08-2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1573 | ("4-8-14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1574 | ("04-2-2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1575 | ("8-8-1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1576 | ( 1577 | "8-8-1965 01:00:01 PM", 1578 | Utc.ymd(1965, 8, 8).and_hms(13, 0, 1), 1579 | ), 1580 | ("8-8-1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1581 | ("8-8-1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1582 | ("8-8-1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1583 | ("4-02-2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1584 | ( 1585 | "03-19-2012 10:11:59", 1586 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 1587 | ), 1588 | ( 1589 | "03-19-2012 10:11:59.3186369", 1590 | Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900), 1591 | ), 1592 | ]; 1593 | 1594 | for &(input, want) in test_cases.iter() { 1595 | assert_eq!( 1596 | parse.hyphen_mdy_hms(input).unwrap().unwrap(), 1597 | want, 1598 | "hyphen_mdy_hms/{}", 1599 | input 1600 | ) 1601 | } 1602 | assert!(parse.hyphen_mdy_hms("not-date-time").is_none()); 1603 | } 1604 | 1605 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1606 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1607 | fn slash_ymd_hms() { 1608 | let parse = Parse::new(&Utc, None); 1609 | 1610 | let test_cases = [ 1611 | ("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1612 | ("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1613 | ("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1614 | ("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1615 | ( 1616 | "2012/03/19 10:11:59", 1617 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 1618 | ), 1619 | ( 1620 | "2012/03/19 10:11:59.3186369", 1621 | Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900), 1622 | ), 1623 | ]; 1624 | 1625 | for &(input, want) in test_cases.iter() { 1626 | assert_eq!( 1627 | parse.slash_ymd_hms(input).unwrap().unwrap(), 1628 | want, 1629 | "slash_ymd_hms/{}", 1630 | input 1631 | ) 1632 | } 1633 | assert!(parse.slash_ymd_hms("not-date-time").is_none()); 1634 | } 1635 | 1636 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1637 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1638 | fn slash_ymd() { 1639 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1640 | 1641 | let test_cases = [ 1642 | ( 1643 | "2014/3/31", 1644 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1645 | ), 1646 | ( 1647 | "2014/03/31", 1648 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1649 | ), 1650 | ]; 1651 | 1652 | for &(input, want) in test_cases.iter() { 1653 | assert_eq!( 1654 | parse 1655 | .slash_ymd(input) 1656 | .unwrap() 1657 | .unwrap() 1658 | .trunc_subsecs(0) 1659 | .with_second(0) 1660 | .unwrap(), 1661 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1662 | "slash_ymd/{}", 1663 | input 1664 | ) 1665 | } 1666 | assert!(parse.slash_ymd("not-date-time").is_none()); 1667 | } 1668 | 1669 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1670 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1671 | fn dot_mdy_or_ymd() { 1672 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1673 | 1674 | let test_cases = [ 1675 | // mm.dd.yyyy 1676 | ( 1677 | "3.31.2014", 1678 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1679 | ), 1680 | ( 1681 | "03.31.2014", 1682 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1683 | ), 1684 | ("08.21.71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())), 1685 | // yyyy.mm.dd 1686 | ( 1687 | "2014.03.30", 1688 | Utc.ymd(2014, 3, 30).and_time(Utc::now().time()), 1689 | ), 1690 | ( 1691 | "2014.03", 1692 | Utc.ymd(2014, 3, Utc::now().day()) 1693 | .and_time(Utc::now().time()), 1694 | ), 1695 | ]; 1696 | 1697 | for &(input, want) in test_cases.iter() { 1698 | assert_eq!( 1699 | parse 1700 | .dot_mdy_or_ymd(input) 1701 | .unwrap() 1702 | .unwrap() 1703 | .trunc_subsecs(0) 1704 | .with_second(0) 1705 | .unwrap(), 1706 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1707 | "dot_mdy_or_ymd/{}", 1708 | input 1709 | ) 1710 | } 1711 | assert!(parse.dot_mdy_or_ymd("not-date-time").is_none()); 1712 | } 1713 | 1714 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1715 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1716 | fn mysql_log_timestamp() { 1717 | let parse = Parse::new(&Utc, None); 1718 | 1719 | let test_cases = [ 1720 | // yymmdd hh:mm:ss mysql log 1721 | ("171113 14:14:20", Utc.ymd(2017, 11, 13).and_hms(14, 14, 20)), 1722 | ]; 1723 | 1724 | for &(input, want) in test_cases.iter() { 1725 | assert_eq!( 1726 | parse.mysql_log_timestamp(input).unwrap().unwrap(), 1727 | want, 1728 | "mysql_log_timestamp/{}", 1729 | input 1730 | ) 1731 | } 1732 | assert!(parse.mysql_log_timestamp("not-date-time").is_none()); 1733 | } 1734 | 1735 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1736 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1737 | fn chinese_ymd_hms() { 1738 | let parse = Parse::new(&Utc, None); 1739 | 1740 | let test_cases = [( 1741 | "2014年04月08日11时25分18秒", 1742 | Utc.ymd(2014, 4, 8).and_hms(11, 25, 18), 1743 | )]; 1744 | 1745 | for &(input, want) in test_cases.iter() { 1746 | assert_eq!( 1747 | parse.chinese_ymd_hms(input).unwrap().unwrap(), 1748 | want, 1749 | "chinese_ymd_hms/{}", 1750 | input 1751 | ) 1752 | } 1753 | assert!(parse.chinese_ymd_hms("not-date-time").is_none()); 1754 | } 1755 | 1756 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] 1757 | #[cfg_attr(not(target_arch = "wasm32"), test)] 1758 | fn chinese_ymd() { 1759 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1760 | 1761 | let test_cases = [( 1762 | "2014年04月08日", 1763 | Utc.ymd(2014, 4, 8).and_time(Utc::now().time()), 1764 | )]; 1765 | 1766 | for &(input, want) in test_cases.iter() { 1767 | assert_eq!( 1768 | parse 1769 | .chinese_ymd(input) 1770 | .unwrap() 1771 | .unwrap() 1772 | .trunc_subsecs(0) 1773 | .with_second(0) 1774 | .unwrap(), 1775 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1776 | "chinese_ymd/{}", 1777 | input 1778 | ) 1779 | } 1780 | assert!(parse.chinese_ymd("not-date-time").is_none()); 1781 | } 1782 | } 1783 | --------------------------------------------------------------------------------