speedate-0.15.0/.cargo_vcs_info.json0000644000000001360000000000100127110ustar { "git": { "sha1": "5128eaaac103ed2cd873a7ca7d52613ab5265a24" }, "path_in_vcs": "" }speedate-0.15.0/.codecov.yml000064400000000000000000000002231046102023000137210ustar 00000000000000coverage: precision: 2 range: [90, 100] status: patch: false project: false comment: layout: 'header, diff, flags, files, footer' speedate-0.15.0/.github/FUNDING.yml000064400000000000000000000000251046102023000146530ustar 00000000000000github: samuelcolvin speedate-0.15.0/.github/dependabot.yml000064400000000000000000000005171046102023000156740ustar 00000000000000version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "monthly" groups: rust-deps: patterns: - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" groups: actions: patterns: - "*" speedate-0.15.0/.github/workflows/ci.yml000064400000000000000000000045001046102023000162130ustar 00000000000000name: CI on: push: branches: - main tags: - '**' pull_request: {} jobs: test: name: test rust-${{ matrix.rust-version }} strategy: fail-fast: false matrix: rust-version: [stable, beta, nightly] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust-version }} - id: cache-rust uses: Swatinem/rust-cache@v2 - run: cargo install rustfilt coverage-prepare if: steps.cache-rust.outputs.cache-hit != 'true' - run: rustup component add llvm-tools-preview - run: cargo test --test main env: RUST_BACKTRACE: 1 RUSTFLAGS: '-C instrument-coverage' - run: coverage-prepare --ignore-filename-regex '/tests/' lcov $(find target/debug/deps -regex '.*/main[^.]*') - run: cargo test --doc - uses: codecov/codecov-action@v4 bench: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly - uses: Swatinem/rust-cache@v2 - run: cargo bench lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: install rust stable uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - uses: actions/setup-python@v5 with: python-version: '3.11' - uses: pre-commit/action@v3.0.1 with: extra_args: --all-files --verbose env: PRE_COMMIT_COLOR: always SKIP: test - run: cargo doc # https://github.com/marketplace/actions/alls-green#why used for branch protection checks check: if: always() needs: [test, bench, lint] runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} release: needs: [check] if: "success() && startsWith(github.ref, 'refs/tags/')" runs-on: ubuntu-latest environment: release steps: - uses: actions/checkout@v4 - name: install rust stable uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo publish env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} speedate-0.15.0/.gitignore000064400000000000000000000001631046102023000134710ustar 00000000000000/target/ /Cargo.lock /.idea/ /sandbox/ /htmlcov/ /default.profraw /default.profdata /*.profraw /*.profdata /*.lcov speedate-0.15.0/.pre-commit-config.yaml000064400000000000000000000013061046102023000157620ustar 00000000000000fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: check-yaml - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace - id: check-added-large-files - repo: local hooks: - id: format-check name: Format Check entry: cargo fmt -- --check types: [rust] language: system pass_filenames: false - id: clippy name: Clippy entry: cargo clippy -- -D warnings -A incomplete_features -W clippy::dbg_macro -W clippy::print_stdout types: [rust] language: system pass_filenames: false - id: test name: Test entry: cargo test types: [rust] language: system pass_filenames: false speedate-0.15.0/.rustfmt.toml000064400000000000000000000000201046102023000141500ustar 00000000000000max_width = 120 speedate-0.15.0/Cargo.toml0000644000000025760000000000100107210ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "speedate" version = "0.15.0" authors = ["Samuel Colvin "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Fast and simple datetime, date, time and duration parsing" homepage = "https://github.com/pydantic/speedate/" readme = "README.md" keywords = [ "ISO8601", "RFC3339", "datetime", "date", "time", ] categories = [ "date-and-time", "parsing", ] license = "MIT" repository = "https://github.com/pydantic/speedate/" [lib] name = "speedate" path = "src/lib.rs" [[test]] name = "main" path = "tests/main.rs" [[bench]] name = "main" path = "benches/main.rs" [dependencies.strum] version = "0.26" features = ["derive"] [dependencies.strum_macros] version = "0.26" [dev-dependencies.chrono] version = "0.4.19" [dev-dependencies.iso8601] version = "0.6.1" [dev-dependencies.paste] version = "1.0.7" speedate-0.15.0/Cargo.toml.orig000064400000000000000000000011201046102023000143620ustar 00000000000000[package] name = "speedate" authors = ["Samuel Colvin "] version = "0.15.0" edition = "2021" description = "Fast and simple datetime, date, time and duration parsing" readme = "README.md" license = "MIT" keywords = ["ISO8601", "RFC3339", "datetime", "date", "time"] categories = ["date-and-time", "parsing"] homepage = "https://github.com/pydantic/speedate/" repository = "https://github.com/pydantic/speedate/" [dependencies] strum = { version = "0.26", features = ["derive"] } strum_macros = "0.26" [dev-dependencies] chrono = "0.4.19" iso8601 = "0.6.1" paste = "1.0.7" speedate-0.15.0/LICENSE000064400000000000000000000020701046102023000125050ustar 00000000000000The MIT License (MIT) Copyright (c) 2022 Samuel Colvin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. speedate-0.15.0/Makefile000064400000000000000000000015171046102023000131450ustar 00000000000000.DEFAULT_GOAL := all .PHONY: install-rust-coverage install-rust-coverage: cargo install rustfilt cargo-binutils rustup component add llvm-tools-preview .PHONY: build-prod build-prod: cargo build --release .PHONY: format format: cargo fmt .PHONY: lint lint: cargo fmt --version cargo fmt --all -- --check cargo clippy --version cargo clippy -- -D warnings -A incomplete_features cargo doc .PHONY: test test: RUSTFLAGS='-Z macro-backtrace' cargo test .PHONY: bench bench: cargo bench .PHONY: testcov testcov: RUSTFLAGS='-C instrument-coverage' cargo test --test main coverage-prepare --ignore-filename-regex '/tests/' lcov $(shell find target/debug/deps -regex '.*/main[^.]*') genhtml rust_coverage.lcov --output-directory htmlcov @echo "HTML coverage report available at htmlcov/index.html" .PHONY: all all: format lint test speedate-0.15.0/README.md000064400000000000000000000116471046102023000127710ustar 00000000000000# speedate [![CI](https://github.com/pydantic/speedate/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/speedate/actions/workflows/ci.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/pydantic/speedate/branch/main/graph/badge.svg)](https://codecov.io/gh/pydantic/speedate) [![Crates.io](https://img.shields.io/crates/v/speedate?color=green)](https://crates.io/crates/speedate) Fast and simple datetime, date, time and duration parsing for rust. **speedate** is a lax† **RFC 3339** date and time parser, in other words, it parses common **ISO 8601** formats. **†** - all relaxations of from [RFC 3339](https://tools.ietf.org/html/rfc3339) are compliant with [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). The following formats are supported: * Date: `YYYY-MM-DD` * Time: `HH:MM:SS` * Time: `HH:MM:SS.FFFFFF` 1 to 6 digits are reflected in the `time.microsecond`, extra digits are ignored * Time: `HH:MM` * Date time: `YYYY-MM-DDTHH:MM:SS` - all the above time formats are allowed for the time part * Date time: `YYYY-MM-DD HH:MM:SS` - `T`, `t`, ` ` and `_` are allowed as separators * Date time: `YYYY-MM-DDTHH:MM:SSZ` - `Z` or `z` is allowed as timezone * Date time: `YYYY-MM-DDTHH:MM:SS+08:00`- positive and negative timezone are allowed, as per ISO 8601, U+2212 minus `−` is allowed as well as ascii minus `-` (U+002D) * Date time: `YYYY-MM-DDTHH:MM:SS+0800` - the colon (`:`) in the timezone is optional * Duration: `PnYnMnDTnHnMnS` - ISO 8601 duration format, see [wikipedia](https://en.wikipedia.org/wiki/ISO_8601#Durations) for more details, `W` for weeks is also allowed * Duration: `HH:MM:SS` - any of the above time formats are allowed to represent a duration * Duration: `D days, HH:MM:SS` - time prefixed by `X days`, case-insensitive, spaces `s` and `,` are all optional * Duration: `D d, HH:MM:SS` - time prefixed by `X d`, case-insensitive, spaces and `,` are optional * Duration: `±...` - all duration formats shown here can be prefixed with `+` or `-` to indicate positive and negative durations respectively In addition, unix timestamps (both seconds and milliseconds) can be used to create dates and datetimes. See [the documentation](https://docs.rs/speedate/latest/speedate/index.html#structs) for each struct for more details. This will be the datetime parsing logic for [pydantic-core](https://github.com/pydantic/pydantic-core). ## Usage ```rust use speedate::{DateTime, Date, Time}; let dt = DateTime::parse_str("2022-01-01T12:13:14Z").unwrap(); assert_eq!( dt, DateTime { date: Date { year: 2022, month: 1, day: 1, }, time: Time { hour: 12, minute: 13, second: 14, microsecond: 0, tz_offset: Some(0), }, } ); assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); ``` To control the specifics of time parsing you can use provide a `TimeConfig`: ```rust use speedate::{DateTime, Date, Time, TimeConfig, MicrosecondsPrecisionOverflowBehavior}; let dt = DateTime::parse_bytes_with_config( "1689102037.5586429".as_bytes(), &TimeConfig::builder() .unix_timestamp_offset(Some(0)) .microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate) .build(), ).unwrap(); assert_eq!( dt, DateTime { date: Date { year: 2023, month: 7, day: 11, }, time: Time { hour: 19, minute: 0, second: 37, microsecond: 558643, tz_offset: Some(0), }, } ); assert_eq!(dt.to_string(), "2023-07-11T19:00:37.558643Z"); ``` ## Performance **speedate** is significantly faster than [chrono's `parse_from_rfc3339`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html#method.parse_from_rfc3339) and [iso8601](https://crates.io/crates/iso8601). Micro-benchmarking from [`benches/main.rs`](https://github.com/pydantic/speedate/blob/main/benches/main.rs): ```text test datetime_error_speedate ... bench: 6 ns/iter (+/- 0) test datetime_error_chrono ... bench: 50 ns/iter (+/- 1) test datetime_error_iso8601 ... bench: 118 ns/iter (+/- 2) test datetime_ok_speedate ... bench: 9 ns/iter (+/- 0) test datetime_ok_chrono ... bench: 182 ns/iter (+/- 0) test datetime_ok_iso8601 ... bench: 77 ns/iter (+/- 1) test duration_ok_speedate ... bench: 23 ns/iter (+/- 0) test duration_ok_iso8601 ... bench: 48 ns/iter (+/- 0) test timestamp_ok_speedate ... bench: 9 ns/iter (+/- 0) test timestamp_ok_chrono ... bench: 10 ns/iter (+/- 0) ``` ## Why not full iso8601? ISO8601 allows many formats, see [ijmacd.github.io/rfc3339-iso8601](https://ijmacd.github.io/rfc3339-iso8601/). Most of these are unknown to most users, and not desired. This library aims to support the most common formats without introducing ambiguity. speedate-0.15.0/benches/main.rs000064400000000000000000000147061046102023000144120ustar 00000000000000#![feature(test)] extern crate test; use speedate::{Date, DateTime, Duration, Time}; use test::{black_box, Bencher}; #[bench] fn compare_datetime_ok_speedate(bench: &mut Bencher) { let s = black_box("2000-01-01T00:02:03Z"); bench.iter(|| { let dt = DateTime::parse_str(s).unwrap(); black_box(( dt.date.year, dt.date.month, dt.date.day, dt.time.hour, dt.time.minute, dt.time.second, dt.time.microsecond, )); }) } #[bench] fn compare_datetime_ok_iso8601(bench: &mut Bencher) { let s = black_box("2000-01-01T00:02:03Z"); bench.iter(|| { // No way to actually get the numeric values from iso8601! black_box(iso8601::datetime(s).unwrap()); }) } #[bench] fn compare_datetime_ok_chrono(bench: &mut Bencher) { use chrono::{Datelike, Timelike}; let s = black_box("2000-01-01T00:02:03Z"); bench.iter(|| { let dt = chrono::DateTime::parse_from_rfc3339(s).unwrap(); black_box(( dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second(), dt.nanosecond(), )); }) } #[bench] fn compare_duration_ok_speedate(bench: &mut Bencher) { let s = black_box("P1Y2M3DT4H5M6S"); bench.iter(|| { black_box(Duration::parse_str(s).unwrap()); }) } #[bench] fn compare_duration_ok_iso8601(bench: &mut Bencher) { let s = black_box("P1Y2M3DT4H5M6S"); bench.iter(|| { black_box(iso8601::duration(s).unwrap()); }) } macro_rules! expect_error { ($expr:expr) => { match $expr { Ok(t) => panic!("unexpectedly valid: {:?}", t), Err(e) => e, } }; } #[bench] fn compare_datetime_error_speedate(bench: &mut Bencher) { let s = black_box("2000-01-01T25:02:03Z"); bench.iter(|| { let e = expect_error!(DateTime::parse_str(s)); black_box(e); }) } #[bench] fn compare_datetime_error_iso8601(bench: &mut Bencher) { let s = black_box("2000-01-01T25:02:03Z"); bench.iter(|| { let e = expect_error!(iso8601::datetime(s)); black_box(e); }) } #[bench] fn compare_datetime_error_chrono(bench: &mut Bencher) { let s = black_box("2000-01-01T25:02:03Z"); bench.iter(|| { let e = expect_error!(chrono::DateTime::parse_from_rfc3339(s)); black_box(e); }) } #[bench] fn compare_timestamp_ok_speedate(bench: &mut Bencher) { let ts = black_box(1654617803); bench.iter(|| { let dt = DateTime::from_timestamp(ts, 0).unwrap(); black_box(( dt.date.year, dt.date.month, dt.date.day, dt.time.hour, dt.time.minute, dt.time.second, dt.time.microsecond, )); }) } #[bench] fn compare_timestamp_ok_chrono(bench: &mut Bencher) { use chrono::{Datelike, Timelike}; let ts = black_box(1654617803); bench.iter(|| { let dt = chrono::NaiveDateTime::from_timestamp_opt(ts, 0).unwrap(); black_box(( dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second(), dt.nanosecond(), )); }) } #[bench] fn dt_custom_tz(bench: &mut Bencher) { let s = black_box("1997-09-09T09:09:09-09:09"); bench.iter(|| { black_box(DateTime::parse_str(s).unwrap()); }) } #[bench] fn dt_naive(bench: &mut Bencher) { let s = black_box("1997-09-09T09:09:09"); bench.iter(|| { black_box(DateTime::parse_str(s).unwrap()); }) } #[bench] fn date(bench: &mut Bencher) { let s = black_box("1997-09-09"); bench.iter(|| { black_box(Date::parse_str(s).unwrap()); }) } #[bench] fn time(bench: &mut Bencher) { let s = black_box("09:09:09.09"); bench.iter(|| { black_box(Time::parse_str(s).unwrap()); }) } #[bench] fn x_combined(bench: &mut Bencher) { let dt1 = black_box("1997-09-09T09:09:09Z"); let dt2 = black_box("1997-09-09 09:09:09"); let dt3 = black_box("2000-02-29 01:01:50.123456"); let dt4 = black_box("2000-02-29 01:01:50.123456+08:00"); let d1 = black_box("1997-09-09"); let d2 = black_box("2000-02-29"); let d3 = black_box("2001-02-28"); let d4 = black_box("2001-12-28"); let t1 = black_box("12:13"); let t2 = black_box("12:13:14"); let t3 = black_box("12:13:14.123"); let t4 = black_box("12:13:14.123456"); bench.iter(|| { black_box(DateTime::parse_str(dt1).unwrap()); black_box(DateTime::parse_str(dt2).unwrap()); black_box(DateTime::parse_str(dt3).unwrap()); black_box(DateTime::parse_str(dt4).unwrap()); black_box(Date::parse_str(d1).unwrap()); black_box(Date::parse_str(d2).unwrap()); black_box(Date::parse_str(d3).unwrap()); black_box(Date::parse_str(d4).unwrap()); black_box(Time::parse_str(t1).unwrap()); black_box(Time::parse_str(t2).unwrap()); black_box(Time::parse_str(t3).unwrap()); black_box(Time::parse_str(t4).unwrap()); }) } #[bench] fn format_date(bench: &mut Bencher) { let date = black_box(Date { year: 2022, month: 7, day: 10, }); bench.iter(|| { black_box(date.to_string()); }) } #[bench] fn format_time(bench: &mut Bencher) { let time = black_box(Time { hour: 10, minute: 11, second: 12, microsecond: 11, tz_offset: None, }); bench.iter(|| { black_box(time.to_string()); }) } #[bench] fn format_date_time(bench: &mut Bencher) { let date = black_box(DateTime { date: Date { year: 2022, month: 7, day: 10, }, time: Time { hour: 0, minute: 0, second: 0, microsecond: 0, tz_offset: Some(60), }, }); bench.iter(|| { black_box(date.to_string()); }) } #[bench] fn parse_timestamp_str(bench: &mut Bencher) { let timestamps = black_box([ "1654646400", "-1654646400", "1654646404", "-1654646404", "1654646404.5", "1654646404.123456", "1654646404000.5", "1654646404123.456", "-1654646404.123456", "-1654646404000.123", ]); bench.iter(|| { for timestamp in ×tamps { black_box(DateTime::parse_str(black_box(*timestamp)).unwrap()); } }); } speedate-0.15.0/src/date.rs000064400000000000000000000326001046102023000135540ustar 00000000000000use std::fmt; use std::str::FromStr; use crate::numbers::int_parse_bytes; use crate::{get_digit_unchecked, DateTime, ParseError}; /// A Date /// /// Allowed formats: /// * `YYYY-MM-DD` /// /// Leap years are correct calculated according to the Gregorian calendar. /// Thus `2000-02-29` is a valid date, but `2001-02-29` is not. /// /// # Comparison /// /// `Date` supports equality (`==`) and inequality (`>`, `<`, `>=`, `<=`) comparisons. /// /// ``` /// use speedate::Date; /// /// let d1 = Date::parse_str("2022-01-01").unwrap(); /// let d2 = Date::parse_str("2022-01-02").unwrap(); /// assert!(d2 > d1); /// ``` #[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] pub struct Date { /// Year: four digits pub year: u16, /// Month: 1 to 12 pub month: u8, /// Day: 1 to {28, 29, 30, 31} (based on month & year) pub day: u8, } impl fmt::Display for Date { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut buf: [u8; 10] = *b"0000-00-00"; crate::display_num_buf(4, 0, self.year as u32, &mut buf); crate::display_num_buf(2, 5, self.month as u32, &mut buf); crate::display_num_buf(2, 8, self.day as u32, &mut buf); f.write_str(std::str::from_utf8(&buf[..]).unwrap()) } } impl FromStr for Date { type Err = ParseError; #[inline] fn from_str(s: &str) -> Result { // Delegate to parse_str, which is more permissive - users can call parse_str_rfc3339 directly instead if they // want to be stricter Self::parse_str(s) } } // 2e10 if greater than this, the number is in ms, if less than or equal, it's in seconds // (in seconds this is 11th October 2603, in ms it's 20th August 1970) pub(crate) const MS_WATERSHED: i64 = 20_000_000_000; // 9999-12-31T23:59:59 as a unix timestamp, used as max allowed value below const UNIX_9999: i64 = 253_402_300_799; // 0000-01-01T00:00:00+00:00 as a unix timestamp, used as min allowed value below const UNIX_0000: i64 = -62_167_219_200; impl Date { /// Parse a date from a string using RFC 3339 format /// /// # Arguments /// /// * `str` - The string to parse /// /// # Examples /// /// ``` /// use speedate::Date; /// /// let d = Date::parse_str_rfc3339("2020-01-01").unwrap(); /// assert_eq!( /// d, /// Date { /// year: 2020, /// month: 1, /// day: 1 /// } /// ); /// assert_eq!(d.to_string(), "2020-01-01"); /// ``` #[inline] pub fn parse_str_rfc3339(str: &str) -> Result { Self::parse_bytes_rfc3339(str.as_bytes()) } /// Parse a date from a string using RFC 3339 format, or a unix timestamp. /// /// In the input is purely numeric, then the number is interpreted as a unix timestamp, /// using [`Date::from_timestamp`]. /// /// # Arguments /// /// * `str` - The string to parse /// /// # Examples /// /// ``` /// use speedate::Date; /// /// let d = Date::parse_str("2020-01-01").unwrap(); /// assert_eq!(d.to_string(), "2020-01-01"); /// let d = Date::parse_str("1577836800").unwrap(); /// assert_eq!(d.to_string(), "2020-01-01"); /// ``` #[inline] pub fn parse_str(str: &str) -> Result { Self::parse_bytes(str.as_bytes()) } /// Parse a date from bytes using RFC 3339 format /// /// # Arguments /// /// * `bytes` - The bytes to parse /// /// # Examples /// /// ``` /// use speedate::Date; /// /// let d = Date::parse_bytes_rfc3339(b"2020-01-01").unwrap(); /// assert_eq!( /// d, /// Date { /// year: 2020, /// month: 1, /// day: 1 /// } /// ); /// assert_eq!(d.to_string(), "2020-01-01"); /// ``` #[inline] pub fn parse_bytes_rfc3339(bytes: &[u8]) -> Result { let d = Self::parse_bytes_partial(bytes)?; if bytes.len() > 10 { return Err(ParseError::ExtraCharacters); } Ok(d) } /// Parse a date from bytes using RFC 3339 format, or a unix timestamp. /// /// In the input is purely numeric, then the number is interpreted as a unix timestamp, /// using [`Date::from_timestamp`]. /// /// # Arguments /// /// * `bytes` - The bytes to parse /// /// # Examples /// /// ``` /// use speedate::Date; /// /// let d = Date::parse_bytes(b"2020-01-01").unwrap(); /// assert_eq!(d.to_string(), "2020-01-01"); /// /// let d = Date::parse_bytes(b"1577836800").unwrap(); /// assert_eq!(d.to_string(), "2020-01-01"); /// ``` #[inline] pub fn parse_bytes(bytes: &[u8]) -> Result { match Self::parse_bytes_rfc3339(bytes) { Ok(d) => Ok(d), Err(e) => match int_parse_bytes(bytes) { Some(int) => Self::from_timestamp(int, true), None => Err(e), }, } } /// Create a date from a Unix Timestamp in seconds or milliseconds /// /// ("Unix Timestamp" means number of seconds or milliseconds since 1970-01-01) /// /// Input must be between `-62,167,219,200,000` (`0000-01-01`) and `253,402,300,799,000` (`9999-12-31`) inclusive. /// /// If the absolute value is > 2e10 (`20,000,000,000`) it is interpreted as being in milliseconds. /// /// That means: /// * `20,000,000,000` is `2603-10-11` /// * `20,000,000,001` is `1970-08-20` /// * `-62,167,219,200,001` gives an error - `DateTooSmall` as it would be before 0000-01-01 /// * `-20,000,000,001` is `1969-05-14` /// * `-20,000,000,000` is `1336-03-23` /// /// # Arguments /// /// * `timestamp` - timestamp in either seconds or milliseconds /// * `require_exact` - if true, then the timestamp must be exactly at midnight, otherwise it will be rounded down /// /// # Examples /// /// ``` /// use speedate::Date; /// /// let d = Date::from_timestamp(1_654_560_000, true).unwrap(); /// assert_eq!(d.to_string(), "2022-06-07"); /// ``` pub fn from_timestamp(timestamp: i64, require_exact: bool) -> Result { let (seconds, microseconds) = Self::timestamp_watershed(timestamp)?; let (d, remaining_seconds) = Self::from_timestamp_calc(seconds)?; if require_exact && (remaining_seconds != 0 || microseconds != 0) { return Err(ParseError::DateNotExact); } Ok(d) } /// Unix timestamp in seconds (number of seconds between self and 1970-01-01) /// /// # Example /// /// ``` /// use speedate::Date; /// /// let d = Date::parse_str("2022-06-07").unwrap(); /// assert_eq!(d.timestamp(), 1_654_560_000); /// ``` pub fn timestamp(&self) -> i64 { let days = (self.year as i64) * 365 + (self.ordinal_day() - 1) as i64 + intervening_leap_years(self.year as i64); days * 86400 + UNIX_0000 } /// Current date. Internally, this uses [DateTime::now]. /// /// # Arguments /// /// * `tz_offset` - timezone offset in seconds, meaning as per [DateTime::now], must be less than `86_400` /// /// # Example /// /// ``` /// use speedate::Date; /// /// let d = Date::today(0).unwrap(); /// println!("The date today is: {}", d) /// ``` pub fn today(tz_offset: i32) -> Result { Ok(DateTime::now(tz_offset)?.date) } /// Day of the year, starting from 1. #[allow(clippy::bool_to_int_with_if)] pub fn ordinal_day(&self) -> u16 { let leap_extra = if is_leap_year(self.year) { 1 } else { 0 }; let day = self.day as u16; match self.month { 1 => day, 2 => day + 31, 3 => day + 59 + leap_extra, 4 => day + 90 + leap_extra, 5 => day + 120 + leap_extra, 6 => day + 151 + leap_extra, 7 => day + 181 + leap_extra, 8 => day + 212 + leap_extra, 9 => day + 243 + leap_extra, 10 => day + 273 + leap_extra, 11 => day + 304 + leap_extra, _ => day + 334 + leap_extra, } } pub(crate) fn timestamp_watershed(timestamp: i64) -> Result<(i64, u32), ParseError> { let ts_abs = timestamp.checked_abs().ok_or(ParseError::DateTooSmall)?; if ts_abs <= MS_WATERSHED { return Ok((timestamp, 0)); } let mut seconds = timestamp / 1_000; let mut microseconds = ((timestamp % 1_000) * 1000) as i32; if microseconds < 0 { seconds -= 1; microseconds += 1_000_000; } Ok((seconds, microseconds as u32)) } pub(crate) fn from_timestamp_calc(timestamp_second: i64) -> Result<(Self, u32), ParseError> { if timestamp_second < UNIX_0000 { return Err(ParseError::DateTooSmall); } if timestamp_second > UNIX_9999 { return Err(ParseError::DateTooLarge); } let seconds_diff = timestamp_second - UNIX_0000; let delta_days = seconds_diff / 86_400; let delta_years = delta_days / 365; let leap_years = intervening_leap_years(delta_years); // year day is the day of the year, starting from 1 let mut ordinal_day: i16 = (delta_days % 365 - leap_years + 1) as i16; let mut year: u16 = delta_years as u16; let mut leap_year: bool = is_leap_year(year); while ordinal_day < 1 { year -= 1; leap_year = is_leap_year(year); ordinal_day += if leap_year { 366 } else { 365 }; } let (month, day) = match leap_year { true => leap_year_month_day(ordinal_day), false => common_year_month_day(ordinal_day), }; Ok((Self { year, month, day }, (timestamp_second.rem_euclid(86_400)) as u32)) } /// Parse a date from bytes, no check is performed for extract characters at the end of the string pub(crate) fn parse_bytes_partial(bytes: &[u8]) -> Result { if bytes.len() < 10 { return Err(ParseError::TooShort); } let year: u16; let month: u8; let day: u8; unsafe { let y1 = get_digit_unchecked!(bytes, 0, InvalidCharYear) as u16; let y2 = get_digit_unchecked!(bytes, 1, InvalidCharYear) as u16; let y3 = get_digit_unchecked!(bytes, 2, InvalidCharYear) as u16; let y4 = get_digit_unchecked!(bytes, 3, InvalidCharYear) as u16; year = y1 * 1000 + y2 * 100 + y3 * 10 + y4; match bytes.get_unchecked(4) { b'-' => (), _ => return Err(ParseError::InvalidCharDateSep), } let m1 = get_digit_unchecked!(bytes, 5, InvalidCharMonth); let m2 = get_digit_unchecked!(bytes, 6, InvalidCharMonth); month = m1 * 10 + m2; match bytes.get_unchecked(7) { b'-' => (), _ => return Err(ParseError::InvalidCharDateSep), } let d1 = get_digit_unchecked!(bytes, 8, InvalidCharDay); let d2 = get_digit_unchecked!(bytes, 9, InvalidCharDay); day = d1 * 10 + d2; } // calculate the maximum number of days in the month, accounting for leap years in the // gregorian calendar let max_days = match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 => { if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) { 29 } else { 28 } } _ => return Err(ParseError::OutOfRangeMonth), }; if day < 1 || day > max_days { return Err(ParseError::OutOfRangeDay); } Ok(Self { year, month, day }) } } fn is_leap_year(year: u16) -> bool { if year % 100 == 0 { year % 400 == 0 } else { year % 4 == 0 } } /// internal function to calculate the number of leap years since 0000, `delta_years` is the number of /// years since 0000 fn intervening_leap_years(delta_years: i64) -> i64 { if delta_years == 0 { 0 } else { (delta_years - 1) / 4 - (delta_years - 1) / 100 + (delta_years - 1) / 400 + 1 } } fn leap_year_month_day(day: i16) -> (u8, u8) { match day { 1..=31 => (1, day as u8), 32..=60 => (2, day as u8 - 31), 61..=91 => (3, day as u8 - 60), 92..=121 => (4, day as u8 - 91), 122..=152 => (5, day as u8 - 121), 153..=182 => (6, day as u8 - 152), 183..=213 => (7, day as u8 - 182), 214..=244 => (8, day as u8 - 213), 245..=274 => (9, (day - 244) as u8), 275..=305 => (10, (day - 274) as u8), 306..=335 => (11, (day - 305) as u8), _ => (12, (day - 335) as u8), } } fn common_year_month_day(day: i16) -> (u8, u8) { match day { 1..=31 => (1, day as u8), 32..=59 => (2, day as u8 - 31), 60..=90 => (3, day as u8 - 59), 91..=120 => (4, day as u8 - 90), 121..=151 => (5, day as u8 - 120), 152..=181 => (6, day as u8 - 151), 182..=212 => (7, day as u8 - 181), 213..=243 => (8, day as u8 - 212), 244..=273 => (9, (day - 243) as u8), 274..=304 => (10, (day - 273) as u8), 305..=334 => (11, (day - 304) as u8), _ => (12, (day - 334) as u8), } } speedate-0.15.0/src/datetime.rs000064400000000000000000000563101046102023000144370ustar 00000000000000use crate::date::MS_WATERSHED; use crate::{ float_parse_bytes, numbers::decimal_digits, IntFloat, MicrosecondsPrecisionOverflowBehavior, TimeConfigBuilder, }; use crate::{time::TimeConfig, Date, ParseError, Time}; use std::cmp::Ordering; use std::fmt; use std::str::FromStr; use std::time::SystemTime; /// A DateTime /// /// Combines a [Date], [Time]. /// Allowed values: /// * `YYYY-MM-DDTHH:MM:SS` - all the above time formats are allowed for the time part /// * `YYYY-MM-DD HH:MM:SS` - `T`, `t`, ` ` and `_` are allowed as separators /// * `YYYY-MM-DDTHH:MM:SSZ` - `Z` or `z` is allowed as timezone /// * `YYYY-MM-DDTHH:MM:SS+08:00`- positive and negative timezone are allowed, /// as per ISO 8601, U+2212 minus `−` is allowed as well as ascii minus `-` (U+002D) /// * `YYYY-MM-DDTHH:MM:SS+0800` - the colon (`:`) in the timezone is optional /// /// # Comparison /// /// `DateTime` supports equality (`==`) and inequality (`>`, `<`, `>=`, `<=`) comparisons. /// /// See [DateTime::partial_cmp] for how this works. #[derive(Debug, PartialEq, Eq, Clone)] pub struct DateTime { /// date part of the datetime pub date: Date, /// time part of the datetime pub time: Time, } impl fmt::Display for DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.date)?; write!(f, "T")?; write!(f, "{}", self.time)?; Ok(()) } } impl FromStr for DateTime { type Err = ParseError; #[inline] fn from_str(s: &str) -> Result { // Delegate to parse_str, which is more permissive - users can call parse_str_rfc3339 directly instead if they // want to be stricter Self::parse_str(s) } } impl PartialOrd for DateTime { /// Compare two datetimes by inequality. /// /// `DateTime` supports equality (`==`, `!=`) and inequality comparisons (`>`, `<`, `>=` & `<=`). /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let dt1 = DateTime::parse_str("2020-02-03T04:05:06.07").unwrap(); /// let dt2 = DateTime::parse_str("2020-02-03T04:05:06.08").unwrap(); /// /// assert!(dt2 > dt1); /// ``` /// /// # Comparison with Timezones /// /// When comparing two datetimes, we want "less than" or "greater than" refer to "earlier" or "later" /// in the absolute course of time. We therefore need to be careful when comparing datetimes with different /// timezones. (If it wasn't for timezones, we could omit all this extra logic and thinking and just compare /// struct members directly as we do with [Date] and [crate::Duration]). /// /// From [wikipedia](https://en.wikipedia.org/wiki/UTC_offset#Time_zones_and_time_offsets) /// /// > The UTC offset is an amount of time subtracted from or added to UTC time to specify the local solar time... /// /// So, we can imagine that at 3pm in the UK (UTC+0) (in winter, to avoid DST confusion) it's 4pm in France (UTC+1). /// /// Thus to compare two datetimes in absolute terms we need to **SUBTRACT** the timezone offset. /// /// As if timezones weren't complicated enough, there are three extra considerations here: /// 1. **naïve vs. non-naïve:** We also have to consider the case where one datetime has a timezone and the other /// does not (e.g. is "timezone "naïve"). When comparing naïve datetimes to non-naïve, this library /// assumes the naïve datetime has the same timezone as the non-naïve, this is different to other /// implementations (e.g. python) where such comparisons fail. /// 2. **Direction:** As described in PostgreSQL's docs, in the POSIX Time Zone Specification /// "The positive sign is used for zones west of Greenwich", which is opposite to the ISO-8601 sign convention. /// In other words, the offset is reversed, see the end of /// [this blog](http://blog.untrod.com/2016/08/actually-understanding-timezones-in-postgresql.html) /// and the [PostgreSQL docs](https://www.postgresql.org/docs/14/datetime-posix-timezone-specs.html) for more /// info. /// 3. **Equality comparison:** None of this logic is used for equality (`==`) comparison where we can just compare /// struct members directly, e.g. require the timezone offset to be the same for two datetimes to be equal. /// /// ## Timezone Examples /// /// ``` /// use speedate::DateTime; /// /// let dt_uk_3pm = DateTime::parse_str("2000-01-01T15:00:00Z").unwrap(); /// let dt_france_4pm = DateTime::parse_str("2000-01-01T16:00:00+01:00").unwrap(); /// /// assert!(dt_uk_3pm >= dt_france_4pm); // the two dts are actually the same instant /// assert!(dt_uk_3pm <= dt_france_4pm); // the two dts are actually the same instant /// assert_ne!(dt_uk_3pm, dt_france_4pm); // no equal because timezones much match for equality /// /// let dt_uk_330pm = DateTime::parse_str("2000-01-01T15:30:00Z").unwrap(); /// /// assert!(dt_uk_330pm > dt_uk_3pm); /// assert!(dt_uk_330pm > dt_france_4pm); /// /// // as described in point 1 above, naïve datetimes are assumed to /// // have the same timezone as the non-naïve /// let dt_naive_330pm = DateTime::parse_str("2000-01-01T15:30:00").unwrap(); /// assert!(dt_uk_3pm < dt_naive_330pm); /// assert!(dt_france_4pm > dt_naive_330pm); /// ``` fn partial_cmp(&self, other: &Self) -> Option { match (self.time.tz_offset, other.time.tz_offset) { (Some(_), Some(_)) => match self.timestamp_tz().partial_cmp(&other.timestamp_tz()) { Some(Ordering::Equal) => self.time.microsecond.partial_cmp(&other.time.microsecond), otherwise => otherwise, }, _ => match self.date.partial_cmp(&other.date) { Some(Ordering::Equal) => self.time.partial_cmp(&other.time), otherwise => otherwise, }, } } } impl DateTime { /// Parse a datetime from a string /// /// # Arguments /// /// * `str` - The string to parse /// /// # Examples /// /// ``` /// use speedate::{DateTime, Date, Time}; /// /// let dt = DateTime::parse_str("2022-01-01T12:13:14Z").unwrap(); /// assert_eq!( /// dt, /// DateTime { /// date: Date { /// year: 2022, /// month: 1, /// day: 1, /// }, /// time: Time { /// hour: 12, /// minute: 13, /// second: 14, /// microsecond: 0, /// tz_offset: Some(0), /// }, /// } /// ); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// ``` /// /// With a non-zero timezone /// (we also use a different separator and omit the colon in timezone here): /// /// ``` /// use speedate::{DateTime, Date, Time}; /// /// let dt = DateTime::parse_str_rfc3339("2000-02-29 12:13:14-0830").unwrap(); /// assert_eq!( /// dt, /// DateTime { /// date: Date { /// year: 2000, /// month: 2, /// day: 29, /// }, /// time: Time { /// hour: 12, /// minute: 13, /// second: 14, /// microsecond: 0, /// tz_offset: Some(-30600), /// }, /// } /// ); /// assert_eq!(dt.to_string(), "2000-02-29T12:13:14-08:30"); /// ``` /// (note: the string representation is still canonical ISO8601) #[inline] pub fn parse_str_rfc3339(str: &str) -> Result { Self::parse_bytes_rfc3339(str.as_bytes()) } /// As with [DateTime::parse_str] but also supports unix timestamps. /// /// # Arguments /// /// * `bytes` - The bytes to parse /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let dt = DateTime::parse_str("2022-01-01T12:13:14Z").unwrap(); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// /// let dt = DateTime::parse_str("1641039194").unwrap(); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14"); /// ``` pub fn parse_str(str: &str) -> Result { Self::parse_bytes(str.as_bytes()) } /// Parse a datetime from bytes using RFC 3339 format /// /// # Arguments /// /// * `bytes` - The bytes to parse /// /// # Examples /// /// ``` /// use speedate::{DateTime, Date, Time}; /// /// let dt = DateTime::parse_bytes_rfc3339(b"2022-01-01T12:13:14Z").unwrap(); /// assert_eq!( /// dt, /// DateTime { /// date: Date { /// year: 2022, /// month: 1, /// day: 1, /// }, /// time: Time { /// hour: 12, /// minute: 13, /// second: 14, /// microsecond: 0, /// tz_offset: Some(0), /// }, /// } /// ); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// ``` pub fn parse_bytes_rfc3339(bytes: &[u8]) -> Result { DateTime::parse_bytes_rfc3339_with_config(bytes, &TimeConfigBuilder::new().build()) } /// Same as `parse_bytes_rfc3339` with with a `TimeConfig` parameter. /// /// # Arguments /// /// * `bytes` - The bytes to parse /// * `config` - The `TimeConfig` to use /// /// # Examples /// /// ``` /// use speedate::{DateTime, Date, Time, TimeConfigBuilder}; /// /// let dt = DateTime::parse_bytes_rfc3339_with_config(b"2022-01-01T12:13:14Z", &TimeConfigBuilder::new().build()).unwrap(); /// assert_eq!( /// dt, /// DateTime { /// date: Date { /// year: 2022, /// month: 1, /// day: 1, /// }, /// time: Time { /// hour: 12, /// minute: 13, /// second: 14, /// microsecond: 0, /// tz_offset: Some(0), /// }, /// } /// ); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// ``` pub fn parse_bytes_rfc3339_with_config(bytes: &[u8], config: &TimeConfig) -> Result { // First up, parse the full date if we can let date = Date::parse_bytes_partial(bytes)?; // Next parse the separator between date and time let sep = bytes.get(10).copied(); if sep != Some(b'T') && sep != Some(b't') && sep != Some(b' ') && sep != Some(b'_') { return Err(ParseError::InvalidCharDateTimeSep); } // Next try to parse the time let time = Time::parse_bytes_offset(bytes, 11, config)?; Ok(Self { date, time }) } /// As with [DateTime::parse_bytes_rfc3339] but also supports unix timestamps. /// /// # Arguments /// /// * `bytes` - The bytes to parse /// /// # Examples /// /// ``` /// use speedate::{DateTime, Date, Time}; /// /// let dt = DateTime::parse_bytes(b"2022-01-01T12:13:14Z").unwrap(); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// /// let dt = DateTime::parse_bytes(b"1641039194").unwrap(); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14"); /// ``` pub fn parse_bytes(bytes: &[u8]) -> Result { DateTime::parse_bytes_with_config(bytes, &TimeConfigBuilder::new().build()) } /// Same as `DateTime::parse_bytes` but supporting TimeConfig /// /// # Arguments /// /// * `bytes` - The bytes to parse /// * `config` - The TimeConfig to use when parsing the time portion /// /// # Examples /// /// ``` /// use speedate::{DateTime, Date, Time, TimeConfigBuilder}; /// /// let dt = DateTime::parse_bytes_with_config(b"2022-01-01T12:13:14Z", &TimeConfigBuilder::new().build()).unwrap(); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// ``` pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result { match Self::parse_bytes_rfc3339_with_config(bytes, config) { Ok(d) => Ok(d), Err(e) => match float_parse_bytes(bytes) { IntFloat::Int(int) => Self::from_timestamp_with_config(int, 0, config), IntFloat::Float(float) => { let timestamp_in_milliseconds = float.abs() > MS_WATERSHED as f64; if config.microseconds_precision_overflow_behavior == MicrosecondsPrecisionOverflowBehavior::Error { let decimal_digits_count = decimal_digits(bytes); // If the number of decimal digits exceeds the maximum allowed for the timestamp precision, // return an error. For timestamps in milliseconds, the maximum is 3, for timestamps in seconds, // the maximum is 6. These end up being the same in terms of allowing microsecond precision. if timestamp_in_milliseconds && decimal_digits_count > 3 { return Err(ParseError::MillisecondFractionTooLong); } else if !timestamp_in_milliseconds && decimal_digits_count > 6 { return Err(ParseError::SecondFractionTooLong); } } let timestamp_normalized: f64 = if timestamp_in_milliseconds { float / 1_000f64 } else { float }; // if seconds is negative, we round down (left on the number line), so -6.25 -> -7 // which allows for a positive number of microseconds to compensate back up to -6.25 // which is the equivalent of doing (seconds - 1) and (microseconds + 1_000_000) // like we do in Date::timestamp_watershed let seconds = timestamp_normalized.floor() as i64; let microseconds = ((timestamp_normalized - seconds as f64) * 1_000_000f64).round() as u32; Self::from_timestamp_with_config(seconds, microseconds, config) } IntFloat::Err => Err(e), }, } } /// Like `from_timestamp` but with a `TimeConfig`. /// /// ("Unix Timestamp" means number of seconds or milliseconds since 1970-01-01) /// /// Input must be between `-11_676_096_000` (`1600-01-01T00:00:00`) and /// `253_402_300_799_000` (`9999-12-31T23:59:59.999999`) inclusive. /// /// If the absolute value is > 2e10 (`20_000_000_000`) it is interpreted as being in milliseconds. /// /// That means: /// * `20_000_000_000` is `2603-10-11T11:33:20` /// * `20_000_000_001` is `1970-08-20T11:33:20.001` /// * `-20_000_000_000` gives an error - `DateTooSmall` as it would be before 1600 /// * `-20_000_000_001` is `1969-05-14T12:26:39.999` /// /// # Arguments /// /// * `timestamp` - timestamp in either seconds or milliseconds /// * `timestamp_microsecond` - microseconds fraction of a second timestamp /// * `config` - the `TimeConfig` to use /// /// Where `timestamp` is interrupted as milliseconds and is not a whole second, the remainder is added to /// `timestamp_microsecond`. /// /// # Examples /// /// ``` /// use speedate::{DateTime, TimeConfigBuilder}; /// /// let d = DateTime::from_timestamp_with_config(1_654_619_320, 123, &TimeConfigBuilder::new().build()).unwrap(); /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.000123"); /// /// let d = DateTime::from_timestamp_with_config(1_654_619_320_123, 123_000, &TimeConfigBuilder::new().build()).unwrap(); /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.246000"); /// ``` pub fn from_timestamp_with_config( timestamp: i64, timestamp_microsecond: u32, config: &TimeConfig, ) -> Result { let (mut second, extra_microsecond) = Date::timestamp_watershed(timestamp)?; let mut total_microsecond = timestamp_microsecond .checked_add(extra_microsecond) .ok_or(ParseError::TimeTooLarge)?; if total_microsecond >= 1_000_000 { second = second .checked_add(total_microsecond as i64 / 1_000_000) .ok_or(ParseError::TimeTooLarge)?; total_microsecond %= 1_000_000; } let (date, time_second) = Date::from_timestamp_calc(second)?; Ok(Self { date, time: Time::from_timestamp_with_config(time_second, total_microsecond, config)?, }) } /// Create a datetime from a Unix Timestamp in seconds or milliseconds /// /// ("Unix Timestamp" means number of seconds or milliseconds since 1970-01-01) /// /// Input must be between `-62,167,219,200,000` (`0000-01-01`) and `253,402,300,799,000` (`9999-12-31`) inclusive. /// /// If the absolute value is > 2e10 (`20,000,000,000`) it is interpreted as being in milliseconds. /// /// That means: /// * `20,000,000,000` is `2603-10-11` /// * `20,000,000,001` is `1970-08-20` /// * `-62,167,219,200,001` gives an error - `DateTooSmall` as it would be before 0000-01-01 /// * `-20,000,000,001` is `1969-05-14` /// * `-20,000,000,000` is `1336-03-23` /// /// # Arguments /// /// * `timestamp` - timestamp in either seconds or milliseconds /// * `timestamp_microsecond` - microseconds fraction of a second timestamp /// /// Where `timestamp` is interrupted as milliseconds and is not a whole second, the remainder is added to /// `timestamp_microsecond`. /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let d = DateTime::from_timestamp(1_654_619_320, 123).unwrap(); /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.000123"); /// /// let d = DateTime::from_timestamp(1_654_619_320_123, 123_000).unwrap(); /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.246000"); /// ``` pub fn from_timestamp(timestamp: i64, timestamp_microsecond: u32) -> Result { Self::from_timestamp_with_config(timestamp, timestamp_microsecond, &TimeConfigBuilder::new().build()) } /// Create a datetime from the system time. This method uses [std::time::SystemTime] to get /// the system time and uses it to create a [DateTime] adjusted to the specified timezone offset. /// /// # Arguments /// /// * `tz_offset` - timezone offset in seconds, must be less than `86_400` /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let now = DateTime::now(0).unwrap(); /// println!("Current date and time: {}", now); /// ``` pub fn now(tz_offset: i32) -> Result { let t = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .map_err(|_| ParseError::SystemTimeError)?; let mut now = Self::from_timestamp(t.as_secs() as i64, t.subsec_micros())?; now.time.tz_offset = Some(0); if tz_offset == 0 { Ok(now) } else { now.in_timezone(tz_offset) } } /// Clone the datetime and set a new timezone offset. /// /// The returned datetime will represent a different point in time since the timezone offset is changed without /// modifying the date and time. See [DateTime::in_timezone] for alternative behaviour. /// /// # Arguments /// /// * `tz_offset` - optional timezone offset in seconds, set to `None` to create a naïve datetime. /// /// This method will return `Err(ParseError::OutOfRangeTz)` if `abs(tz_offset)` is not less than 24 hours `86_400`. /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let dt = DateTime::parse_str("2022-01-01T12:13:14Z").unwrap(); /// /// let dt2 = dt.with_timezone_offset(Some(-8 * 3600)).unwrap(); /// assert_eq!(dt2.to_string(), "2022-01-01T12:13:14-08:00"); /// ``` pub fn with_timezone_offset(&self, tz_offset: Option) -> Result { Ok(Self { date: self.date.clone(), time: self.time.with_timezone_offset(tz_offset)?, }) } /// Create a new datetime in a different timezone with date & time adjusted to represent the same moment in time. /// See [DateTime::with_timezone_offset] for alternative behaviour. /// /// The datetime must have a offset, otherwise a `ParseError::TzRequired` error is returned. /// /// # Arguments /// /// * `tz_offset` - new timezone offset in seconds. /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let dt_z = DateTime::parse_str("2000-01-01T15:00:00Z").unwrap(); /// /// let dt_utc_plus2 = dt_z.in_timezone(7200).unwrap(); /// assert_eq!(dt_utc_plus2.to_string(), "2000-01-01T17:00:00+02:00"); /// ``` pub fn in_timezone(&self, tz_offset: i32) -> Result { if tz_offset.abs() >= 24 * 3600 { Err(ParseError::OutOfRangeTz) } else if let Some(current_offset) = self.time.tz_offset { let new_ts = self.timestamp() + (tz_offset - current_offset) as i64; let mut new_dt = Self::from_timestamp(new_ts, self.time.microsecond)?; new_dt.time.tz_offset = Some(tz_offset); Ok(new_dt) } else { Err(ParseError::TzRequired) } } /// Unix timestamp (seconds since epoch, 1970-01-01T00:00:00) omitting timezone offset /// (or equivalently comparing to 1970-01-01T00:00:00 in the same timezone as self) /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let dt = DateTime::from_timestamp(1_654_619_320, 123).unwrap(); /// assert_eq!(dt.to_string(), "2022-06-07T16:28:40.000123"); /// assert_eq!(dt.timestamp(), 1_654_619_320); /// /// let dt = DateTime::parse_str("1970-01-02T00:00").unwrap(); /// assert_eq!(dt.timestamp(), 24 * 3600); /// ``` pub fn timestamp(&self) -> i64 { self.date.timestamp() + self.time.total_seconds() as i64 } /// Unix timestamp assuming epoch is in zulu timezone (1970-01-01T00:00:00Z) and accounting for /// timezone offset. /// /// This is effectively [Self::timestamp] minus [Self.time::tz_offset], see [Self::partial_cmp] for details on /// why timezone offset is subtracted. If [Self.time::tz_offset] if `None`, this is the same as [Self::timestamp]. /// /// # Examples /// /// ``` /// use speedate::DateTime; /// /// let dt_naive = DateTime::parse_str("1970-01-02T00:00").unwrap(); /// assert_eq!(dt_naive.timestamp_tz(), 24 * 3600); /// /// let dt_zulu = DateTime::parse_str("1970-01-02T00:00Z").unwrap(); /// assert_eq!(dt_zulu.timestamp_tz(), 24 * 3600); /// /// let dt_plus_1 = DateTime::parse_str("1970-01-02T00:00+01:00").unwrap(); /// assert_eq!(dt_plus_1.timestamp_tz(), 23 * 3600); /// ``` pub fn timestamp_tz(&self) -> i64 { match self.time.tz_offset { Some(tz_offset) => self.timestamp() - (tz_offset as i64), None => self.timestamp(), } } } speedate-0.15.0/src/duration.rs000064400000000000000000000475051046102023000144760ustar 00000000000000use std::cmp::Ordering; use std::fmt; use std::str::FromStr; use crate::{time::TimeConfig, ParseError, TimeConfigBuilder}; /// A Duration /// /// Allowed values: /// * `PnYnMnDTnHnMnS` - ISO 8601 duration format, /// see [wikipedia](https://en.wikipedia.org/wiki/ISO_8601#Durations) for more details, /// `W` for weeks is also allowed before the `T` separator - **Note**: `W` is allowed combined /// with other quantities which is a slight deviation from the ISO 8601 standard. /// * `HH:MM:SS` - any of the above time formats are allowed to represent a duration /// * `D days, HH:MM:SS` - time prefixed by `X days`, case-insensitive, /// spaces `s` and `,` are all optional /// * `D d, HH:MM:SS` - time prefixed by `X d`, case-insensitive, spaces and `,` are optional /// /// All duration formats can be prefixed with `+` or `-` to indicate /// positive and negative durations respectively. /// /// `Duration` stores durations in days, seconds and microseconds (all ints), therefore /// durations like years need be scaled when creating a `Duration`. The following scaling /// factors are used: /// * `Y` - 365 days /// * `M` - 30 days /// * `W` - 7 days /// * `D` - 1 day /// * `H` - 3600 seconds /// * `M` - 60 seconds /// * `S` - 1 second /// /// Fractions of quantities are permitted by ISO 8601 in the final quantity included, e.g. /// `P1.5Y` or `P1Y1.5M`. Wen fractions of quantities are found `day`, `second` and `microsecond` /// are calculated to most accurately represent the fraction. For example `P1.123W` is represented /// as /// ```text /// Duration { /// positive: true, /// day: 7, /// second: 74390, /// microsecond: 400_000 /// } /// ``` #[derive(Debug, PartialEq, Eq, Clone)] pub struct Duration { /// The positive or negative sign of the duration pub positive: bool, /// The number of days pub day: u32, /// The number of seconds, range 0 to 86399 pub second: u32, /// The number of microseconds, range 0 to 999999 pub microsecond: u32, } impl fmt::Display for Duration { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.positive { write!(f, "-")?; } write!(f, "P")?; if self.day != 0 { let year = self.day / 365; if year != 0 { write!(f, "{year}Y")?; } let day = self.day % 365; if day != 0 { write!(f, "{day}D")?; } } if self.second != 0 || self.microsecond != 0 { let (hour, minute, sec) = self.to_hms(); write!(f, "T")?; if hour != 0 { write!(f, "{hour}H")?; } if minute != 0 { write!(f, "{minute}M")?; } if sec != 0 || self.microsecond != 0 { write!(f, "{sec}")?; if self.microsecond != 0 { let s = format!("{:06}", self.microsecond); write!(f, ".{}", s.trim_end_matches('0'))?; } write!(f, "S")?; } } if self.second == 0 && self.microsecond == 0 && self.day == 0 { write!(f, "T0S")?; } Ok(()) } } impl FromStr for Duration { type Err = ParseError; #[inline] fn from_str(s: &str) -> Result { Self::parse_str(s) } } impl Duration { fn to_hms(&self) -> (u32, u32, u32) { let hours = self.second / 3600; let minutes = (self.second % 3600) / 60; let remaining_seconds = self.second % 60; (hours, minutes, remaining_seconds) } } impl PartialOrd for Duration { /// Compare two durations by inequality. /// /// `Duration` supports equality (`==`, `!=`) and inequality (`>`, `<`, `>=` & `<=`) comparisons. /// /// # Example /// /// ``` /// use speedate::Duration; /// /// let duration = |s| Duration::parse_str(s).unwrap(); /// /// let d1 = duration("P3DT4H5M6.7S"); /// let d2 = duration("P4DT1H"); /// /// assert!(d2 > d1); /// ``` /// /// `positive` is included in in comparisons, thus `+P1D` is greater than `-P2D`, /// similarly `-P2D` is less than `-P1D`. /// ``` /// # use speedate::Duration; /// # let duration = |s| Duration::parse_str(s).unwrap(); /// assert!(duration("+P1D") > duration("-P2D")); /// assert!(duration("-P2D") < duration("-P1D")); /// ``` fn partial_cmp(&self, other: &Self) -> Option { match (self.positive, other.positive) { (true, false) => Some(Ordering::Greater), (false, true) => Some(Ordering::Less), (self_positive, _) => { let self_t = (self.day, self.second, self.microsecond); let other_t = (other.day, other.second, other.microsecond); if self_positive { self_t.partial_cmp(&other_t) } else { other_t.partial_cmp(&self_t) } } } } } macro_rules! checked { ($a:ident + $b:expr) => { $a.checked_add($b).ok_or(ParseError::DurationValueTooLarge)? }; ($a:ident * $b:expr) => { $a.checked_mul($b).ok_or(ParseError::DurationValueTooLarge)? }; } impl Duration { /// Create a duration from raw values. /// /// # Arguments /// * `positive` - the positive or negative sign of the duration /// * `day` - the number of days in the `Duration`, max allowed value is `999_999_999` to match python's `timedelta` /// * `second` - the number of seconds in the `Duration` /// * `microsecond` - the number of microseconds in the `Duration` /// /// `second` and `microsecond` are normalised to be in the ranges 0 to `86_400` and 0 to `999_999` /// respectively. /// /// Due to the limit on days, the maximum duration which can be represented is `P2739726Y9DT86399.999999S`, /// that is 1 microsecond short of 2,739,726 years and 10 days, positive or negative. /// /// # Examples /// /// ``` /// use speedate::Duration; /// /// let d = Duration::new(false, 1, 86500, 1_000_123).unwrap(); /// assert_eq!( /// d, /// Duration { /// positive: false, /// day: 2, /// second: 101, /// microsecond: 123, /// } /// ); /// ``` pub fn new(positive: bool, day: u32, second: u32, microsecond: u32) -> Result { let mut d = Self { positive, day, second, microsecond, }; d.normalize()?; Ok(d) } /// Parse a duration from a string /// /// # Arguments /// /// * `str` - The string to parse /// /// # Examples /// /// ``` /// use speedate::Duration; /// /// let d = Duration::parse_str("P1YT2.1S").unwrap(); /// assert_eq!( /// d, /// Duration { /// positive: true, /// day: 365, /// second: 2, /// microsecond: 100_000 /// } /// ); /// assert_eq!(d.to_string(), "P1YT2.1S"); /// ``` #[inline] pub fn parse_str(str: &str) -> Result { Self::parse_bytes(str.as_bytes()) } /// Parse a duration from bytes /// /// # Arguments /// /// * `bytes` - The bytes to parse /// /// # Examples /// /// ``` /// use speedate::Duration; /// /// let d = Duration::parse_bytes(b"P1Y").unwrap(); /// assert_eq!( /// d, /// Duration { /// positive: true, /// day: 365, /// second: 0, /// microsecond: 0 /// } /// ); /// assert_eq!(d.to_string(), "P1Y"); /// ``` #[inline] pub fn parse_bytes(bytes: &[u8]) -> Result { Duration::parse_bytes_with_config(bytes, &TimeConfigBuilder::new().build()) } /// Same as `Duration::parse_bytes` but with a TimeConfig component. /// /// # Arguments /// /// * `bytes` - The bytes to parse /// * `config` - The `TimeConfig` to use /// /// # Examples /// /// ``` /// use speedate::{Duration, TimeConfigBuilder}; /// /// let d = Duration::parse_bytes_with_config(b"P1Y", &TimeConfigBuilder::new().build()).unwrap(); /// assert_eq!( /// d, /// Duration { /// positive: true, /// day: 365, /// second: 0, /// microsecond: 0 /// } /// ); /// assert_eq!(d.to_string(), "P1Y"); /// ``` #[inline] pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result { let (positive, bytes) = match bytes { [b'-', bytes @ ..] => (false, bytes), [b'+', bytes @ ..] | bytes => (true, bytes), }; let mut d = match bytes { [] => return Err(ParseError::TooShort), [b'P', iso_duration @ ..] => Self::parse_iso_duration(iso_duration)?, bytes => { if Self::is_duration_date_format(bytes) || bytes.len() < 5 { Self::parse_days_time(bytes, config)? } else { Self::parse_time(bytes, config)? } } }; d.positive = positive; d.normalize()?; Ok(d) } /// Total number of seconds in the duration (days + seconds) with sign based on `self.positive` #[inline] pub fn signed_total_seconds(&self) -> i64 { let sign = if self.positive { 1 } else { -1 }; sign * (self.day as i64 * 86400 + self.second as i64) } /// Microseconds in the duration with sign based on `self.positive` #[inline] pub fn signed_microseconds(&self) -> i32 { let sign = if self.positive { 1 } else { -1 }; sign * self.microsecond as i32 } fn normalize(&mut self) -> Result<(), ParseError> { if self.microsecond >= 1_000_000 { self.second = self .second .checked_add(self.microsecond / 1_000_000) .ok_or(ParseError::DurationValueTooLarge)?; self.microsecond %= 1_000_000; } if self.second >= 86_400 { self.day = self .day .checked_add(self.second / 86_400) .ok_or(ParseError::DurationValueTooLarge)?; self.second %= 86_400; } if self.day > 999_999_999 { Err(ParseError::DurationDaysTooLarge) } else { Ok(()) } } /// Parse ISO duration (excluding the 'P' prefix) fn parse_iso_duration(bytes: &[u8]) -> Result { let mut got_t = false; let mut last_had_fraction = false; let mut position: usize = 0; let mut day: u32 = 0; let mut second: u32 = 0; let mut microsecond: u32 = 0; loop { match bytes.get(position).copied() { Some(b'T') => { if got_t { return Err(ParseError::DurationTRepeated); } got_t = true; } Some(c) => { let (value, op_fraction, offset) = Self::parse_number_frac(&bytes[position..], c)?; if last_had_fraction { return Err(ParseError::DurationInvalidFraction); } if op_fraction.is_some() { last_had_fraction = true; } position += offset; if got_t { let mult: u32 = match bytes.get(position).copied() { Some(b'H') => 3600, Some(b'M') => 60, Some(b'S') => 1, _ => return Err(ParseError::DurationInvalidTimeUnit), }; second = checked!(second + checked!(mult * value)); if let Some(fraction) = op_fraction { let extra_seconds = fraction * mult as f64; let extra_full_seconds = extra_seconds.trunc(); second = checked!(second + extra_full_seconds as u32); let micro_extra = ((extra_seconds - extra_full_seconds) * 1_000_000.0).round() as u32; microsecond = checked!(microsecond + micro_extra); } } else { let mult: u32 = match bytes.get(position).copied() { Some(b'Y') => 365, Some(b'M') => 30, Some(b'W') => 7, Some(b'D') => 1, _ => return Err(ParseError::DurationInvalidDateUnit), }; day = checked!(day + checked!(value * mult)); if let Some(fraction) = op_fraction { let extra_days = fraction * mult as f64; let extra_full_days = extra_days.trunc(); day = checked!(day + extra_full_days as u32); let extra_seconds = (extra_days - extra_full_days) * 86_400.0; let extra_full_seconds = extra_seconds.trunc(); second = checked!(second + extra_full_seconds as u32); microsecond += ((extra_seconds - extra_full_seconds) * 1_000_000.0).round() as u32; } } } None => break, } position += 1; } // require at least one field if position < 2 { return Err(ParseError::TooShort); } Ok(Self { positive: false, // is set above day, second, microsecond, }) } fn is_duration_date_format(bytes: &[u8]) -> bool { bytes.iter().any(|&byte| byte == b'd' || byte == b'D') } fn parse_days_time(bytes: &[u8], config: &TimeConfig) -> Result { let (day, position) = match bytes.first().copied() { Some(c) => Self::parse_number(bytes, c), _ => Err(ParseError::TooShort), }?; let Some( // expect d or D next, optionally prefixed by a space [b' ', b'd' | b'D', remaining @ ..] | [b'd' | b'D', remaining @ ..], ) = bytes.get(position..) else { return Err(ParseError::DurationInvalidDays); }; // optionally consume the rest of the word "day/days" let remaining = match remaining { // ays [b'a', b'y', b's', remaining @ ..] // ay | [b'a', b'y', remaining @ ..] // AYS | [b'A', b'Y', b'S', remaining @ ..] // AY | [b'A', b'Y', remaining @ ..] => { remaining } // word continued but not finished correctly [b'a', ..] | [b'A', ..] => return Err(ParseError::DurationInvalidDays), remaining => remaining, }; // days are finished, next prepare to parse the time // optionally consume a comma "," and maybe a space let remaining = match remaining { // b", " [b',', b' ', remaining @ ..] // b"," | [b',', remaining @ ..] // b" " | [b' ', remaining @ ..] // no comma / space | remaining => remaining, }; if remaining.is_empty() { return Ok(Self { positive: false, // is set above day, second: 0, microsecond: 0, }); } let t = Self::parse_time(remaining, config)?; if t.day > 0 { // 1d 24:00:00 is not allowed return Err(ParseError::DurationHourValueTooLarge); } Ok(Self { positive: false, // is set above day, second: t.second, microsecond: t.microsecond, }) } fn parse_time(bytes: &[u8], config: &TimeConfig) -> Result { let byte_len = bytes.len(); if byte_len < 5 { return Err(ParseError::TooShort); } const HOUR_NUMERIC_LIMIT: i64 = 24 * 10i64.pow(8); let mut hour: i64 = 0; let mut chunks = bytes.splitn(2, |&byte| byte == b':'); // can just use `.split_once()` in future maybe, if that stabilises let (hour_part, mut remaining) = match (chunks.next(), chunks.next(), chunks.next()) { (_, _, Some(_)) | (None, _, _) => unreachable!("should always be 1 or 2 chunks"), (Some(_hour_part), None, _) => return Err(ParseError::InvalidCharHour), (Some(hour_part), Some(remaining), None) => (hour_part, remaining), }; // > 9.999.999.999 if hour_part.len() > 10 { return Err(ParseError::DurationHourValueTooLarge); } for byte in hour_part { let h = byte.wrapping_sub(b'0'); if h > 9 { return Err(ParseError::InvalidCharHour); } hour = (hour * 10) + (h as i64); } if hour > HOUR_NUMERIC_LIMIT { return Err(ParseError::DurationHourValueTooLarge); } let mut new_bytes = *b"00:00:00.000000"; if 3 + remaining.len() > new_bytes.len() { match config.microseconds_precision_overflow_behavior { crate::MicrosecondsPrecisionOverflowBehavior::Truncate => remaining = &remaining[..new_bytes.len() - 3], crate::MicrosecondsPrecisionOverflowBehavior::Error => return Err(ParseError::SecondFractionTooLong), } } let new_bytes = &mut new_bytes[..3 + remaining.len()]; new_bytes[3..].copy_from_slice(remaining); let t = crate::time::PureTime::parse(new_bytes, 0, config)?; if new_bytes.len() > t.position { return Err(ParseError::ExtraCharacters); } let day = hour as u32 / 24; hour %= 24; Ok(Self { positive: false, // is set above day, second: t.total_seconds() + (hour as u32) * 3_600, microsecond: t.microsecond, }) } fn parse_number(bytes: &[u8], d1: u8) -> Result<(u32, usize), ParseError> { let mut value = match d1 { c if d1.is_ascii_digit() => (c - b'0') as u32, _ => return Err(ParseError::DurationInvalidNumber), }; let mut position = 1; loop { match bytes.get(position) { Some(c) if c.is_ascii_digit() => { value = checked!(value * 10); value = checked!(value + (c - b'0') as u32); position += 1; } _ => return Ok((value, position)), } } } fn parse_number_frac(bytes: &[u8], d1: u8) -> Result<(u32, Option, usize), ParseError> { let (value, mut position) = Self::parse_number(bytes, d1)?; let next_char = bytes.get(position).copied(); if next_char == Some(b'.') || next_char == Some(b',') { let mut decimal = 0_f64; let mut denominator = 1_f64; loop { position += 1; match bytes.get(position) { Some(c) if c.is_ascii_digit() => { decimal *= 10.0; decimal += (c - b'0') as f64; denominator *= 10.0; } _ => return Ok((value, Some(decimal / denominator), position)), } } } else { Ok((value, None, position)) } } } speedate-0.15.0/src/lib.rs000064400000000000000000000127461046102023000134160ustar 00000000000000#![doc = include_str ! ("../README.md")] extern crate core; extern crate strum; use strum::{Display, EnumMessage}; mod date; mod datetime; mod duration; mod numbers; mod time; pub use date::Date; pub use datetime::DateTime; pub use duration::Duration; pub use time::{MicrosecondsPrecisionOverflowBehavior, Time, TimeConfig, TimeConfigBuilder}; pub use numbers::{float_parse_bytes, float_parse_str, int_parse_bytes, int_parse_str, IntFloat}; /// Parsing datetime, date, time & duration values // get a character from the bytes as as a decimal macro_rules! get_digit { ($bytes:ident, $index:expr, $error:ident) => { match $bytes.get($index) { Some(c) if c.is_ascii_digit() => c - b'0', _ => return Err(ParseError::$error), } }; } pub(crate) use get_digit; // as above without bounds check, requires length to checked first! macro_rules! get_digit_unchecked { ($bytes:ident, $index:expr, $error:ident) => { match $bytes.get_unchecked($index) { c if c.is_ascii_digit() => c - b'0', _ => return Err(ParseError::$error), } }; } pub(crate) use get_digit_unchecked; /// Details about errors when parsing datetime, date, time & duration values /// /// As well as comparing enum values, machine and human readable representations of /// errors are provided. /// /// # Examples /// (Note: the `strum::EnumMessage` trait must be used to support `.get_documentation()`) /// ``` /// use strum::EnumMessage; /// use speedate::{Date, ParseError}; /// /// match Date::parse_str("invalid") { /// Ok(_) => println!("Parsed successfully"), /// Err(error) => { /// assert_eq!(error, ParseError::TooShort); /// assert_eq!(error.to_string(), "too_short"); /// assert_eq!(error.get_documentation(), Some("input is too short")); /// } /// }; /// ``` #[derive(Debug, Display, EnumMessage, PartialEq, Eq, Clone)] #[strum(serialize_all = "snake_case")] pub enum ParseError { /// input is too short TooShort, /// unexpected extra characters at the end of the input ExtraCharacters, /// invalid datetime separator, expected `T`, `t`, `_` or space InvalidCharDateTimeSep, /// invalid date separator, expected `-` InvalidCharDateSep, /// Timestamp is not an exact date DateNotExact, /// invalid character in year InvalidCharYear, /// invalid character in month InvalidCharMonth, /// invalid character in day InvalidCharDay, /// invalid time separator, expected `:` InvalidCharTimeSep, /// invalid character in hour InvalidCharHour, /// invalid character in minute InvalidCharMinute, /// invalid character in second InvalidCharSecond, /// invalid character in second fraction InvalidCharSecondFraction, /// invalid timezone sign InvalidCharTzSign, /// invalid timezone hour InvalidCharTzHour, /// invalid timezone minute InvalidCharTzMinute, /// timezone minute value is outside expected range of 0-59 OutOfRangeTzMinute, /// timezone offset must be less than 24 hours OutOfRangeTz, /// timezone is required to adjust to a new timezone TzRequired, /// Error getting system time SystemTimeError, /// month value is outside expected range of 1-12 OutOfRangeMonth, /// day value is outside expected range OutOfRangeDay, /// hour value is outside expected range of 0-23 OutOfRangeHour, /// minute value is outside expected range of 0-59 OutOfRangeMinute, /// second value is outside expected range of 0-59 OutOfRangeSecond, /// second fraction value is more than 6 digits long SecondFractionTooLong, /// second fraction digits missing after `.` SecondFractionMissing, /// millisecond fraction value is more than 3 digits long MillisecondFractionTooLong, /// invalid digit in duration DurationInvalidNumber, /// `t` character repeated in duration DurationTRepeated, /// quantity fraction invalid in duration DurationInvalidFraction, /// quantity invalid in time part of duration DurationInvalidTimeUnit, /// quantity invalid in date part of duration DurationInvalidDateUnit, /// "day" identifier in duration not correctly formatted DurationInvalidDays, /// a numeric value in the duration is too large DurationValueTooLarge, /// durations may not exceed 999,999,999 hours DurationHourValueTooLarge, /// durations may not exceed 999,999,999 days DurationDaysTooLarge, /// dates before 0000 are not supported as unix timestamps DateTooSmall, /// dates after 9999 are not supported as unix timestamps DateTooLarge, /// numeric times may not exceed 86,399 seconds TimeTooLarge, } #[derive(Debug, Display, EnumMessage, PartialEq, Eq, Clone)] #[strum(serialize_all = "snake_case")] pub enum ConfigError { // SecondsPrecisionOverflowBehavior string representation, must be one of "error" or "truncate" UnknownMicrosecondsPrecisionOverflowBehaviorString, } /// Used internally to write numbers to a buffer for `Display` of speedate types fn display_num_buf(num: usize, start: usize, value: u32, buf: &mut [u8]) { for i in 0..num { if (i + 1) == num { buf[i + start] = b'0' + (value % 10) as u8; } else if num <= 2 { buf[i + start] = b'0' + (value / (10i32.pow((num - 1 - i) as u32)) as u32) as u8; } else { buf[i + start] = b'0' + (value / (10i32.pow((num - 1 - i) as u32)) as u32 % 10) as u8; } } } speedate-0.15.0/src/numbers.rs000064400000000000000000000064731046102023000143230ustar 00000000000000/// Parse a string as an int. /// /// This is around 2x faster than using `str::parse::()` pub fn int_parse_str(s: &str) -> Option { int_parse_bytes(s.as_bytes()) } /// Parse bytes as an int. pub fn int_parse_bytes(s: &[u8]) -> Option { let (neg, first_digit, digits) = match s { [b'-', first, digits @ ..] => (true, first, digits), [b'+', first, digits @ ..] | [first, digits @ ..] => (false, first, digits), _ => return None, }; let mut result = match first_digit { b'0' => 0, b'1'..=b'9' => (first_digit & 0x0f) as i64, _ => return None, }; for digit in digits { result = result.checked_mul(10)?; match digit { b'0' => {} b'1'..=b'9' => result = result.checked_add((digit & 0x0f) as i64)?, _ => return None, } } if neg { Some(-result) } else { Some(result) } } #[derive(Debug)] pub enum IntFloat { Int(i64), Float(f64), Err, } impl IntFloat { pub fn is_err(&self) -> bool { matches!(self, IntFloat::Err) } } /// Parse a string as a float. /// /// This is around 2x faster than using `str::parse::()` pub fn float_parse_str(s: &str) -> IntFloat { float_parse_bytes(s.as_bytes()) } /// Parse bytes as an float. pub fn float_parse_bytes(s: &[u8]) -> IntFloat { let (neg, first_digit, digits) = match s { [b'-', first, digits @ ..] => (true, first, digits), [b'+', first, digits @ ..] | [first, digits @ ..] => (false, first, digits), _ => return IntFloat::Err, }; let mut int_part = match first_digit { b'0' => 0, b'1'..=b'9' => (first_digit & 0x0f) as i64, _ => return IntFloat::Err, }; let mut found_dot = false; let mut bytes = digits.iter().copied(); for digit in bytes.by_ref() { match digit { b'0'..=b'9' => { int_part = match int_part.checked_mul(10) { Some(i) => i, None => return IntFloat::Err, }; int_part = match int_part.checked_add((digit & 0x0f) as i64) { Some(i) => i, None => return IntFloat::Err, }; } b'.' => { found_dot = true; break; } _ => return IntFloat::Err, } } if found_dot { let mut result = int_part as f64; let mut div = 10_f64; for digit in bytes { match digit { b'0'..=b'9' => { result += (digit & 0x0f) as f64 / div; div *= 10_f64; } _ => return IntFloat::Err, } } if neg { IntFloat::Float(-result) } else { IntFloat::Float(result) } } else if neg { IntFloat::Int(-int_part) } else { IntFloat::Int(int_part) } } /// Count the number of decimal places in a byte slice. /// Caution: does not verify the integrity of the input, /// so it may return incorrect results for invalid inputs. pub(crate) fn decimal_digits(bytes: &[u8]) -> usize { match bytes.splitn(2, |&b| b == b'.').nth(1) { Some(b"") | None => 0, Some(fraction) => fraction.len(), } } speedate-0.15.0/src/time.rs000064400000000000000000000515771046102023000136130ustar 00000000000000use std::cmp::Ordering; use std::default::Default; use std::fmt; use std::str::FromStr; use crate::{get_digit, get_digit_unchecked, ConfigError, ParseError}; /// A Time /// /// Allowed formats: /// * `HH:MM:SS` /// * `HH:MM:SS.FFFFFF` 1 to 6 digits are allowed /// * `HH:MM` /// * `HH:MM:SSZ` /// * `HH:MM:SS.FFFFFFZ` /// /// Fractions of a second are to microsecond precision, if the value contains greater /// precision, an error is raised. /// /// # Comparison /// /// `Time` supports equality (`==`) and inequality (`>`, `<`, `>=`, `<=`) comparisons. /// /// See [Time::partial_cmp] for how this works. #[derive(Debug, PartialEq, Eq, Clone)] pub struct Time { /// Hour: 0 to 23 pub hour: u8, /// Minute: 0 to 59 pub minute: u8, /// Second: 0 to 59 pub second: u8, /// microseconds: 0 to 999999 pub microsecond: u32, /// timezone offset in seconds if provided, must be >-24h and <24h // This range is to match python, // Note: [Stack Overflow suggests](https://stackoverflow.com/a/8131056/949890) larger offsets can happen pub tz_offset: Option, } impl fmt::Display for Time { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.microsecond != 0 { let mut buf: [u8; 15] = *b"00:00:00.000000"; crate::display_num_buf(2, 0, self.hour as u32, &mut buf); crate::display_num_buf(2, 3, self.minute as u32, &mut buf); crate::display_num_buf(2, 6, self.second as u32, &mut buf); crate::display_num_buf(6, 9, self.microsecond, &mut buf); f.write_str(std::str::from_utf8(&buf[..]).unwrap())? } else { let mut buf: [u8; 8] = *b"00:00:00"; crate::display_num_buf(2, 0, self.hour as u32, &mut buf); crate::display_num_buf(2, 3, self.minute as u32, &mut buf); crate::display_num_buf(2, 6, self.second as u32, &mut buf); f.write_str(std::str::from_utf8(&buf[..]).unwrap())? } if let Some(tz_offset) = self.tz_offset { if tz_offset == 0 { write!(f, "Z")?; } else { // tz offset is given in seconds, so we do convertions from seconds -> mins -> hours let total_minutes = tz_offset / 60; let hours = total_minutes / 60; let minutes = total_minutes % 60; let mut buf: [u8; 6] = *b"+00:00"; if tz_offset < 0 { buf[0] = b'-'; } crate::display_num_buf(2, 1, hours.unsigned_abs(), &mut buf); crate::display_num_buf(2, 4, minutes.unsigned_abs(), &mut buf); f.write_str(std::str::from_utf8(&buf[..]).unwrap())?; } } Ok(()) } } impl FromStr for Time { type Err = ParseError; #[inline] fn from_str(s: &str) -> Result { Self::parse_str(s) } } impl PartialOrd for Time { /// Compare two times by inequality. /// /// `Time` supports equality (`==`, `!=`) and inequality comparisons (`>`, `<`, `>=` & `<=`). /// /// # Examples /// /// ``` /// use speedate::Time; /// /// let t1 = Time::parse_str("04:05:06.07").unwrap(); /// let t2 = Time::parse_str("04:05:06.08").unwrap(); /// /// assert!(t1 < t2); /// ``` /// /// # Comparison with Timezones /// /// When comparing two times, we want "less than" or "greater than" refer to "earlier" or "later" /// in the absolute course of time. We therefore need to be careful when comparing times with different /// timezones. (If it wasn't for timezones, we could omit all this extra logic and thinking and just compare /// struct members directly as we do with [crate::Date] and [crate::Duration]). /// /// See [crate::DateTime::partial_cmp] for more information about comparisons with timezones. /// /// ## Timezone Examples /// /// ``` /// use speedate::Time; /// /// let t1 = Time::parse_str("15:00:00Z").unwrap(); /// let t2 = Time::parse_str("15:00:00+01:00").unwrap(); /// /// assert!(t1 > t2); /// /// let t3 = Time::parse_str("15:00:00-01:00").unwrap(); /// let t4 = Time::parse_str("15:00:00+01:00").unwrap(); /// /// assert!(t3 > t4); /// ``` fn partial_cmp(&self, other: &Self) -> Option { match (self.tz_offset, other.tz_offset) { (Some(tz_offset), Some(other_tz_offset)) => match (self.total_seconds() as i64 - tz_offset as i64) .partial_cmp(&(other.total_seconds() as i64 - other_tz_offset as i64)) { Some(Ordering::Equal) => self.microsecond.partial_cmp(&other.microsecond), otherwise => otherwise, }, _ => match self.total_seconds().partial_cmp(&other.total_seconds()) { Some(Ordering::Equal) => self.microsecond.partial_cmp(&other.microsecond), otherwise => otherwise, }, } } } impl Time { /// Parse a time from a string /// /// # Arguments /// /// * `str` - The string to parse /// /// # Examples /// /// ``` /// use speedate::Time; /// /// let d = Time::parse_str("12:13:14.123456").unwrap(); /// assert_eq!( /// d, /// Time { /// hour: 12, /// minute: 13, /// second: 14, /// microsecond: 123456, /// tz_offset: None, /// } /// ); /// assert_eq!(d.to_string(), "12:13:14.123456"); /// ``` #[inline] pub fn parse_str(str: &str) -> Result { Self::parse_bytes(str.as_bytes()) } /// Parse a time from bytes /// /// # Arguments /// /// * `bytes` - The bytes to parse /// /// # Examples /// /// ``` /// use speedate::Time; /// /// let d = Time::parse_bytes(b"12:13:14.123456").unwrap(); /// assert_eq!( /// d, /// Time { /// hour: 12, /// minute: 13, /// second: 14, /// microsecond: 123456, /// tz_offset: None, /// } /// ); /// assert_eq!(d.to_string(), "12:13:14.123456"); /// ``` #[inline] pub fn parse_bytes(bytes: &[u8]) -> Result { Self::parse_bytes_offset(bytes, 0, &TimeConfigBuilder::new().build()) } /// Same as `Time::parse_bytes` but with a `TimeConfig`. /// /// # Arguments /// /// * `bytes` - The bytes to parse /// * `config` - The `TimeConfig` to use /// /// # Examples /// /// ``` /// use speedate::{Time, TimeConfigBuilder}; /// /// let d = Time::parse_bytes_with_config(b"12:13:14.123456", &TimeConfigBuilder::new().build()).unwrap(); /// assert_eq!( /// d, /// Time { /// hour: 12, /// minute: 13, /// second: 14, /// microsecond: 123456, /// tz_offset: None, /// } /// ); /// assert_eq!(d.to_string(), "12:13:14.123456"); /// ``` #[inline] pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result { Self::parse_bytes_offset(bytes, 0, config) } /// Create a time from seconds and microseconds. /// /// # Arguments /// /// * `timestamp_second` - timestamp in seconds /// * `timestamp_microsecond` - microseconds fraction of a second timestamp /// /// If `seconds + timestamp_microsecond` exceeds 86400, an error is returned. /// /// # Examples /// /// ``` /// use speedate::Time; /// /// let d = Time::from_timestamp(3740, 123).unwrap(); /// assert_eq!(d.to_string(), "01:02:20.000123"); /// ``` pub fn from_timestamp(timestamp_second: u32, timestamp_microsecond: u32) -> Result { Time::from_timestamp_with_config( timestamp_second, timestamp_microsecond, &TimeConfigBuilder::new().build(), ) } /// Like `from_timestamp` but with a `TimeConfig` /// /// # Arguments /// /// * `timestamp_second` - timestamp in seconds /// * `timestamp_microsecond` - microseconds fraction of a second timestamp /// * `config` - the `TimeConfig` to use /// /// If `seconds + timestamp_microsecond` exceeds 86400, an error is returned. /// /// # Examples /// /// ``` /// use speedate::{Time, TimeConfigBuilder}; /// /// let d = Time::from_timestamp_with_config(3740, 123, &TimeConfigBuilder::new().build()).unwrap(); /// assert_eq!(d.to_string(), "01:02:20.000123"); /// ``` pub fn from_timestamp_with_config( timestamp_second: u32, timestamp_microsecond: u32, config: &TimeConfig, ) -> Result { let mut second = timestamp_second; let mut microsecond = timestamp_microsecond; if microsecond >= 1_000_000 { second = second .checked_add(microsecond / 1_000_000) .ok_or(ParseError::TimeTooLarge)?; microsecond %= 1_000_000; } if second >= 86_400 { return Err(ParseError::TimeTooLarge); } Ok(Self { hour: (second / 3600) as u8, minute: ((second % 3600) / 60) as u8, second: (second % 60) as u8, microsecond, tz_offset: config.unix_timestamp_offset, }) } /// Parse a time from bytes with a starting index, extra characters at the end of the string result in an error pub(crate) fn parse_bytes_offset(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result { let pure_time = PureTime::parse(bytes, offset, config)?; // Parse the timezone offset let mut tz_offset: Option = None; let mut position = pure_time.position; if let Some(next_char) = bytes.get(position).copied() { position += 1; if next_char == b'Z' || next_char == b'z' { tz_offset = Some(0); } else { let sign = match next_char { b'+' => 1, b'-' => -1, 226 => { // U+2212 MINUS "−" is allowed under ISO 8601 for negative timezones // > python -c 'print([c for c in "−".encode()])' // its raw byte values are [226, 136, 146] if bytes.get(position).copied() != Some(136) { return Err(ParseError::InvalidCharTzSign); } if bytes.get(position + 1).copied() != Some(146) { return Err(ParseError::InvalidCharTzSign); } position += 2; -1 } _ => return Err(ParseError::InvalidCharTzSign), }; let h1 = get_digit!(bytes, position, InvalidCharTzHour) as i32; let h2 = get_digit!(bytes, position + 1, InvalidCharTzHour) as i32; let m1 = match bytes.get(position + 2) { Some(b':') => { position += 3; get_digit!(bytes, position, InvalidCharTzMinute) as i32 } Some(c) if c.is_ascii_digit() => { position += 2; (c - b'0') as i32 } _ => return Err(ParseError::InvalidCharTzMinute), }; let m2 = get_digit!(bytes, position + 1, InvalidCharTzMinute) as i32; let minute_seconds = m1 * 600 + m2 * 60; if minute_seconds >= 3600 { return Err(ParseError::OutOfRangeTzMinute); } let offset_val = sign * (h1 * 36000 + h2 * 3600 + minute_seconds); // TZ must be less than 24 hours to match python if offset_val.abs() >= 24 * 3600 { return Err(ParseError::OutOfRangeTz); } tz_offset = Some(offset_val); position += 2; } } if bytes.len() > position { return Err(ParseError::ExtraCharacters); } Ok(Self { hour: pure_time.hour, minute: pure_time.minute, second: pure_time.second, microsecond: pure_time.microsecond, tz_offset, }) } /// Get the total seconds of the time /// /// E.g. hours + minutes + seconds /// /// # Examples /// /// ``` /// use speedate::Time; /// /// let d = Time::parse_str("12:13:14.123456").unwrap(); /// assert_eq!(d.total_seconds(), 12 * 3600 + 13 * 60 + 14); /// ``` pub fn total_seconds(&self) -> u32 { let mut total_seconds = self.hour as u32 * 3600; total_seconds += self.minute as u32 * 60; total_seconds += self.second as u32; total_seconds } /// Clone the time and set a new timezone offset. /// /// The returned time will represent a different point in time since the timezone offset is changed without /// modifying the time. See [Time::in_timezone] for alternative behaviour. /// /// # Arguments /// /// * `tz_offset` - optional timezone offset in seconds. /// /// This method will return `Err(ParseError::OutOfRangeTz)` if `abs(tz_offset)` is not less than /// 24 hours - `86_400` seconds. /// /// # Examples /// /// ``` /// use speedate::Time; /// /// let t1 = Time::parse_str("12:13:14Z").unwrap(); /// /// let t2 = t1.with_timezone_offset(Some(-8 * 3600)).unwrap(); /// assert_eq!(t2.to_string(), "12:13:14-08:00"); /// ``` pub fn with_timezone_offset(&self, tz_offset: Option) -> Result { if let Some(offset_val) = tz_offset { if offset_val.abs() >= 24 * 3600 { return Err(ParseError::OutOfRangeTz); } } let mut time = self.clone(); time.tz_offset = tz_offset; Ok(time) } /// Create a new time in a different timezone. /// See [Time::with_timezone_offset] for alternative behaviour. /// /// The time must have an offset, otherwise a `ParseError::TzRequired` error is returned. /// /// # Arguments /// /// * `tz_offset` - new timezone offset in seconds. /// /// # Examples /// /// ``` /// use speedate::Time; /// /// let t1 = Time::parse_str("15:00:00Z").unwrap(); /// /// let t2 = t1.in_timezone(7200).unwrap(); // / assert_eq!(t2.to_string(), "17:00:00+02:00"); /// ``` pub fn in_timezone(&self, tz_offset: i32) -> Result { if tz_offset.abs() >= 24 * 3600 { Err(ParseError::OutOfRangeTz) } else if let Some(current_offset) = self.tz_offset { let offset = tz_offset - current_offset; let seconds = self.total_seconds().saturating_add_signed(offset); let mut time = Self::from_timestamp(seconds, self.microsecond)?; time.tz_offset = Some(offset); Ok(time) } else { Err(ParseError::TzRequired) } } } /// Used internally for parsing both times and durations from time format pub(crate) struct PureTime { /// Hour: 0 to 23 hour: u8, /// Minute: 0 to 59 minute: u8, /// Second: 0 to 59 second: u8, /// microseconds: 0 to 999999 pub microsecond: u32, /// position of the cursor after parsing pub position: usize, } impl PureTime { pub fn parse(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result { if bytes.len() - offset < 5 { return Err(ParseError::TooShort); } let hour: u8; let minute: u8; unsafe { let h1 = get_digit_unchecked!(bytes, offset, InvalidCharHour); let h2 = get_digit_unchecked!(bytes, offset + 1, InvalidCharHour); hour = h1 * 10 + h2; match bytes.get_unchecked(offset + 2) { b':' => (), _ => return Err(ParseError::InvalidCharTimeSep), } let m1 = get_digit_unchecked!(bytes, offset + 3, InvalidCharMinute); let m2 = get_digit_unchecked!(bytes, offset + 4, InvalidCharMinute); minute = m1 * 10 + m2; } if hour > 23 { return Err(ParseError::OutOfRangeHour); } if minute > 59 { return Err(ParseError::OutOfRangeMinute); } let mut length: usize = 5; let (second, microsecond) = match bytes.get(offset + 5) { Some(b':') => { let s1 = get_digit!(bytes, offset + 6, InvalidCharSecond); let s2 = get_digit!(bytes, offset + 7, InvalidCharSecond); let second = s1 * 10 + s2; if second > 59 { return Err(ParseError::OutOfRangeSecond); } length = 8; let mut microsecond = 0; let frac_sep = bytes.get(offset + 8).copied(); if frac_sep == Some(b'.') || frac_sep == Some(b',') { length = 9; let mut i: usize = 0; loop { match bytes.get(offset + length + i) { Some(c) if c.is_ascii_digit() => { // If we've passed `i=6` then we are "truncating" the extra precision // The easiest way to do this is to simply no-op and continue the loop if i < 6 { microsecond *= 10; microsecond += (c - b'0') as u32; } } _ => { break; } } i += 1; if i > 6 { match config.microseconds_precision_overflow_behavior { MicrosecondsPrecisionOverflowBehavior::Truncate => continue, MicrosecondsPrecisionOverflowBehavior::Error => { return Err(ParseError::SecondFractionTooLong) } } } } if i == 0 { return Err(ParseError::SecondFractionMissing); } if i < 6 { microsecond *= 10_u32.pow(6 - i as u32); } length += i; } (second, microsecond) } _ => (0, 0), }; Ok(Self { hour, minute, second, microsecond, position: offset + length, }) } pub fn total_seconds(&self) -> u32 { self.hour as u32 * 3_600 + self.minute as u32 * 60 + self.second as u32 } } #[derive(Debug, Clone, Default, Copy, PartialEq)] pub enum MicrosecondsPrecisionOverflowBehavior { Truncate, #[default] Error, } impl TryFrom<&str> for MicrosecondsPrecisionOverflowBehavior { type Error = ConfigError; fn try_from(value: &str) -> Result { match value.to_lowercase().as_str() { "truncate" => Ok(Self::Truncate), "error" => Ok(Self::Error), _ => Err(ConfigError::UnknownMicrosecondsPrecisionOverflowBehaviorString), } } } #[derive(Debug, Clone, Default, PartialEq)] pub struct TimeConfig { pub microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior, pub unix_timestamp_offset: Option, } impl TimeConfig { pub fn builder() -> TimeConfigBuilder { TimeConfigBuilder::new() } } #[derive(Debug, Clone, Default)] pub struct TimeConfigBuilder { microseconds_precision_overflow_behavior: Option, unix_timestamp_offset: Option, } impl TimeConfigBuilder { pub fn new() -> Self { Self::default() } pub fn microseconds_precision_overflow_behavior( mut self, microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior, ) -> Self { self.microseconds_precision_overflow_behavior = Some(microseconds_precision_overflow_behavior); self } pub fn unix_timestamp_offset(mut self, unix_timestamp_offset: Option) -> Self { self.unix_timestamp_offset = unix_timestamp_offset; self } pub fn build(self) -> TimeConfig { TimeConfig { microseconds_precision_overflow_behavior: self.microseconds_precision_overflow_behavior.unwrap_or_default(), unix_timestamp_offset: self.unix_timestamp_offset, } } } speedate-0.15.0/tests/main.rs000064400000000000000000001452061046102023000141450ustar 00000000000000use std::fs::File; use std::io::Read; use std::str::FromStr; use chrono::{Datelike, FixedOffset as ChronoFixedOffset, NaiveDate, NaiveDateTime, Timelike, Utc as ChronoUtc}; use strum::EnumMessage; use speedate::{ float_parse_bytes, float_parse_str, int_parse_bytes, int_parse_str, Date, DateTime, Duration, IntFloat, MicrosecondsPrecisionOverflowBehavior, ParseError, Time, TimeConfig, TimeConfigBuilder, }; /// macro for expected values macro_rules! expect_ok_or_error { ($type:ty, $name:ident, ok, $input:literal, $expected:expr) => { paste::item! { #[test] fn [< expect_ $name _ok >]() { let v = <$type>::parse_str($input).unwrap(); assert_eq!(v.to_string(), $expected); } } }; ($type:ty, $name:ident, err, $input:literal, $error:expr) => { paste::item! { #[test] fn [< expect_ $name _ $error:snake _error >]() { match <$type>::parse_str($input) { Ok(t) => panic!("unexpectedly valid: {:?} -> {:?}", $input, t), Err(e) => assert_eq!(e, ParseError::$error), } } } }; } /// macro for comparing a type's `from_str` and `parse_str` methods macro_rules! expect_fromstr_matches_parse_str { ($type:ty, $name:ident, ok, $_expected:expr, $example:expr) => { paste::item! { #[test] fn [< expect_ $name _fromstr_matches_parse_str_ok >]() { expect_fromstr_matches_parse_str!(@body $type, $example); } } }; ($type:ty, $name:ident, err, $error:expr, $example:expr) => { paste::item! { #[test] fn [< expect_ $name _fromstr_matches_parse_str_ $error:snake _error >]() { expect_fromstr_matches_parse_str!(@body $type, $example); } } }; (@body $type:ty, $example:expr) => {{ let fromstr = <$type>::from_str($example); let parse_str = <$type>::parse_str($example); assert_eq!(fromstr, parse_str, "fromstr: {fromstr:?}, parse_str: {parse_str:?}"); }}; } /// macro to define many tests for expected values macro_rules! param_tests { ($type:ty, $($name:ident: $ok_or_err:ident => $input:literal, $expected:expr;)*) => { $( expect_ok_or_error!($type, $name, $ok_or_err, $input, $expected); expect_fromstr_matches_parse_str!($type, $name, $ok_or_err, $expected, $input); )* } } #[test] fn date() { let d = Date::parse_str("2020-01-01").unwrap(); assert_eq!( d, Date { year: 2020, month: 1, day: 1 } ); assert_eq!(d.to_string(), "2020-01-01"); assert_eq!(format!("{d:?}"), "Date { year: 2020, month: 1, day: 1 }"); } #[test] fn date_bytes_err() { // https://github.com/python/cpython/blob/5849af7a80166e9e82040e082f22772bd7cf3061/Lib/test/datetimetester.py#L3237 // bytes of '\ud800' let bytes: Vec = vec![92, 117, 100, 56, 48, 48]; match Date::parse_bytes_rfc3339(&bytes) { Ok(_) => panic!("unexpectedly valid"), Err(e) => assert_eq!(e, ParseError::TooShort), } let bytes: Vec = vec![b'2', b'0', b'0', b'0', 92, 117, 100, 56, 48, 48]; match Date::parse_bytes_rfc3339(&bytes) { Ok(_) => panic!("unexpectedly valid"), Err(e) => assert_eq!(e, ParseError::InvalidCharDateSep), } } #[test] fn error_str() { let error = match Date::parse_str_rfc3339("123") { Ok(_) => panic!("unexpectedly valid"), Err(e) => e, }; assert_eq!(error, ParseError::TooShort); assert_eq!(error.to_string(), "too_short"); assert_eq!(error.get_documentation(), Some("input is too short")); } param_tests! { Date, // date_short_3: err => "123", TooShort; date_short_9: err => "2000:12:1", TooShort; date: err => "xxxx:12:31", InvalidCharYear; date_year_sep: err => "2020x12:13", InvalidCharDateSep; date_mo_sep: err => "2020-12x13", InvalidCharDateSep; date: err => "2020-13-01", OutOfRangeMonth; date: err => "2020-04-31", OutOfRangeDay; date_extra_space: err => "2020-04-01 ", ExtraCharacters; date_extra_xxx: err => "2020-04-01xxx", ExtraCharacters; // leap year dates date_simple: ok => "2020-04-01", "2020-04-01"; date_normal_not_leap: ok => "2003-02-28", "2003-02-28"; date_normal_not_leap: err => "2003-02-29", OutOfRangeDay; date_normal_leap_year: ok => "2004-02-29", "2004-02-29"; date_special_100_not_leap: err => "1900-02-29", OutOfRangeDay; date_special_400_leap: ok => "2000-02-29", "2000-02-29"; date_special_1600ad_leap: ok => "1600-02-29", "1600-02-29"; date_special_1200ad_leap: ok => "1200-02-29", "1200-02-29"; date_special_1201ad_not_leap: err => "1201-02-29", OutOfRangeDay; date_special_1202ad_not_leap: err => "1202-02-29", OutOfRangeDay; date_special_1203ad_not_leap: err => "1203-02-29", OutOfRangeDay; date_special_1204ad_leap: ok => "1204-02-29", "1204-02-29"; date_special_1300ad_not_leap: err => "1300-02-29", OutOfRangeDay; date_special_1400ad_not_leap: err => "1400-02-29", OutOfRangeDay; date_special_1500ad_not_leap: err => "1500-02-29", OutOfRangeDay; date_special_1bc_leap: ok => "0000-02-29", "0000-02-29"; date_special_1ad_not_leap: err => "0001-02-29", OutOfRangeDay; date_special_4ad_leap: ok => "0004-02-29", "0004-02-29"; date_special_100ad_not_leap: err => "0100-02-29", OutOfRangeDay; date_special_200ad_not_leap: err => "0200-02-29", OutOfRangeDay; date_special_300ad_not_leap: err => "0300-02-29", OutOfRangeDay; date_special_400ad_leap: ok => "0400-02-29", "0400-02-29"; date_special_404ad_leap: ok => "0404-02-29", "0404-02-29"; date_unix_before_watershed: ok => "19999872000", "2603-10-10"; date_unix_after_watershed: ok => "20044800000", "1970-08-21"; date_unix_too_low: err => "-62167219200001", DateTooSmall; } #[test] fn date_from_timestamp_extremes() { match Date::from_timestamp(i64::MIN, false) { Ok(dt) => panic!("unexpectedly valid, {dt}"), Err(e) => assert_eq!(e, ParseError::DateTooSmall), } match Date::from_timestamp(i64::MAX, false) { Ok(dt) => panic!("unexpectedly valid, {dt}"), Err(e) => assert_eq!(e, ParseError::DateTooLarge), } let d = Date::from_timestamp(-62_167_219_200_000, false).unwrap(); assert_eq!(d.to_string(), "0000-01-01"); match Date::from_timestamp(-62_167_219_200_001, false) { Ok(dt) => panic!("unexpectedly valid, {dt}"), Err(e) => assert_eq!(e, ParseError::DateTooSmall), } let d = Date::from_timestamp(253_402_300_799_000, false).unwrap(); assert_eq!(d.to_string(), "9999-12-31"); match Date::from_timestamp(253_402_300_800_000, false) { Ok(dt) => panic!("unexpectedly valid, {dt}"), Err(e) => assert_eq!(e, ParseError::DateTooLarge), } } #[test] fn date_from_timestamp_special_dates() { let d = Date::from_timestamp(-11_676_096_000 + 1000, false).unwrap(); assert_eq!(d.to_string(), "1600-01-01"); // check if there is any error regarding offset at the second level // and if rounding down works let d = Date::from_timestamp(-11_676_096_000 + 86399, false).unwrap(); assert_eq!(d.to_string(), "1600-01-01"); let d = Date::from_timestamp(-11_673_417_600, false).unwrap(); assert_eq!(d.to_string(), "1600-02-01"); } #[test] fn date_watershed() { let dt = Date::from_timestamp(20_000_000_000, false).unwrap(); assert_eq!(dt.to_string(), "2603-10-11"); let dt = Date::from_timestamp(20_000_000_001, false).unwrap(); assert_eq!(dt.to_string(), "1970-08-20"); let dt = Date::from_timestamp(-20_000_000_000, false).unwrap(); assert_eq!(dt.to_string(), "1336-03-23"); let dt = Date::from_timestamp(-20_000_000_001, false).unwrap(); assert_eq!(dt.to_string(), "1969-05-14"); } #[test] fn date_from_timestamp_milliseconds() { let d1 = Date::from_timestamp(1_654_472_524, false).unwrap(); assert_eq!( d1, Date { year: 2022, month: 6, day: 5 } ); let d2 = Date::from_timestamp(1_654_472_524_000, false).unwrap(); assert_eq!(d2, d1); } fn try_date_timestamp(ts: i64, check_timestamp: bool) { let chrono_date = NaiveDateTime::from_timestamp_opt(ts, 0).unwrap().date(); let d = Date::from_timestamp(ts, false).unwrap(); // println!("{} => {:?}", ts, d); assert_eq!( d, Date { year: chrono_date.year() as u16, month: chrono_date.month() as u8, day: chrono_date.day() as u8, }, "timestamp: {ts} => {chrono_date}" ); if check_timestamp { assert_eq!(d.timestamp(), ts); } } #[test] fn date_from_timestamp_range() { for ts in (0..4_000_000_000).step_by(86_400) { try_date_timestamp(ts, true); try_date_timestamp(ts + 40_000, false); try_date_timestamp(-ts, true); try_date_timestamp(-ts - 40_000, false); } } #[test] fn date_comparison() { let d1 = Date::parse_str("2020-02-03").unwrap(); let d2 = Date::parse_str("2021-01-02").unwrap(); assert!(d1 < d2); assert!(d1 <= d2); assert!(d1 <= d1.clone()); assert!(d2 > d1); assert!(d2 >= d1); assert!(d2 >= d2.clone()); } #[test] fn date_timestamp_exact() { let d = Date::from_timestamp(1_654_560_000, true).unwrap(); assert_eq!(d.to_string(), "2022-06-07"); assert_eq!(d.timestamp(), 1_654_560_000); match Date::from_timestamp(1_654_560_001, true) { Ok(d) => panic!("unexpectedly valid, {d}"), Err(e) => assert_eq!(e, ParseError::DateNotExact), } // milliseconds let d = Date::from_timestamp(1_654_560_000_000, true).unwrap(); assert_eq!(d.to_string(), "2022-06-07"); assert_eq!(d.timestamp(), 1_654_560_000); match Date::from_timestamp(1_654_560_000_001, true) { Ok(d) => panic!("unexpectedly valid, {d}"), Err(e) => assert_eq!(e, ParseError::DateNotExact), } } macro_rules! date_from_timestamp { ($($year:literal, $month:literal, $day:literal;)*) => { $( paste::item! { #[test] fn [< date_from_timestamp_ $year _ $month _ $day >]() { let chrono_date = NaiveDate::from_ymd_opt($year, $month, $day).unwrap(); let ts = chrono_date.and_hms_opt(0, 0, 0).unwrap().timestamp(); let d = Date::from_timestamp(ts, false).unwrap(); assert_eq!( d, Date { year: $year, month: $month, day: $day, }, "timestamp: {} => {}", ts, chrono_date ); } } )* } } date_from_timestamp! { 1970, 1, 1; 1970, 1, 31; 1970, 2, 1; 1970, 2, 28; 1970, 3, 1; 1600, 1, 1; 1601, 1, 1; 1700, 1, 1; 1842, 8, 20; 1900, 1, 1; 1900, 6, 1; 1901, 1, 1; 1904, 1, 1; 1904, 2, 29; 1904, 6, 1; 1924, 6, 1; 2200, 1, 1; } #[test] fn date_today() { let today = Date::today(0).unwrap(); let chrono_now = ChronoUtc::now(); assert_eq!( today, Date { year: chrono_now.year() as u16, month: chrono_now.month() as u8, day: chrono_now.day() as u8, } ); } #[test] fn date_today_offset() { for offset in (-86399..86399).step_by(1000) { let today = Date::today(offset).unwrap(); let chrono_now_utc = ChronoUtc::now(); let chrono_tz = ChronoFixedOffset::east_opt(offset).unwrap(); let chrono_now = chrono_now_utc.with_timezone(&chrono_tz); assert_eq!( today, Date { year: chrono_now.year() as u16, month: chrono_now.month() as u8, day: chrono_now.day() as u8, } ); } } macro_rules! time_from_timestamp { ($($ts_secs:literal, $ts_micro:literal => $hour:literal, $minute:literal, $second:literal, $microsecond:literal;)*) => { $( paste::item! { #[test] fn [< time_from_timestamp_ $hour _ $minute _ $second _ $microsecond >]() { let d = Time::from_timestamp($ts_secs, $ts_micro).unwrap(); assert_eq!(d, Time { hour: $hour, minute: $minute, second: $second, microsecond: $microsecond, tz_offset: None, }, "timestamp: {} => {}:{}:{}.{}", $ts_secs, $hour, $minute, $second, $microsecond); } } )* } } time_from_timestamp! { 0, 0 => 0, 0, 0, 0; 1, 0 => 0, 0, 1, 0; 3600, 0 => 1, 0, 0, 0; 3700, 0 => 1, 1, 40, 0; 86399, 0 => 23, 59, 59, 0; 0, 100 => 0, 0, 0, 100; 0, 5_000_000 => 0, 0, 5, 0; 36_005, 1_500_000 => 10, 0, 6, 500_000; } #[test] fn time_from_timestamp_error() { match Time::from_timestamp(86400, 0) { Ok(_) => panic!("unexpectedly valid"), Err(e) => assert_eq!(e, ParseError::TimeTooLarge), } match Time::from_timestamp(86390, 10_000_000) { Ok(_) => panic!("unexpectedly valid"), Err(e) => assert_eq!(e, ParseError::TimeTooLarge), } match Time::from_timestamp(u32::MAX, u32::MAX) { Ok(_) => panic!("unexpectedly valid"), Err(e) => assert_eq!(e, ParseError::TimeTooLarge), } } fn try_datetime_timestamp(chrono_dt: NaiveDateTime) { let ts = chrono_dt.timestamp(); let dt = DateTime::from_timestamp(ts, chrono_dt.nanosecond() / 1_000).unwrap(); // println!("{} ({}) => {}", ts, chrono_dt, dt); assert_eq!( dt, DateTime { date: Date { year: chrono_dt.year() as u16, month: chrono_dt.month() as u8, day: chrono_dt.day() as u8, }, time: Time { hour: chrono_dt.hour() as u8, minute: chrono_dt.minute() as u8, second: chrono_dt.second() as u8, microsecond: chrono_dt.nanosecond() / 1_000, tz_offset: None, }, }, "timestamp: {ts} => {chrono_dt}" ); assert_eq!(dt.timestamp(), ts); } macro_rules! datetime_from_timestamp { ($($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal, $microsecond:literal;)*) => { $( paste::item! { #[test] fn [< datetime_from_timestamp_ $year _ $month _ $day _t_ $hour _ $minute _ $second _ $microsecond >]() { let chrono_dt = NaiveDate::from_ymd_opt($year, $month, $day).unwrap().and_hms_nano_opt($hour, $minute, $second, $microsecond * 1_000).unwrap(); try_datetime_timestamp(chrono_dt); } } )* } } datetime_from_timestamp! { 1970, 1, 1, 0, 0, 0, 0; 1970, 1, 1, 0, 0, 1, 0; 1970, 1, 1, 0, 1, 0, 0; 1970, 1, 2, 0, 0, 0, 0; 1970, 1, 2, 0, 0, 0, 500000; 1969, 12, 30, 15, 51, 29, 10630; } #[test] fn datetime_from_timestamp_range() { for ts in (0..157_766_400).step_by(757) { try_datetime_timestamp(NaiveDateTime::from_timestamp_opt(ts, 0).unwrap()); try_datetime_timestamp(NaiveDateTime::from_timestamp_opt(-ts, 0).unwrap()); } } #[test] fn datetime_from_timestamp_specific() { let dt = DateTime::from_timestamp(-11676095999, 4291493).unwrap(); assert_eq!(dt.to_string(), "1600-01-01T00:00:05.291493"); let dt = DateTime::from_timestamp(-1, 1667444).unwrap(); assert_eq!(dt.to_string(), "1970-01-01T00:00:00.667444"); let dt = DateTime::from_timestamp(32_503_680_000_000, 0).unwrap(); assert_eq!(dt.to_string(), "3000-01-01T00:00:00"); let dt = DateTime::from_timestamp(-11_676_096_000, 0).unwrap(); assert_eq!(dt.to_string(), "1600-01-01T00:00:00"); let dt = DateTime::from_timestamp(1_095_216_660_480, 3221223).unwrap(); assert_eq!(dt.to_string(), "2004-09-15T02:51:03.701223"); let d = DateTime::from_timestamp(253_402_300_799_000, 999999).unwrap(); assert_eq!(d.to_string(), "9999-12-31T23:59:59.999999"); match Date::from_timestamp(253_402_300_800_000, false) { Ok(dt) => panic!("unexpectedly valid, {dt}"), Err(e) => assert_eq!(e, ParseError::DateTooLarge), } } #[test] fn datetime_watershed() { let dt = DateTime::from_timestamp(20_000_000_000, 0).unwrap(); assert_eq!(dt.to_string(), "2603-10-11T11:33:20"); let dt = DateTime::from_timestamp(20_000_000_001, 0).unwrap(); assert_eq!(dt.to_string(), "1970-08-20T11:33:20.001000"); let dt = DateTime::from_timestamp(-20_000_000_001, 0).unwrap(); assert_eq!(dt.to_string(), "1969-05-14T12:26:39.999000"); let dt = DateTime::from_timestamp(-20_000_000_000, 0).unwrap(); assert_eq!(dt.to_string(), "1336-03-23T12:26:40"); } #[test] fn datetime_now() { let speedate_now = DateTime::now(0).unwrap(); let chrono_now = ChronoUtc::now(); let diff = speedate_now.timestamp() as f64 - chrono_now.timestamp() as f64; assert!(diff.abs() < 0.1); } #[test] fn datetime_now_offset() { let speedate_now = DateTime::now(3600).unwrap(); let chrono_now = ChronoUtc::now(); let diff = speedate_now.timestamp() as f64 - chrono_now.timestamp() as f64 - 3600.0; assert!(diff.abs() < 0.1); let diff = speedate_now.timestamp_tz() as f64 - chrono_now.timestamp() as f64; assert!(diff.abs() < 0.1); } #[test] fn datetime_with_tz_offset() { let dt_z = DateTime::parse_str("2022-01-01T12:13:14.567+00:00").unwrap(); let dt_m8 = dt_z.with_timezone_offset(Some(-8 * 3600)).unwrap(); assert_eq!(dt_m8.to_string(), "2022-01-01T12:13:14.567000-08:00"); let dt_naive = dt_z.with_timezone_offset(None).unwrap(); assert_eq!(dt_naive.to_string(), "2022-01-01T12:13:14.567000"); let dt_naive = DateTime::parse_str("2000-01-01T00:00:00").unwrap(); let dt_p16 = dt_naive.with_timezone_offset(Some(16 * 3600)).unwrap(); assert_eq!(dt_p16.to_string(), "2000-01-01T00:00:00+16:00"); let error = match dt_naive.with_timezone_offset(Some(86_400)) { Ok(_) => panic!("unexpectedly valid"), Err(e) => e, }; assert_eq!(error, ParseError::OutOfRangeTz); } #[test] fn datetime_in_timezone() { let dt_z = DateTime::parse_str("2000-01-01T15:00:00.567000Z").unwrap(); let dt_p1 = dt_z.in_timezone(3_600).unwrap(); assert_eq!(dt_p1.to_string(), "2000-01-01T16:00:00.567000+01:00"); let dt_m2 = dt_z.in_timezone(-7_200).unwrap(); assert_eq!(dt_m2.to_string(), "2000-01-01T13:00:00.567000-02:00"); let dt_naive = DateTime::parse_str("2000-01-01T00:00:00").unwrap(); let error = match dt_naive.in_timezone(3_600) { Ok(_) => panic!("unexpectedly valid"), Err(e) => e, }; assert_eq!(error, ParseError::TzRequired); let error = match dt_z.in_timezone(86_400) { Ok(_) => panic!("unexpectedly valid"), Err(e) => e, }; assert_eq!(error, ParseError::OutOfRangeTz); } #[test] fn time() { let t = Time::parse_str("12:13:14.123456").unwrap(); assert_eq!( t, Time { hour: 12, minute: 13, second: 14, microsecond: 123456, tz_offset: None, } ); assert_eq!(t.to_string(), "12:13:14.123456"); assert_eq!( format!("{t:?}"), "Time { hour: 12, minute: 13, second: 14, microsecond: 123456, tz_offset: None }" ); } #[test] fn time_comparison() { let t1 = Time::parse_str("12:13:14").unwrap(); let t2 = Time::parse_str("12:10:20").unwrap(); assert!(t1 > t2); assert!(t1 >= t2); assert!(t1 >= t1.clone()); assert!(t2 < t1); assert!(t2 <= t1); assert!(t2 <= t2.clone()); assert!(t1.eq(&t1.clone())); assert!(!t1.eq(&t2)); let t3 = Time::parse_str("12:13:14.123").unwrap(); let t4 = Time::parse_str("12:13:13.999").unwrap(); assert!(t3 > t4); } #[test] fn time_comparison_timezone() { let t1 = Time::parse_str("12:10:00+00:00").unwrap(); let t2 = Time::parse_str("12:10:00+01:00").unwrap(); assert!(t1 > t2); assert!(t1 >= t2); assert!(t1 >= t1.clone()); assert!(t2 < t1); assert!(t2 <= t1); assert!(t2 <= t2.clone()); assert!(t1.eq(&t1.clone())); assert!(!t1.eq(&t2)); let t3 = Time::parse_str("12:13:13.999+00:00").unwrap(); let t4 = Time::parse_str("12:13:13.123+00:00").unwrap(); assert!(t3 > t4); let t5 = Time::parse_str("12:13:13-20:00").unwrap(); let t6 = Time::parse_str("12:13:13+00:00").unwrap(); assert!(t5 > t6); } #[test] fn time_total_seconds() { let t = Time::parse_str("01:02:03.04").unwrap(); assert_eq!(t.total_seconds(), 3600 + 2 * 60 + 3); let t = Time::parse_str("12:13:14.999999").unwrap(); assert_eq!(t.total_seconds(), 12 * 3600 + 13 * 60 + 14); } #[test] fn time_with_tz_offset() { let t_z = Time::parse_str("12:13:14.567+00:00").unwrap(); let t_m8 = t_z.with_timezone_offset(Some(-8 * 3600)).unwrap(); assert_eq!(t_m8.to_string(), "12:13:14.567000-08:00"); let t_naive = t_z.with_timezone_offset(None).unwrap(); assert_eq!(t_naive.to_string(), "12:13:14.567000"); let t_naive = Time::parse_str("00:00:00").unwrap(); let t_p16 = t_naive.with_timezone_offset(Some(16 * 3600)).unwrap(); assert_eq!(t_p16.to_string(), "00:00:00+16:00"); let error = match t_naive.with_timezone_offset(Some(86_400)) { Ok(_) => panic!("unexpectedly valid"), Err(e) => e, }; assert_eq!(error, ParseError::OutOfRangeTz); } #[test] fn time_in_timezone() { let t_z = Time::parse_str("15:00:00.567Z").unwrap(); let t_p1 = t_z.in_timezone(3_600).unwrap(); assert_eq!(t_p1.to_string(), "16:00:00.567000+01:00"); let t_m2 = t_z.in_timezone(-7_200).unwrap(); assert_eq!(t_m2.to_string(), "13:00:00.567000-02:00"); let t_naive = Time::parse_str("10:00:00").unwrap(); let error = match t_naive.in_timezone(3_600) { Ok(_) => panic!("unexpectedly valid"), Err(e) => e, }; assert_eq!(error, ParseError::TzRequired); let error = match t_z.in_timezone(86_400) { Ok(_) => panic!("unexpectedly valid"), Err(e) => e, }; assert_eq!(error, ParseError::OutOfRangeTz); } param_tests! { Time, time_min: ok => "00:00:00.000000", "00:00:00"; time_max: ok => "23:59:59.999999", "23:59:59.999999"; time_no_fraction: ok => "12:13:14", "12:13:14"; time_fraction_small: ok => "12:13:14.123", "12:13:14.123000"; time_no_sec: ok => "12:13", "12:13:00"; time_tz: ok => "12:13:14z", "12:13:14Z"; time: err => "xxx", TooShort; time: err => "xx:12", InvalidCharHour; time_sep_hour: err => "12x12", InvalidCharTimeSep; time: err => "12:x0", InvalidCharMinute; time_sep_min: err => "12:13x", InvalidCharTzSign; time: err => "12:13:x", InvalidCharSecond; time: err => "12:13:12.", SecondFractionMissing; time: err => "12:13:12.1234567", SecondFractionTooLong; time: err => "24:00:00", OutOfRangeHour; time: err => "23:60:00", OutOfRangeMinute; time: err => "23:59:60", OutOfRangeSecond; time_extra_x: err => "23:59:59xxx", InvalidCharTzSign; time_extra_space: err => "23:59:59 ", InvalidCharTzSign; } #[test] fn datetime_naive() { let dt = DateTime::parse_str("2020-01-01T12:13:14.123456").unwrap(); assert_eq!( dt, DateTime { date: Date { year: 2020, month: 1, day: 1, }, time: Time { hour: 12, minute: 13, second: 14, microsecond: 123456, tz_offset: None, }, } ); assert_eq!(dt.to_string(), "2020-01-01T12:13:14.123456"); assert_eq!( format!("{dt:?}"), "DateTime { date: Date { year: 2020, month: 1, day: 1 }, time: Time { hour: 12, minute: 13, second: 14, microsecond: 123456, tz_offset: None } }" ); } #[test] fn datetime_tz_z() { let dt = DateTime::parse_str("2020-01-01 12:13:14z").unwrap(); assert_eq!( dt, DateTime { date: Date { year: 2020, month: 1, day: 1, }, time: Time { hour: 12, minute: 13, second: 14, microsecond: 0, tz_offset: Some(0), }, } ); assert_eq!(dt.to_string(), "2020-01-01T12:13:14Z"); } #[test] fn datetime_bytes() { let dt = DateTime::parse_bytes(b"2020-01-01 12:13:14z").unwrap(); assert_eq!(dt.to_string(), "2020-01-01T12:13:14Z"); } #[test] fn datetime_tz_2hours() { let dt = DateTime::parse_str("2020-01-01T12:13:14+02:00").unwrap(); assert_eq!( dt, DateTime { date: Date { year: 2020, month: 1, day: 1, }, time: Time { hour: 12, minute: 13, second: 14, microsecond: 0, tz_offset: Some(7_200), }, } ); assert_eq!(dt.to_string(), "2020-01-01T12:13:14+02:00"); } #[test] fn datetime_tz_negative_2212() { // using U+2212 for negative timezones let dt = DateTime::parse_str("2020-01-01T12:13:14−02:15").unwrap(); assert_eq!(dt.time.tz_offset, Some(-8100)); assert_eq!(dt.to_string(), "2020-01-01T12:13:14-02:15"); let dt = DateTime::parse_str("2020-01-01T12:13:14−00:01").unwrap(); assert_eq!(dt.time.tz_offset, Some(-60)); assert_eq!(dt.to_string(), "2020-01-01T12:13:14-00:01"); let dt = DateTime::parse_str("2020-01-01T12:13:14−01:00").unwrap(); assert_eq!(dt.time.tz_offset, Some(-3600)); assert_eq!(dt.to_string(), "2020-01-01T12:13:14-01:00"); } #[test] fn datetime_timestamp() { let dt = DateTime::from_timestamp(1_000_000_000, 999_999).unwrap(); assert_eq!(dt.to_string(), "2001-09-09T01:46:40.999999"); assert_eq!(dt.timestamp(), 1_000_000_000); // using ms unix timestamp let dt = DateTime::from_timestamp(1_000_000_000_000, 999_999).unwrap(); assert_eq!(dt.to_string(), "2001-09-09T01:46:40.999999"); assert_eq!(dt.timestamp(), 1_000_000_000); // using ms unix timestamp let d_naive = DateTime::parse_str("1970-01-02T00:00").unwrap(); assert_eq!(d_naive.timestamp(), 86400); } #[test] fn datetime_timestamp_tz() { let t_naive = DateTime::parse_str("1970-01-02T00:00").unwrap(); assert_eq!(t_naive.timestamp(), 24 * 3600); assert_eq!(t_naive.timestamp_tz(), 24 * 3600); let dt_zulu = DateTime::parse_str("1970-01-02T00:00Z").unwrap(); assert_eq!(dt_zulu.timestamp(), 24 * 3600); assert_eq!(dt_zulu.timestamp_tz(), 24 * 3600); let dt_plus_1 = DateTime::parse_str("1970-01-02T00:00+01:00").unwrap(); assert_eq!(dt_plus_1.timestamp(), 24 * 3600); assert_eq!(dt_plus_1.timestamp_tz(), 23 * 3600); } #[test] fn datetime_comparison_naive() { let dt1 = DateTime::parse_str("2020-02-03T04:05:06.07").unwrap(); let dt2 = DateTime::parse_str("2021-01-02T03:04:05.06").unwrap(); assert!(dt2 > dt1); assert!(dt2 >= dt1); assert!(dt2 >= dt2.clone()); assert!(dt1 < dt2); assert!(dt1 <= dt2); assert!(dt1 <= dt1.clone()); let dt3 = DateTime::parse_str("2020-02-03T04:05:06.123").unwrap(); let dt4 = DateTime::parse_str("2020-02-03T04:05:06.124").unwrap(); assert!(dt4 > dt3); assert!(dt3 < dt4); } #[test] fn datetime_comparison_timezone() { let dt1 = DateTime::parse_str("2000-01-01T00:00:00+01:00").unwrap(); let dt2 = DateTime::parse_str("2000-01-01T00:00:00+02:00").unwrap(); assert!(dt1 > dt2); assert!(dt1 >= dt2); assert!(dt2 < dt1); assert!(dt2 <= dt1); let dt3 = DateTime::parse_str("2000-01-01T00:00:00").unwrap(); assert!(dt1 >= dt3); assert!(dt3 <= dt1); let dt4 = DateTime::parse_str("1970-01-01T04:00:00.222+02:00").unwrap(); assert_eq!(dt4.timestamp_tz(), 2 * 3600); let dt5 = DateTime::parse_str("1970-01-01T03:00:00Z").unwrap(); assert_eq!(dt5.timestamp_tz(), 3 * 3600); assert!(dt5 > dt4); assert!(dt4 <= dt4.clone()); // assert that microseconds are used for comparison here let dt6 = DateTime::parse_str("1970-01-01T04:00:00.333+02:00").unwrap(); assert_eq!(dt6.timestamp_tz(), 2 * 3600); assert!(dt6 > dt4); assert_ne!(dt6, dt4); // even on different dates, tz has an effect let dt7 = DateTime::parse_str("2022-01-01T23:00:00Z").unwrap(); let dt8 = DateTime::parse_str("2022-01-02T01:00:00+03:00").unwrap(); assert!(dt7 > dt8); } param_tests! { DateTime, dt_longest: ok => "2020-01-01T12:13:14.123456−02:15", "2020-01-01T12:13:14.123456-02:15"; dt_tz_negative: ok => "2020-01-01T12:13:14-02:15", "2020-01-01T12:13:14-02:15"; dt_tz_negative_10: ok => "2020-01-01T12:13:14-11:30", "2020-01-01T12:13:14-11:30"; dt_tz_no_colon: ok => "2020-01-01T12:13:14+1234", "2020-01-01T12:13:14+12:34"; dt_seconds_fraction_break: ok => "2020-01-01 12:13:14.123z", "2020-01-01T12:13:14.123000Z"; dt_seconds_fraction_comma: ok => "2020-01-01 12:13:14,123z", "2020-01-01T12:13:14.123000Z"; dt_underscore: ok => "2020-01-01_12:13:14,123z", "2020-01-01T12:13:14.123000Z"; dt_unix1: ok => "1654646400", "2022-06-08T00:00:00"; dt_unix2: ok => "1654646404", "2022-06-08T00:00:04"; dt_unix_1_neg: ok => "-1654646400", "1917-07-27T00:00:00"; dt_unix_2_neg: ok => "-1654646404", "1917-07-26T23:59:56"; dt_unix_float: ok => "1654646404.5", "2022-06-08T00:00:04.500000"; dt_unix_float_limit: ok => "1654646404.123456", "2022-06-08T00:00:04.123456"; dt_unix_float_ms: ok => "1654646404000.5", "2022-06-08T00:00:04.000500"; dt_unix_float_ms_limit: ok => "1654646404123.456", "2022-06-08T00:00:04.123456"; dt_unix_float_ms_neg: ok => "-1654646404.123456", "1917-07-26T23:59:55.876544"; dt_unix_float_ms_neg_limit: ok => "-1654646404000.123", "1917-07-26T23:59:55.999877"; dt_unix_float_empty: ok => "1654646404.", "2022-06-08T00:00:04"; dt_unix_float_ms_empty: ok => "1654646404000.", "2022-06-08T00:00:04"; dt_unix_float_too_long: err => "1654646404.1234567", SecondFractionTooLong; dt_unix_float_ms_too_long: err => "1654646404123.4567", MillisecondFractionTooLong; dt_short_date: err => "xxx", TooShort; dt_short_time: err => "2020-01-01T12:0", TooShort; dt: err => "202x-01-01", InvalidCharYear; dt: err => "2020-01-01x", InvalidCharDateTimeSep; dt: err => "2020-01-01Txx:00", InvalidCharHour; dt_1: err => "2020-01-01T12:00:00x", InvalidCharTzSign; // same first byte as U+2212, different second b'\xe2\x89\x92'.decode() dt_2: err => "2020-01-01T12:00:00≒", InvalidCharTzSign; // same first and second bytes as U+2212, different third b'\xe2\x88\x93'.decode() dt_3: err => "2020-01-01T12:00:00∓", InvalidCharTzSign; dt: err => "2020-01-01T12:00:00+x", InvalidCharTzHour; dt: err => "2020-01-01T12:00:00+00x", InvalidCharTzMinute; dt_extra_space_z: err => "2020-01-01T12:00:00Z ", ExtraCharacters; dt_extra_space_tz1: err => "2020-01-01T12:00:00+00:00 ", ExtraCharacters; dt_extra_space_tz2: err => "2020-01-01T12:00:00+0000 ", ExtraCharacters; dt_extra_xxx: err => "2020-01-01T12:00:00Zxxx", ExtraCharacters; tz_pos_2359: ok => "2020-01-01T12:00:00+23:59", "2020-01-01T12:00:00+23:59"; tz_neg_2359: ok => "2020-01-01T12:00:00-23:59", "2020-01-01T12:00:00-23:59"; tz_60mins: err => "2020-01-01T12:00:00+00:60", OutOfRangeTzMinute; tz_pos_gt2359: err => "2020-01-01T12:00:00+24:00", OutOfRangeTz; tz_neg_gt2359: err => "2020-01-01T12:00:00-24:00", OutOfRangeTz; tz_pos_99hr: err => "2020-01-01T12:00:00+99:59", OutOfRangeTz; tz_neg_99hr: err => "2020-01-01T12:00:00-99:59", OutOfRangeTz; } fn extract_values(line: &str, prefix: &str) -> (String, String) { let parts: Vec<&str> = line.trim_start_matches(prefix).split("->").collect(); assert_eq!(parts.len(), 2); (parts[0].trim().to_string(), parts[1].trim().to_string()) } #[test] fn test_ok_values_txt() { let mut f = File::open("./tests/values_ok.txt").unwrap(); let mut contents = String::new(); f.read_to_string(&mut contents).unwrap(); let mut success = 0; for (i, line) in contents.split('\n').enumerate() { let line_no = i + 1; if line.starts_with('#') || line.is_empty() || line == "\r" { continue; } else if line.starts_with("date:") { let (input, expected_str) = extract_values(line, "date:"); let d = Date::parse_str(&input) .map_err(|e| panic!("error on line {line_no} {line:?}: {e:?}")) .unwrap(); assert_eq!(d.to_string(), expected_str, "error on line {line_no}"); } else if line.starts_with("time:") { let (input, expected_str) = extract_values(line, "time:"); let t = Time::parse_str(&input) .map_err(|e| panic!("error on line {line_no} {line:?}: {e:?}")) .unwrap(); assert_eq!(t.to_string(), expected_str, "error on line {line_no}"); } else if line.starts_with("dt:") { let (input, expected_str) = extract_values(line, "dt:"); let dt = DateTime::parse_str(&input) .map_err(|e| panic!("error on line {line_no} {line:?}: {e:?}")) .unwrap(); assert_eq!(dt.to_string(), expected_str, "error on line {line_no}"); } else { panic!("unexpected line: {line:?}"); } success += 1; } println!("{success} formats successfully parsed"); } #[test] fn test_err_values_txt() { let mut f = File::open("./tests/values_err.txt").unwrap(); let mut contents = String::new(); f.read_to_string(&mut contents).unwrap(); let mut success = 0; for (i, line) in contents.split('\n').enumerate() { let line_no = i + 1; if line.starts_with('#') || line.is_empty() { continue; } if DateTime::parse_str_rfc3339(line.trim()).is_ok() { panic!("unexpected valid line {line_no}: {line:?}") } success += 1; } println!("{success} correctly invalid"); } #[test] fn duration_simple() { let d = Duration::parse_str("P1Y").unwrap(); assert_eq!( d, Duration { positive: true, day: 365, second: 0, microsecond: 0 } ); assert_eq!(d.to_string(), "P1Y"); } #[test] fn duration_zero() { let d = Duration::parse_str("PT0S").unwrap(); assert_eq!( d, Duration { positive: true, day: 0, second: 0, microsecond: 0 } ); assert_eq!(d.to_string(), "PT0S"); } #[test] fn duration_total_seconds() { let d = Duration::parse_str("P1MT1.5S").unwrap(); assert_eq!( d, Duration { positive: true, day: 30, second: 1, microsecond: 500_000 } ); assert_eq!(d.to_string(), "P30DT1.5S"); assert_eq!(d.signed_total_seconds(), 30 * 86_400 + 1); assert_eq!(d.signed_microseconds(), 500_000); } #[test] fn duration_total_seconds_neg() { let d = Duration::parse_str("-P1DT42.123456S").unwrap(); assert_eq!( d, Duration { positive: false, day: 1, second: 42, microsecond: 123_456 } ); assert_eq!(d.to_string(), "-P1DT42.123456S"); assert_eq!(d.signed_total_seconds(), -86_442); assert_eq!(d.signed_microseconds(), -123_456); } #[test] fn duration_fractions() { let d = Duration::parse_str("P1.123W").unwrap(); assert_eq!( d, Duration { positive: true, day: 7, second: 74390, microsecond: 400_000 } ); } #[test] fn duration_new_normalise() { let d = Duration::new(false, 1, 86500, 1_000_123).unwrap(); assert_eq!( d, Duration { positive: false, day: 2, second: 101, microsecond: 123, } ); } #[test] fn duration_new_normalise2() { let d = Duration::new(true, 0, 0, 1_000_000).unwrap(); assert_eq!( d, Duration { positive: true, day: 0, second: 1, microsecond: 0, } ); } #[test] fn duration_comparison() { let d1 = Duration::new(true, 0, 0, 1_000_000).unwrap(); let d2 = Duration::new(true, 0, 0, 1_000_001).unwrap(); assert!(d1 < d2); assert!(d1 <= d2); assert!(d1 <= d1.clone()); assert!(d2 > d1); assert!(d2 >= d1); assert!(d2 >= d2.clone()); let d3 = Duration::new(true, 3, 0, 0).unwrap(); let d4 = Duration::new(false, 4, 0, 0).unwrap(); assert!(d3 > d4); assert!(d3 >= d4); assert!(d4 < d3); assert!(d4 <= d3); // from docs: `positive` is included in in comparisons, thus `+P1D` is greater than `-P2D` let d5 = Duration::parse_str("+P1D").unwrap(); let d6 = Duration::parse_str("-P2D").unwrap(); assert!(d5 > d6); let d7 = Duration::new(false, 3, 0, 0).unwrap(); let d8 = Duration::new(false, 4, 0, 0).unwrap(); assert!(d7 > d8); assert!(d8 < d7); } #[test] fn duration_new_err() { let d = Duration::new(true, u32::MAX, 4294967295, 905969663); match d { Ok(t) => panic!("unexpectedly valid: {t:?}"), Err(e) => assert_eq!(e, ParseError::DurationValueTooLarge), } let d = Duration::new(true, u32::MAX, 0, 0); match d { Ok(t) => panic!("unexpectedly valid: {t:?}"), Err(e) => assert_eq!(e, ParseError::DurationDaysTooLarge), } } #[test] fn duration_hours() { let d = Duration::parse_str("PT5H45M").unwrap(); assert_eq!( d, Duration { positive: true, day: 0, second: 20700, microsecond: 0 } ); assert_eq!(d.to_string(), "PT5H45M"); assert_eq!(d.signed_total_seconds(), (5 * 60 * 60) + (45 * 60)); assert_eq!(d.signed_microseconds(), 0); } #[test] fn duration_minutes() { let d = Duration::parse_str("PT30M").unwrap(); assert_eq!( d, Duration { positive: true, day: 0, second: 1800, microsecond: 0 } ); assert_eq!(d.to_string(), "PT30M"); assert_eq!(d.signed_total_seconds(), 30 * 60); assert_eq!(d.signed_microseconds(), 0); } param_tests! { Duration, duration_too_short1: err => "", TooShort; duration_too_short2: err => "+", TooShort; duration_too_short3: err => "P", TooShort; duration_too_short4: err => "+PT", TooShort; duration_1y: ok => "P1Y", "P1Y"; duration_123y: ok => "P123Y", "P123Y"; duration_123_8y: ok => "P123.8Y", "P123Y292D"; duration_1m: ok => "P1M", "P30D"; duration_1_5m: ok => "P1.5M", "P45D"; duration_1w: ok => "P1W", "P7D"; duration_1_1w: ok => "P1.1W", "P7DT16H48M"; duration_1_123w: ok => "P1.123W", "P7DT20H39M50.4S"; duration_simple_negative: ok => "-P1Y", "-P1Y"; duration_simple_positive: ok => "+P1Y", "P1Y"; duration_fraction1: ok => "PT0.555555S", "PT0.555555S"; duration_fraction2: ok => "P1Y1DT2H0.5S", "P1Y1DT2H0.5S"; duration_1: ok => "P1DT1S", "P1DT1S"; duration_all: ok => "P1Y2M3DT4H5M6S", "P1Y63DT4H5M6S"; // FIXME: this is current behaviour, but we should probably error on // out-of order durations (not RFC3339 compliant) duration_all_wrong_order: ok => "P3D2M1YT6S5M4H", "P1Y63DT4H5M6S"; // FIXME: this is current behaviour, but we should probably error on // repeated units (not RFC3339 compliant) duration_unit_repeated: ok => "P1Y2Y", "P3Y"; duration: err => "PD", DurationInvalidNumber; duration: err => "P1DT1MT1S", DurationTRepeated; duration: err => "P1DT1.1M1S", DurationInvalidFraction; duration: err => "P1DT1X", DurationInvalidTimeUnit; duration_invalid_day_unit1: err => "P1X", DurationInvalidDateUnit; duration_invalid_day_unit2: err => "P1", DurationInvalidDateUnit; duration_time_42s: ok => "00:00:42", "PT42S"; duration_time_42s_no_leading_0: ok => "0:00:42", "PT42S"; duration_time_1m: ok => "00:01", "PT1M"; duration_time_1m_no_leading_0: ok => "0:01:00", "PT1M"; duration_time_1h_2m_3s: ok => "01:02:03", "PT1H2M3S"; duration_time_1h_2m_3s_no_leading_0: ok => "1:02:03", "PT1H2M3S"; duration_time_fraction: ok => "00:01:03.123", "PT1M3.123S"; duration_time_extra: err => "00:01:03.123x", ExtraCharacters; duration_time_timezone: err => "00:01:03x", ExtraCharacters; duration_time_more_than_24_hour: ok => "24:01:03", "P1DT1M3S"; duration_time_way_more_than_24_hour: ok => "2400000000:01:03", "P273972Y220DT1M3S"; duration_time_way_more_than_24_hour_long_fraction: ok => "2400000000:01:03.654321", "P273972Y220DT1M3.654321S"; duration_time_invalid_over_limit_hour: err => "100000000000:01:03", DurationHourValueTooLarge; duration_time_overflow_hour: err => "100000000000000000000000:01:03", DurationHourValueTooLarge; duration_time_invalid_format_hour: err => "1000xxx000:01:03", InvalidCharHour; duration_time_invalid_format_hour2: err => "1 10:10", InvalidCharHour; duration_time_invalid_minute: err => "00:60:03", OutOfRangeMinute; duration_time_invalid_second: err => "00:00:60", OutOfRangeSecond; duration_time_fraction_too_long: err => "00:00:00.1234567", SecondFractionTooLong; duration_time_fraction_missing: err => "00:00:00.", SecondFractionMissing; duration_days_1day1: ok => "1 day", "P1D"; duration_days_1day2: ok => "1day", "P1D"; duration_days_1day3: ok => "1 day,", "P1D"; duration_days_1day4: ok => "1 day, ", "P1D"; duration_days_1day5: ok => "1days", "P1D"; duration_days_1day6: ok => "1DAYS", "P1D"; duration_days_1day7: ok => "1d", "P1D"; duration_days_1day8: ok => "1d ", "P1D"; duration_days_too_short: err => "x", DurationInvalidNumber; duration_days_invalid1: err => "1x", DurationInvalidDays; duration_days_invalid2: err => "1dx", TooShort; duration_days_invalid3: err => "1da", DurationInvalidDays; duration_days_invalid4: err => "1", DurationInvalidDays; duration_days_invalid5: err => "1 ", DurationInvalidDays; duration_days_invalid6: err => "1 x", DurationInvalidDays; duration_days_neg: ok => "-1 day", "-P1D"; duration_days_pos: ok => "+1 day", "P1D"; duration_days_123days: ok => "123days", "P123D"; duration_days_time: ok => "1 day 00:00:42", "P1DT42S"; duration_days_time_no_leading_0: ok => "1 day 1:00:42", "P1DT1H42S"; duration_days_time_comma_no_leading_0: ok => "1 day, 1:00:42", "P1DT1H42S"; duration_days_time_neg: ok => "-1 day 00:00:42", "-P1DT42S"; duration_exceeds_day: ok => "PT86500S", "P1DT1M40S"; duration_days_time_too_short: err => "1 day 00:", TooShort; duration_days_time_wrong: err => "1 day 00:xx", InvalidCharMinute; duration_days_time_extra: err => "1 day 00:00:00.123 ", ExtraCharacters; duration_days_time_more_than_24_hour: err => "1d 24:01:03", DurationHourValueTooLarge; duration_overflow: err => "18446744073709551616 day 12:00", DurationValueTooLarge; duration_fuzz1: err => "P18446744073709551611DT8031M1M1M1M", DurationValueTooLarge; duration_fuzz2: err => "P18446744073709550PT9970442H6R15D1D", DurationValueTooLarge; } #[test] fn duration_large() { let d = Duration::parse_str("999999999 day 00:00").unwrap(); assert_eq!(d.to_string(), "P2739726Y9D"); let input = format!("{}1 day 00:00", u64::MAX); match Duration::parse_str(&input) { Ok(t) => panic!("unexpectedly valid: {input:?} -> {t:?}"), Err(e) => assert_eq!(e, ParseError::DurationValueTooLarge), } } #[test] fn duration_limit() { let d = Duration::new(true, 999_999_999, 86399, 999_999).unwrap(); assert_eq!(d.to_string(), "P2739726Y9DT23H59M59.999999S"); match Duration::new(true, 999_999_999, 86399, 999_999 + 1) { Ok(t) => panic!("unexpectedly valid -> {t:?}"), Err(e) => assert_eq!(e, ParseError::DurationDaysTooLarge), } let d = Duration::new(false, 999_999_999, 86399, 999_999).unwrap(); assert_eq!(d.to_string(), "-P2739726Y9DT23H59M59.999999S"); match Duration::new(false, 999_999_999, 86399, 999_999 + 1) { Ok(t) => panic!("unexpectedly valid -> {t:?}"), Err(e) => assert_eq!(e, ParseError::DurationDaysTooLarge), } } macro_rules! int_ok_tests { ($($name:ident: $input:literal, $expected:expr;)*) => { $( paste::item! { #[test] fn [< parse_int_ok_ $name >]() { let v = int_parse_str($input).unwrap(); assert_eq!(v, $expected); } } )* } } int_ok_tests! { zero: "0", 0; one: "1", 1; small: "123", 123; small_neg: "-123", -123; small_plus: "+123", 123; large: "1585201087123789", 1585201087123789; // big_neg: "-09223372036854775808", -09223372036854775808; } macro_rules! int_err_tests { ($($name:ident: $input:literal;)*) => { $( paste::item! { #[test] fn [< parse_int_err_ $name >]() { assert!(int_parse_str($input).is_none()) } } )* } } int_err_tests! { text: "xxx"; double_neg: "--1"; empty: ""; too_big: "092233720368547758089"; too_big_neg: "-092233720368547758089"; } macro_rules! float_ok_tests { ($($name:ident: $input:literal, $expected:expr;)*) => { $( paste::item! { #[test] fn [< parse_float_ok_ $name >]() { let v = format!("{:?}", float_parse_str($input)); assert_eq!(v, $expected); } } )* } } float_ok_tests! { zero: "0", "Int(0)"; one: "1", "Int(1)"; neg_one: "-1", "Int(-1)"; one_point: "1.", "Float(1.0)"; decimal: "1.5", "Float(1.5)"; neg_decimal: "-1.5", "Float(-1.5)"; decimal_zero: "1.0", "Float(1.0)"; big_int: "1585201087123789", "Int(1585201087123789)"; big_int_ones: "1111111111111111", "Int(1111111111111111)"; big_float: "111111111.11111", "Float(111111111.11111)"; } macro_rules! float_err_tests { ($($name:ident: $input:literal;)*) => { $( paste::item! { #[test] fn [< parse_float_err_ $name >]() { assert!(float_parse_str($input).is_err()) } } )* } } float_err_tests! { text: "xxx"; double_neg: "--1"; text_in_fraction: "123.XX"; empty: ""; i64_minus_1: "9223372036854775809"; too_big: "092233720368547758089"; too_big_neg: "-092233720368547758089"; too_big_neg2: "9223372036854775808"; error_in_decimal: "123.XX"; error_in_decimal_spaces: "123.12 "; } #[test] fn test_time_parse_truncate_seconds() { let time = Time::parse_bytes_with_config( "12:13:12.123456789".as_bytes(), &(TimeConfigBuilder::new() .microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate) .build()), ) .unwrap(); assert_eq!(time.to_string(), "12:13:12.123456"); } #[test] fn test_datetime_parse_truncate_seconds() { let time = DateTime::parse_bytes_with_config( "2020-01-01T12:13:12.123456789".as_bytes(), &(TimeConfigBuilder::new() .microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate) .build()), ) .unwrap(); assert_eq!(time.to_string(), "2020-01-01T12:13:12.123456"); } #[test] fn test_duration_parse_truncate_seconds() { let time = Duration::parse_bytes_with_config( "00:00:00.1234567".as_bytes(), &(TimeConfigBuilder::new() .microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate) .build()), ) .unwrap(); assert_eq!(time.to_string(), "PT0.123456S"); } #[test] fn test_time_parse_bytes_does_not_add_offset_for_rfc3339() { let time = Time::parse_bytes_with_config( "12:13:12".as_bytes(), &(TimeConfigBuilder::new().unix_timestamp_offset(Some(0)).build()), ) .unwrap(); assert_eq!(time.to_string(), "12:13:12"); } #[test] fn test_datetime_parse_bytes_does_not_add_offset_for_rfc3339() { let time = DateTime::parse_bytes_with_config( "2020-01-01T12:13:12".as_bytes(), &(TimeConfigBuilder::new().unix_timestamp_offset(Some(0)).build()), ) .unwrap(); assert_eq!(time.to_string(), "2020-01-01T12:13:12"); } #[test] fn test_datetime_parse_unix_timestamp_from_bytes_with_utc_offset() { let time = DateTime::parse_bytes_with_config( "1689102037.5586429".as_bytes(), &(TimeConfigBuilder::new() .unix_timestamp_offset(Some(0)) .microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate) .build()), ) .unwrap(); assert_eq!(time.to_string(), "2023-07-11T19:00:37.558643Z"); } #[test] fn test_datetime_parse_unix_timestamp_from_bytes_as_naive() { let time = DateTime::parse_bytes_with_config( "1689102037.5586429".as_bytes(), &(TimeConfigBuilder::new() .unix_timestamp_offset(None) .microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate) .build()), ) .unwrap(); assert_eq!(time.to_string(), "2023-07-11T19:00:37.558643"); } #[test] fn test_time_parse_unix_timestamp_from_bytes_with_utc_offset() { let time = Time::from_timestamp_with_config(1, 2, &(TimeConfigBuilder::new().unix_timestamp_offset(Some(0)).build())) .unwrap(); assert_eq!(time.to_string(), "00:00:01.000002Z"); } #[test] fn test_time_parse_unix_timestamp_from_bytes_as_naive() { let time = Time::from_timestamp_with_config(1, 2, &(TimeConfigBuilder::new().unix_timestamp_offset(None).build())) .unwrap(); assert_eq!(time.to_string(), "00:00:01.000002"); } #[test] fn test_time_config_builder() { assert_eq!( TimeConfigBuilder::new().build(), TimeConfig { microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior::Error, unix_timestamp_offset: None, } ); assert_eq!(TimeConfigBuilder::new().build(), TimeConfig::builder().build()); } #[test] fn date_dash_err() { let error = Date::parse_str("-").unwrap_err(); assert_eq!(error, ParseError::TooShort); assert_eq!(error.to_string(), "too_short"); assert_eq!(error.get_documentation(), Some("input is too short")); } #[test] fn number_dash_err() { assert!(int_parse_str("-").is_none()); assert!(int_parse_str("+").is_none()); assert!(int_parse_bytes(b"-").is_none()); assert!(int_parse_bytes(b"+").is_none()); assert!(matches!(float_parse_str("-"), IntFloat::Err)); assert!(matches!(float_parse_str("+"), IntFloat::Err)); assert!(matches!(float_parse_bytes(b"-"), IntFloat::Err)); assert!(matches!(float_parse_bytes(b"+"), IntFloat::Err)); } speedate-0.15.0/tests/values_err.txt000064400000000000000000000034251046102023000155570ustar 00000000000000# from https://github.com/python/cpython/blob/5849af7a80166e9e82040e082f22772bd7cf3061/Lib/test/datetimetester.py#L3104-L3226 2025-01-02 2025-01-02T03 2025-01-02T0304 2025-01-02T030405 2025-01-02T030405.678901 2025-01-02T030405,678901 2025-01-02T03:04:05.6789010 2009-04-19T03:15:45.1234567 20250102 20250102T03 20250102T03:04 20250102T03:04:05 20250102T030405 20250102T03:04:05.6 20250102T03:04:05,6 20250102T03:04:05.678 20250102T03:04:05,678 20250102T03:04:05.678901 20250102T030405.678901 20250102T030405,678901 20250102T030405.6789010 2022W52520 2022W527520 2026W01516 2026W013516 2025W01503 2025W014503 2025W01512 2025W014512 2025W014T121431 2026W013T162100 2026W013 162100 2022W527T202159 2022W527 202159 2025W014 121431 2025W014T030405 2025W014 030405 2020-W53-6T03:04:05 2020W537 03:04:05 2025-W01-4T03:04:05 2025-W01-4T03:04:05.678901 2025-W01-4T12:14:31 2025-W01-4T12:14:31.012345 2026-W01-3T16:21:00 2026-W01-3T16:21:00.000000 2022-W52-7T20:21:59 2022-W52-7T20:21:59.999999 2025-W01003+00 2025-01-02T03:04:05+00 2025-01-02003:04:05,6+00:00:00.00 2000-01-01T00+21 2025-01-02T03:05:06+03 2025-01-02T03:05:06-03 2025-01-02T03:04:05,6+000000.00 # https://github.com/python/cpython/blob/5849af7a80166e9e82040e082f22772bd7cf3061/Lib/test/datetimetester.py#L3237-L3261 2009.04-19T03 2009-04.19T03 2009-04-19T0a 2009-04-19T03:1a:45 2009-04-19T03:15:4a 2009-04-19T03;15:45 2009-04-19T03:15;45 2009-04-19T03:15:4500:00 2009-04-19T03:15:45.123456+24:30 2009-04-19T03:15:45.123456-24:30 2009-04-10ᛇᛇᛇᛇᛇ12:15 2009-04\ud80010T12:15 2009-04-10T12\ud80015 2009-04-19T1 2009-04-19T12:3 2009-04-19T12:30:4 2009-04-19T12: 2009-04-19T12:30: 2009-04-19T12:30:45. 2009-04-19T12:30:45.123456+ 2009-04-19T12:30:45.123456- 2009-04-19T12:30:45.123456-05:00a 2009-04-19T12:30:45.123-05:00a 2009-04-19T12:30:45-05:00a speedate-0.15.0/tests/values_ok.txt000064400000000000000000000073061046102023000154020ustar 00000000000000# RFC 3339 formats from https://ijmacd.github.io/rfc3339-iso8601/ date: 2022-05-30 -> 2022-05-30 # times with timezones are removed since they're not supported by this library time: 20:26:52 -> 20:26:52 time: 20:26:52.3 -> 20:26:52.300000 time: 20:26:52.36 -> 20:26:52.360000 time: 20:26:52.361 -> 20:26:52.361000 time: 20:26:52.361000 -> 20:26:52.361000 dt: 2022-05-30T20:26:52Z -> 2022-05-30T20:26:52Z dt: 2022-05-30T20:26:52.3Z -> 2022-05-30T20:26:52.300000Z dt: 2022-05-30T20:26:52.36Z -> 2022-05-30T20:26:52.360000Z dt: 2022-05-30T20:26:52.361Z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30T20:26:52.361000Z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30T21:26:52+01:00 -> 2022-05-30T21:26:52+01:00 dt: 2022-05-30T21:26:52.361+01:00 -> 2022-05-30T21:26:52.361000+01:00 dt: 2022-05-30T21:26:52.361000+01:00 -> 2022-05-30T21:26:52.361000+01:00 dt: 2022-05-30 21:26:52+01:00 -> 2022-05-30T21:26:52+01:00 dt: 2022-05-30 21:26:52.3+01:00 -> 2022-05-30T21:26:52.300000+01:00 dt: 2022-05-30 21:26:52.36+01:00 -> 2022-05-30T21:26:52.360000+01:00 dt: 2022-05-30 21:26:52.361+01:00 -> 2022-05-30T21:26:52.361000+01:00 dt: 2022-05-30 21:26:52.361000+01:00 -> 2022-05-30T21:26:52.361000+01:00 dt: 2022-05-30 20:26:52Z -> 2022-05-30T20:26:52Z dt: 2022-05-30_20:26:52Z -> 2022-05-30T20:26:52Z dt: 2022-05-30 20:26:52z -> 2022-05-30T20:26:52Z dt: 2022-05-30_20:26:52z -> 2022-05-30T20:26:52Z dt: 2022-05-30 20:26:52.3Z -> 2022-05-30T20:26:52.300000Z dt: 2022-05-30 20:26:52.36Z -> 2022-05-30T20:26:52.360000Z dt: 2022-05-30 20:26:52.361Z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30_20:26:52.361Z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30 20:26:52.361000Z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30_20:26:52.361000Z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30 20:26:52.361z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30_20:26:52.361z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30 20:26:52.361000z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30_20:26:52.361000z -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30 20:26:52-00:00 -> 2022-05-30T20:26:52Z dt: 2022-05-30 20:26:52.361-00:00 -> 2022-05-30T20:26:52.361000Z dt: 2022-05-30T20:26:52-00:00 -> 2022-05-30T20:26:52Z dt: 2022-05-30T20:26:52.361-00:00 -> 2022-05-30T20:26:52.361000Z dt: 2022-05-31T05:11:52+08:45 -> 2022-05-31T05:11:52+08:45 dt: 2022-05-30T20:26:52+00:00 -> 2022-05-30T20:26:52Z dt: 2022-05-30T20:26:52.361+00:00 -> 2022-05-30T20:26:52.361000Z # from https://github.com/python/cpython/blob/5849af7a80166e9e82040e082f22772bd7cf3061/Lib/test/datetimetester.py#L3104-L3226 # with invalid cases moved to values_err.txt dt: 2025-01-02T03:04 -> 2025-01-02T03:04:00 dt: 2025-01-02T03:04:05 -> 2025-01-02T03:04:05 dt: 2025-01-02T03:04:05.6 -> 2025-01-02T03:04:05.600000 dt: 2025-01-02T03:04:05,6 -> 2025-01-02T03:04:05.600000 dt: 2025-01-02T03:04:05.678 -> 2025-01-02T03:04:05.678000 dt: 2025-01-02T03:04:05.678901 -> 2025-01-02T03:04:05.678901 dt: 2025-01-02T03:04:05,678901 -> 2025-01-02T03:04:05.678901 dt: 2009-04-19T03:15:45.2345 -> 2009-04-19T03:15:45.234500 dt: 2025-01-02T03:04:05,678 -> 2025-01-02T03:04:05.678000 dt: 2025-01-02T03:04:05Z -> 2025-01-02T03:04:05Z dt: 2025-01-02T03:05:06+0300 -> 2025-01-02T03:05:06+03:00 dt: 2025-01-02T03:05:06-0300 -> 2025-01-02T03:05:06-03:00 dt: 2025-01-02T03:04:05+0000 -> 2025-01-02T03:04:05Z dt: 2020-01-01T03:05:07.123457-05:00 -> 2020-01-01T03:05:07.123457-05:00 dt: 2020-01-01T03:05:07.123457-0500 -> 2020-01-01T03:05:07.123457-05:00 dt: 2020-06-01T04:05:06.111111-04:00 -> 2020-06-01T04:05:06.111111-04:00 dt: 2020-06-01T04:05:06.111111-0400 -> 2020-06-01T04:05:06.111111-04:00 dt: 2021-10-31T01:30:00.000000+01:00 -> 2021-10-31T01:30:00+01:00 dt: 2021-10-31T01:30:00.000000+0100 -> 2021-10-31T01:30:00+01:00 dt: 2025-01-02T03:04:05,678+00:00 -> 2025-01-02T03:04:05.678000Z