fundu-1.0.0/.bumpversion.cfg000064400000000000000000000003721046102023000140550ustar 00000000000000[bumpversion] current_version = 1.0.0 commit = True message = "Bump version v{current_version} -> v{new_version}" [bumpversion:file:Cargo.toml] search = version = "{current_version}" replace = version = "{new_version}" [bumpversion:file:README.md] fundu-1.0.0/.cargo_vcs_info.json0000644000000001360000000000100121530ustar { "git": { "sha1": "417597be6cc80c542c2244d2c6e2c05d305e78d1" }, "path_in_vcs": "" }fundu-1.0.0/.clippy.toml000064400000000000000000000000201046102023000132060ustar 00000000000000msrv = "1.61.0" fundu-1.0.0/.editorconfig000064400000000000000000000004111046102023000134140ustar 00000000000000root=true [*] end_of_line = LF charset = utf-8 indent_style=space indent_size=2 max_line_length = 80 insert_final_newline = true trim_trailing_whitespace = true [*.md] indent_size = 4 trim_trailing_whitespace = false [*.rs] indent_size = 4 max_line_length = 100 fundu-1.0.0/.github/dependabot.yml000064400000000000000000000007701046102023000151370ustar 00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" fundu-1.0.0/.github/workflows/cicd.yml000064400000000000000000000327171046102023000157770ustar 00000000000000name: Build and Check # TODO: Remove negative feature on: push: branches: ["main", "cicd", "release", "develop"] pull_request: branches: ["main"] workflow_dispatch: inputs: resetBenchmarks: description: "Reset the benchmark data" required: true type: boolean concurrency: group: ${{ github.ref }} cancel-in-progress: true env: CARGO_TERM_COLOR: always RUST_BACKTRACE: "1" BENCHMARK_REGRESSION_PERCENT_FAIL: 10 FEATURES: standard,custom,time,chrono,serde jobs: deny: name: Check dependencies/ubuntu-latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: EmbarkStudios/cargo-deny-action@v1 with: rust-version: "1.61.0" format: name: Check format with nightly rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt - uses: Swatinem/rust-cache@v2 - run: cargo fmt --check base: needs: [format] name: Build, check and test strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] toolchain: - "1.61.0" # MSRV - stable - nightly include: - components: clippy toolchain: stable - features: standard,custom,time,chrono,serde,with-iai,with-flamegraph - features: standard,custom,time,chrono,serde os: windows-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} components: ${{ matrix.components }} - name: Prepare if: matrix.toolchain != 'stable' run: | rustup toolchain install stable --no-self-update --component clippy - uses: Swatinem/rust-cache@v2 with: key: "${{ matrix.os }}_${{ matrix.toolchain }}" - name: Info if unix if: ${{ matrix.os != 'windows-latest' }} run: | set -x uname -a pwd rustup --version rustup show rustup component list --installed - name: Info if windows if: ${{ matrix.os == 'windows-latest' }} shell: bash run: | set -x rustup show rustup component list --installed - name: Build run: cargo build --features ${{ matrix.features }} - name: Lint run: cargo +stable clippy --features ${{ matrix.features }} --all-targets -- -D warnings - name: Test run: cargo test --features ${{ matrix.features }} cross: needs: [format] name: Cross build and test strategy: fail-fast: false matrix: target: ##### big endian targets ##### - s390x-unknown-linux-gnu # - sparc64-unknown-linux-gnu # - powerpc-unknown-linux-gnu - mips-unknown-linux-gnu ##### little endian targets ##### - i686-unknown-linux-gnu - i586-unknown-linux-gnu - aarch64-unknown-linux-gnu - arm-unknown-linux-gnueabihf # - riscv64gc-unknown-linux-gnu # dependency errors: quick-xml # - wasm32-unknown-emscripten # dependency errors: criterion # - x86_64-linux-android - x86_64-unknown-linux-musl runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: toolchain: "1.61.0" - uses: Swatinem/rust-cache@v2 with: key: "ubuntu-latest_1.61.0_${{ matrix.target }}" - uses: taiki-e/install-action@cross - name: Configure cross run: | cat < target cache-on-failure: true - name: Prepare run: sudo apt-get -y update && sudo apt-get -y install llvm - name: Install cargo-fuzz run: cargo install cargo-fuzz - name: Run fuzzing for 5 minutes with dictionary if: matrix.dict run: | cargo fuzz run --all-features ${{ matrix.test }} -- \ -dict=dicts/${{ matrix.dict }} \ -max_total_time=300 \ -print_final_stats=1 \ -print_corpus_stats=1 \ -verbosity=2 - name: Run fuzzing for 5 minutes without dictionary if: matrix.dict == 0 run: | cargo fuzz run --all-features ${{ matrix.test }} -- \ -max_total_time=300 \ -print_final_stats=1 \ -print_corpus_stats=1 \ -verbosity=2 env: RUSTFLAGS: "-C instrument-coverage" LLVM_PROFILE_FILE: "fundu_fuzzy_coverage-%p-%m.profraw" benchmarks: needs: [base, cross] name: Benchmarks/ubuntu-latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: toolchain: "1.61.0" - uses: Swatinem/rust-cache@v2 - name: Install iai-callgrind-runner run: | version=$(cargo metadata --format-version=1 |\ jq '.packages[] | select(.name == "iai-callgrind").version' |\ tr -d '"' ) cargo install iai-callgrind-runner --version $version - name: Prepare run: | sudo apt-get -y update && sudo apt-get -y install valgrind - name: Download reference benchmarks if: ${{ !inputs.resetBenchmarks }} uses: dawidd6/action-download-artifact@v2 with: workflow_conclusion: success name: iai-callgrind-benchmarks check_artifacts: true path: target/iai if_no_artifact_found: warn - name: Run iai-callgrind standard feature benchmarks run: | cargo bench --features standard,with-iai \ --bench iai_bench_standard \ --bench iai_bench_parsing \ --bench iai_bench_parsing_time_units \ --bench iai_bench_reference | tee bench.out - name: Run iai-callgrind custom feature benchmarks run: | cargo bench --features custom,with-iai --no-default-features \ --bench iai_bench_custom | tee -a bench.out - name: Run iai-callgrind time chrono feature benchmarks run: | cargo bench --features time,chrono,with-iai \ --bench iai_bench_parsing_negative | tee -a bench.out - name: Strip colors from output file bench.out run: | # remove colors (from https://unix.stackexchange.com/a/111936 by user55518) sed -Ei 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g' bench.out - name: Check for regression run: | echo 0 > fail echo 0 > skip while IFS= read -r line; do skip=$(( $(< skip) > 0 )) if grep -Eq '^\S*reference$' --color=never <<<"$line"; then echo 6 > skip else echo $(( $(< skip) - 1 )) > skip fi if [[ $skip -eq 0 ]] && grep 'Estimated Cycles:' --color=never <<<"$line"; then p=$(sed -En 's/\s*Estimated Cycles:.*\([+]([0-9]*)([.][0-9]*)?%\)/\1/p' <<<"$line") if [[ $p -ge ${BENCHMARK_REGRESSION_PERCENT_FAIL} ]]; then echo "::error::Regressed +${p}% >= BENCHMARK_REGRESSION_PERCENT_FAIL (=${BENCHMARK_REGRESSION_PERCENT_FAIL}%)" echo 2 > fail fi else echo "$line" fi done < bench.out exit $(< fail) - uses: actions/upload-artifact@v3 with: name: iai-callgrind-benchmarks path: target/iai miri: needs: [base, cross] name: Tests/Miri runs-on: ubuntu-latest strategy: fail-fast: false matrix: target: - x86_64-unknown-linux-gnu - i686-unknown-linux-gnu # 32-bit - mips64-unknown-linux-gnuabi64 # big-endian - aarch64-unknown-linux-gnu - arm-unknown-linux-gnueabihf steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@nightly with: components: miri - uses: Swatinem/rust-cache@v2 with: key: "miri_${{ matrix.target }}" cache-directories: /home/runner/.cache/miri - uses: taiki-e/install-action@cross if: matrix.target != 'x86_64-unknown-linux-gnu' - name: Info run: | set -x pwd rustup --version rustup show rustup component list --installed cargo --list - name: Setup cross/miri run: | cargo miri setup case ${{ matrix.target }} in x86_64-unknown-linux-gnu ) ;; * ) cat < This software is released under the MIT License. https://opensource.org/licenses/MIT --> # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [1.0.0] - 2023-05-29 If upgrading from an previous version, there are some breaking changes, most notably parsing negative durations has been completely revised. The `negative` feature was removed and the parsers now return fundu's own `Duration`, which can represent negative `Durations`, instead of a `std::time::Duration` (or `time::Duration`). ## Added * A new configuration option `allow_negative` to enable parsing negative `Durations` without parsing error * `chrono` feature to provide methods to convert from fundu's `Duration` to `chrono::Duration` and vice versa * `time` feature to provide methods to convert from fundu's `Duration` to `time::Duration` and vice versa * `serde` feature to be able to serialize, deserialize the some important structs and enums with `serde`: `ParseError`, `TryFromDurationError`, `Multiplier`, `Duration` * New trait `SaturatingInto` which provides a method to convert from fundu's `Duration` into another `Duration` saturating at the maximum or minimum of the other `Duration` instead of returning a `TryFromDurationError`. * A new error type `TryFromDurationError` which is returned when the conversion from fundu's `Duration` to `std::time::Duration`, `time::Duration` or `chrono::Duration` fails. * Add a new struct `TimeKeyword` to the `custom` feature. `TimeKeywords` (like `yesterday`) are similar to `CustomTimeUnits` but don't accept a number in front of them in the input string. * The `CustomDurationParser` and `CustomDurationParserBuilder` have two new methods `keyword` and `keywords` which store `TimeKeyword`(s) in the parser. * A new configuration option `allow_ago` was added to enable parsing the `ago` keyword in the input string after a duration to negate the previously parsed duration * `CustomTimeUnit::with_default`: This method creates a `CustomTimeUnit` with the default multiplier of `Multiplier(1, 0)` * Implementation of the standard trait `Hash` for `CustomTimeUnit`, `TimeUnit` and `Multiplier` * New methods for `Multiplier`: `is_negative`, `is_positive`, `checked_mul`, `saturating_neg` and the getters `coefficient` for the first component and `exponent` for the second ## Changed * Fundu's thus far internally used `Duration` is now public and implements most standard arithmetic traits like `Add`, `Sub` etc. It can also be converted to `std::time::Duration` and if the feature is activated `time::Duration`, `chrono::Duration` * BREAKING: The `DurationParser::parse` and `CustomDurationParser::parse` methods now return fundu's own `Duration` instead of `std::time::Duration` * BREAKING: The `Multiplier` changed from (u64, i16) to (i64, i16) to allow negative `Multipliers` * BREAKING: The `parse_multiple` configuration option now optionally allows conjunction words (like `and`) between durations in the input string in addition to a `Delimiter` * BREAKING: The `DurationParserBuilder` can now build the `DurationParser` in `const` context at compile time. To be able to do so, the methods signatures of the `DurationParserBuilder` changed from `(&mut self) -> &mut Self` to `(mut self) -> Self` * BREAKING: Rename `DurationParserBuilder::custom_time_units` -> `DurationParserBuilder::time_units` * BREAKING: The `SYSTEMD_TIME_UNITS`, `DEFAULT_TIME_UNITS` and `DEFAULT_ALL_TIME_UNITS` constants from the `custom` feature now use `CustomTimeUnit`s instead of a tuple. * BREAKING: Remove `CustomDurationParserBuilder::time_unit` and rename `CustomDurationParserBuilder::custom_time_unit` -> `CustomDurationParserBuilder::time_unit` * BREAKING: Remove `CustomDurationParserBuilder::time_units` and rename `CustomDurationParserBuilder::custom_time_units` -> `CustomDurationParserBuilder::time_units` * BREAKING: `CustomDurationParser::with_time_units` now uses `CustomTimeUnits` instead of the `Identifier` type * BREAKING: Rename `CustomDurationParser::custom_time_unit` -> `CustomDurationParser::time_unit` * BREAKING: Rename `CustomDurationParser::custom_time_units` -> `CustomDurationParser::time_units` * BREAKING: If the setting `number_is_optional` is enabled the exponent must have a mantissa. The exponent is now a part of the number * Panic in `CustomTimeUnit::new` when creating a `CustomTimeUnit` with a `Multiplier` and `TimeUnit`. A multiplication of the additional `Multiplier` and the inherent multiplier of the `TimeUnit` would otherwise overflow (and panic) during the parsing * Refactorings of the internal parser improve the parsing speed for all input sizes ## Removed * BREAKING: The `negative` feature and `DurationParser::parse_negative`, `CustomDurationParser::parse_negative` * BREAKING: The `Identifier` type is redundant and was removed ## Fixed * Parsing with the configuration option `allow_delimiter` enabled changed, so that an input ending with a `Delimiter` is an error now * The exponent must always consist of at least one digit * Input starting with a delimiter should result in a ParseError similar to input ending with a delimiter ## [0.5.1] - 2023-05-01 This version introduces a slight performance regression in favour of the new `parse_multiple` method. ### Added * New method `parse_multiple` to parse multiple durations in the source string at once. * New method `disable_infinity` to disable parsing `inf` or `infinity`. * The new methods above make it possible to build a systemd time span parser as defined by systemd * An example for a fully functional systemd time span parser ### Changed * Parsing `infinity` values was improved, so that time unit identifiers can now start with an `i` or `in`. * Running the benchmarks was improved, so that `iai` (feature = `with-iai`) and `flamegraph` (feature = `with-flamegraph`) benchmarks need to be activated via their features. * Make `TimeUnit::multiplier` method public * Make `custom::Identifiers` type public ### Fixed * In the `parser` module an invocation of `ParseError::TimeUnit` error was pointing to a wrong position in the source string. * Use a workaround on s390x systems because parsing negative durations produced wrong results when using `time::Duration::MIN.saturating_add`. The source of this bug is maybe the `time` crate or `rust` itself. ## [0.5.0] - 2023-03-29 ### Added * `negative` feature: parse negative durations to a duration from the `time` crate * The number format is now customizable: * allow one ore more delimiter between the number and the time unit by setting a `Delimiter` * disable parsing an exponent * disable parsing a fraction * make numbers in front of time units and exponents optional * `DurationParser::builder` method and `DurationParserBuilder` * `CustomDurationParser` allows now creating completely new `CustomTimeUnit`s * `CustomDurationParser::builder` method and `CustomDurationParserBuilder` * An example for the `custom` feature ### Changed * The minimum supported rust version changed from `1.60.0` to `1.61.0` * Some internal changes and refactorings improved the overall performance * Improve and enhance the `README` and public api documentation ### Removed * `DurationParser::time_unit` and `DurationParser::time_units`: Use `DurationParser::with_time_units` or the `DurationParserBuilder` instead * `CustomDurationParser::get_current_time_units` ## [0.4.3] - 2023-03-21 ### Changed * Change from `iai` to `iai-callgrind` and use the new features to improve the iai benchmarks ### Fixed * Building the docs on docs.rs should use all features ## [0.4.2] - 2023-03-07 ### Changed * Internal changes to improve the overall performance but especially for large inputs. Parsing large inputs is now faster than the reference function. ## [0.4.1] - 2023-02-26 ### Changed * The exponent maximum/minium changed to i16 bounds. * Make most initialization methods of `DurationParser` applicable in `const` context * Some internal refactors improved the initialization and parsing speeds ### Fixed * Apply cfg attributes in builder mod.rs. This fixes the warnings when not all features were given. ## [0.4.0] - 2023-02-24 ### Added * The `custom` feature adds fully customizable identifiers for time units in a new struct `CustomDurationParser` * Add `cachegrind` based benchmarks in the github `CI` ### Changed * Organize project into features: `standard` (default) and `custom` * Include more error types in `ParseError` and make `ParseError` non exhaustive * Improve performance of time unit parsing * Improve and add more benchmarks ### Fixed * The `DurationParser::parse` method used `self` mutable although this was not necessary ## [0.3.0] - 2023-02-11 ### Added * New builder method `DurationParser::default_unit` to set the default time unit to something different than seconds. * New method `DurationParser::get_time_units` which returns the current set of time units. * More runnable examples/recipes in the `examples` folder. ### Changed * Improve the api and crate level documentation * Update the README and api documentation to better document the handling of values close to zero no matter the sign. ### Removed * Remove `TimeUnit::multiplier` from the public api. It's still used internally. ### Fixed * Parsing the exponent if there is no number must return a `ParseError` * Negative values exceeding `u64::MAX` returned `Duration::MAX`. Now a `ParseError` is returned. ## [0.2.2] - 2023-02-06 * Fix panic in parser when the whole and fraction part of the input number are missing but a dot is given ## [0.2.1] - 2023-02-05 ### Added * New method `TimeUnits::get_time_units` which returns all currently active time units * `PartialOrd, Ord` traits implementation for `TimeUnit` ### Changed * Improve overall performance by 10-20% * Improve performance of parsing with time units by a factor of ~15 (before ~800ns -> after ~55ns) * tests: Improve test coverage. Reach full coverage. * tests: Increase accuracy of benchmarks * CI: Use `grcov` for coverage report creation with the more accurate source-based coverage ## [0.2.0] - 2023-02-03 ### Added * More tests * Add a simple example in examples directory ### Changed * Updated the README, library documentation and provide more examples * Rename `DurationParser::with_no_time_units` -> `DurationParser::without_time_units` * Change to Julian year calculation: 1 year = 365.25 days and 1 month = year / 12 * Refactor test structure * Make the default ids of time units available ### Removed * Unneeded constants `SECONDS_MAX` and `NANOS_MAX` ### Fixed * Fix a possible overflow when multiplying attos with the time unit multiplier * Export `error::ParseError` and make this enum public ## [0.1.0] - 2023-02-01 ### Added * Basic working implementation of the duration parser * Tests and benchmarks * A README * Basic api documentation * This Changelog * CI setup with github actions fundu-1.0.0/Cargo.lock0000644000001162360000000000100101370ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "arrayvec" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytemuck" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdbc37d37da9e5bce8173f3a41b71d9bf3c674deebbaceacd0ebdabde76efb03" dependencies = [ "android-tzdata", "num-traits", ] [[package]] name = "ciborium" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" [[package]] name = "ciborium-ll" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", "once_cell", "strsim", "termcolor", "textwrap", ] [[package]] name = "clap_derive" version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "clap_lex" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] [[package]] name = "cpp_demangle" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c76f98bdfc7f66172e6c7065f981ebb576ffc903fe4c0561d9f0c2509226dc6" dependencies = [ "cfg-if", ] [[package]] name = "criterion" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" dependencies = [ "anes", "atty", "cast", "ciborium", "clap", "criterion-plot", "itertools", "lazy_static", "num-traits", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-channel" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] [[package]] name = "dashmap" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if", "hashbrown", "lock_api", "once_cell", "parking_lot_core", ] [[package]] name = "debugid" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ "uuid", ] [[package]] name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "env_logger" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "log", ] [[package]] name = "errno" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", "windows-sys 0.48.0", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "findshlibs" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" dependencies = [ "cc", "lazy_static", "libc", "winapi", ] [[package]] name = "fundu" version = "1.0.0" dependencies = [ "chrono", "clap", "criterion", "iai-callgrind", "inferno", "pprof", "rstest", "rstest_reuse", "serde", "serde_test", "time", ] [[package]] name = "futures" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", "syn 2.0.18", ] [[package]] name = "futures-sink" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-timer" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" [[package]] name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "iai-callgrind" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76579762a383d5da364779d780704f93898c620703c27087b766700a87e5a8c2" [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "inferno" version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6e66fa9bb3c52f40d05c11b78919ff2f18993c2305bd8a62556d20cb3e9606f" dependencies = [ "ahash", "atty", "clap", "crossbeam-channel", "crossbeam-utils", "dashmap", "env_logger", "indexmap", "itoa", "log", "num-format", "num_cpus", "once_cell", "quick-xml", "rgb", "str_stack", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", "windows-sys 0.48.0", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] [[package]] name = "memoffset" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags", "cfg-if", "libc", "static_assertions", ] [[package]] name = "num-format" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" dependencies = [ "arrayvec", "itoa", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ "hermit-abi 0.2.6", "libc", ] [[package]] name = "num_threads" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "object" version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" [[package]] name = "oorandom" version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "os_str_bytes" version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "plotters" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" [[package]] name = "plotters-svg" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" dependencies = [ "plotters-backend", ] [[package]] name = "pprof" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "196ded5d4be535690899a4631cc9f18cdc41b7ebf24a79400f46f48e49a11059" dependencies = [ "backtrace", "cfg-if", "criterion", "findshlibs", "inferno", "libc", "log", "nix", "once_cell", "parking_lot", "smallvec", "symbolic-demangle", "tempfile", "thiserror", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", "syn 1.0.109", "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", "version_check", ] [[package]] name = "proc-macro2" version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" dependencies = [ "memchr", ] [[package]] name = "quote" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rayon" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", "num_cpus", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "rgb" version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" dependencies = [ "bytemuck", ] [[package]] name = "rstest" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" dependencies = [ "futures", "futures-timer", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" dependencies = [ "cfg-if", "proc-macro2", "quote", "rustc_version", "syn 1.0.109", "unicode-ident", ] [[package]] name = "rstest_reuse" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45f80dcc84beab3a327bbe161f77db25f336a1452428176787c8c79ac79d7073" dependencies = [ "quote", "rand", "rustc_version", "syn 1.0.109", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.48.0", ] [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", "syn 2.0.18", ] [[package]] name = "serde_json" version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_test" version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "100168a8017b89fd4bcbeb8d857d95a8cfcbde829a7147c09cc82d3ab8d8cb41" dependencies = [ "serde", ] [[package]] name = "slab" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "str_stack" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "symbolic-common" version = "10.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b55cdc318ede251d0957f07afe5fed912119b8c1bc5a7804151826db999e737" dependencies = [ "debugid", "memmap2", "stable_deref_trait", "uuid", ] [[package]] name = "symbolic-demangle" version = "10.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79be897be8a483a81fff6a3a4e195b4ac838ef73ca42d348b3f722da9902e489" dependencies = [ "cpp_demangle", "rustc-demangle", "symbolic-common", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix", "windows-sys 0.45.0", ] [[package]] name = "termcolor" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", "syn 2.0.18", ] [[package]] name = "time" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" dependencies = [ "libc", "num_threads", "time-core", ] [[package]] name = "time-core" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "uuid" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "web-sys" version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.0", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", "windows_x86_64_gnullvm 0.48.0", "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" fundu-1.0.0/Cargo.toml0000644000000073760000000000100101660ustar # 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" rust-version = "1.61.0" name = "fundu" version = "1.0.0" authors = ["Joining7943 "] description = "Configurable, precise and fast rust string parser to a Duration" homepage = "https://github.com/Joining7943/fundu" readme = "README.md" keywords = [ "fundu", "parse", "string", "duration", "time", ] categories = [ "command-line-interface", "parsing", "date-and-time", ] license = "MIT" repository = "https://github.com/Joining7943/fundu" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [profile.flamegraph] opt-level = 1 debug = true inherits = "bench" [lib] bench = false [[example]] name = "simple" test = true [[example]] name = "clap_derive" test = true [[example]] name = "clap_builder" test = true [[example]] name = "convert" test = false required-features = [ "chrono", "time", ] [[example]] name = "custom" test = false required-features = ["custom"] [[example]] name = "systemd" test = false required-features = ["custom"] [[example]] name = "gnu" test = false required-features = ["custom"] [[bench]] name = "benchmarks_custom" harness = false required-features = ["custom"] [[bench]] name = "benchmarks_standard" harness = false required-features = ["standard"] [[bench]] name = "iai_bench_parsing_time_units" harness = false required-features = [ "standard", "with-iai", ] [[bench]] name = "iai_bench_parsing" harness = false required-features = [ "standard", "with-iai", ] [[bench]] name = "iai_bench_parsing_negative" harness = false required-features = [ "standard", "with-iai", ] [[bench]] name = "iai_bench_parsing_infinity" harness = false required-features = [ "standard", "with-iai", ] [[bench]] name = "iai_bench_reference" harness = false required-features = ["with-iai"] [[bench]] name = "iai_bench_standard" harness = false required-features = [ "standard", "with-iai", ] [[bench]] name = "iai_bench_custom" harness = false required-features = [ "custom", "with-iai", ] [[bench]] name = "flamegraph_standard" bench = false harness = false required-features = [ "standard", "with-flamegraph", ] [[bench]] name = "flamegraph_custom" bench = false harness = false required-features = [ "custom", "with-flamegraph", ] [dependencies.chrono] version = "0.4.24" optional = true default-features = false [dependencies.serde] version = "1.0.162" features = ["derive"] optional = true [dependencies.time] version = "<= 0.3.16" optional = true default-features = false [dev-dependencies.chrono] version = "0.4.23" default-features = false [dev-dependencies.clap] version = "<=3.2.23" features = [ "cargo", "derive", ] [dev-dependencies.criterion] version = "0.4.0" [dev-dependencies.rstest] version = "0.17.0" [dev-dependencies.rstest_reuse] version = "0.5.0" [dev-dependencies.serde_test] version = "1.0.162" [features] chrono = ["dep:chrono"] custom = [] default = ["standard"] serde = ["dep:serde"] standard = [] time = ["dep:time"] with-flamegraph = [] with-iai = [] [target."cfg(unix)".dev-dependencies.iai-callgrind] version = "0.3.1" [target."cfg(unix)".dev-dependencies.inferno] version = "=0.11.14" [target."cfg(unix)".dev-dependencies.pprof] version = "0.11.0" features = [ "flamegraph", "criterion", ] fundu-1.0.0/Cargo.toml.orig000064400000000000000000000056701046102023000136420ustar 00000000000000[package] name = "fundu" version = "1.0.0" edition = "2021" authors = ["Joining7943 "] description = "Configurable, precise and fast rust string parser to a Duration" readme = "README.md" license = "MIT" keywords = ["fundu", "parse", "string", "duration", "time"] categories = ["command-line-interface", "parsing", "date-and-time"] homepage = "https://github.com/Joining7943/fundu" repository = "https://github.com/Joining7943/fundu" rust-version = "1.61.0" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [lib] bench = false [features] default = ["standard"] standard = [] custom = [] time = ["dep:time"] chrono = ["dep:chrono"] serde = ["dep:serde"] with-iai = [] with-flamegraph = [] [dependencies] time = { version = "<= 0.3.16", optional = true, default-features = false } chrono = { version = "0.4.24", optional = true, default-features = false } serde = { version = "1.0.162", optional = true, features=["derive"]} [dev-dependencies] rstest = "0.17.0" rstest_reuse = "0.5.0" criterion = "0.4.0" chrono = { version = "0.4.23", default-features = false } clap = { version = "<=3.2.23", features = ["cargo", "derive"] } serde_test = "1.0.162" [target.'cfg(unix)'.dev-dependencies] pprof = { version = "0.11.0", features = ["flamegraph", "criterion"] } inferno = { version = "=0.11.14" } iai-callgrind = "0.3.1" [profile.flamegraph] inherits = "bench" opt-level = 1 debug = true [[bench]] name = "benchmarks_custom" harness = false required-features = ["custom"] [[bench]] name = "benchmarks_standard" harness = false required-features = ["standard"] [[bench]] name = "iai_bench_parsing_time_units" harness = false required-features = ["standard", "with-iai"] [[bench]] name = "iai_bench_parsing" harness = false required-features = ["standard", "with-iai"] [[bench]] name = "iai_bench_parsing_negative" harness = false required-features = ["standard", "with-iai"] [[bench]] name = "iai_bench_parsing_infinity" harness = false required-features = ["standard", "with-iai"] [[bench]] name = "iai_bench_reference" harness = false required-features = ["with-iai"] [[bench]] name = "iai_bench_standard" harness = false required-features = ["standard", "with-iai"] [[bench]] name = "iai_bench_custom" harness = false required-features = ["custom", "with-iai"] [[bench]] name = "flamegraph_standard" harness = false bench = false required-features = ["standard", "with-flamegraph"] [[bench]] name = "flamegraph_custom" harness = false bench = false required-features = ["custom", "with-flamegraph"] [[example]] name = "simple" test = true [[example]] name = "clap_derive" test = true [[example]] name = "clap_builder" test = true [[example]] name = "convert" test = false required-features = ["chrono", "time"] [[example]] name = "custom" test = false required-features = ["custom"] [[example]] name = "systemd" test = false required-features = ["custom"] [[example]] name = "gnu" test = false required-features = ["custom"] fundu-1.0.0/LICENSE000064400000000000000000000021001046102023000117410ustar 00000000000000MIT License Copyright (c) 2023 Joining7943 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. fundu-1.0.0/README.md000064400000000000000000000465101046102023000122300ustar 00000000000000

Configurable, precise and fast rust string parser to a Duration


## Table of Contents - [Table of Contents](#table-of-contents) - [Overview](#overview) - [Installation](#installation) - [Examples](#examples) - [Time Units](#time-units) - [Customization](#customization) - [Benchmarks](#benchmarks) - [Comparison](#comparison-fundu-vs-durationfrom_secs_f64) - [Platform support](#platform-support) - [License](#license) # Overview `fundu` provides a flexible and fast parser to convert rust strings into a `Duration`. `fundu` parses into its own `Duration` but provides methods to convert into [`std::time::Duration`], [`chrono::Duration`] and [`time::Duration`]. Some examples for valid input strings with the `standard` feature: - `"1.41"` - `"42"` - `"2e-8"`, `"2e+8"` (or likewise `"2.0e8"`) - `".5"` or likewise `"0.5"` - `"3."` or likewise `"3.0"` - `"inf"`, `"+inf"`, `"infinity"`, `"+infinity"` - `"1w"` (1 week) or likewise `"7d"`, `"168h"`, `"10080m"`, `"604800s"`, ... For examples of the `custom` feature see [Customization section](#customization). Summary of features provided by this crate: - __Precision__: There are no floating point calculations and the input is precisely parsed as it is. So, what you put in you is what you get out within the range of a `Duration`. (See also [Comparison](#comparison-fundu-vs-durationfrom_secs_f64)) - __Performance__: The parser is blazingly fast ([Benchmarks](#benchmarks)) - __Customization__: [`TimeUnits`](#time-units), the number format and other aspects are easily configurable ([Customization](#customization)) - __Sound limits__: The duration saturates at [`Duration::MAX`] if the input number was larger than that maximum or if the input string was positive `infinity`. - __Negative Durations__: The parser can be configured to parse negative durations. Fundu's `Duration` can represent negative durations but also implements `TryFrom` for [`chrono::Duration`] and [`time::Duration`] if the corresponding feature is activated. - __Error handling__: The error messages try to be informative on their own but can also be easily adjusted (See also [Examples](#examples)) `fundu` aims for good performance and being a lightweight crate. It is purely built on top of the rust `stdlib`, and there are no additional dependencies required in the standard configuration. The accepted number format is per default the scientific floating point format and compatible with [`f64::from_str`]. However, the number format and other aspects can be [customized](#customization) up to formats like [systemd time spans](https://www.man7.org/linux/man-pages/man7/systemd.time.7.html) or [gnu relative times](https://www.gnu.org/software/coreutils/manual/html_node/Relative-items-in-date-strings.html). See also the examples [Examples section](#examples) and the [examples](examples) folder. For a direct comparison of `fundu` vs the rust native methods `Duration::(try_)from_secs_f64` see [Comparison](#comparison-fundu-vs-durationfrom_secs_f64). For further details see the [Documentation](https://docs.rs/crate/fundu)! # Installation Add this to `Cargo.toml` for `fundu` with the `standard` feature. ```toml [dependencies] fundu = "1.0.0" ``` fundu is split into two main features, `standard` (providing `DurationParser` and `parse_duration`) and `custom` (providing the `CustomDurationParser`). The first is described here in in detail, the latter adds fully customizable identifiers for [time units](#time-units). Most of the time only one of the parsers is needed. To include only the `CustomDurationParser` add the following to `Cargo.toml`: ```toml [dependencies] fundu = { version = "1.0.0", default-features = false, features = ["custom"] } ``` Activating the `chrono` or `time` feature provides a `TryFrom` implementation for [`chrono::Duration`] or [`time::Duration`]. Activating the `serde` feature allows some structs and enums to be serialized or deserialized with [serde](https://docs.rs/serde/latest/serde/) # Examples If only the default configuration is required once, the `parse_duration` method can be used. Note that `parse_duration` returns a [`std::time::Duration`] in contrast to the `parse` method of the other parsers which return a `fundu::Duration`. ```rust use std::time::Duration; use fundu::parse_duration; let input = "1.0e2s"; assert_eq!(parse_duration(input).unwrap(), Duration::new(100, 0)); ``` When a customization of the accepted [TimeUnit](#time-units)s is required, then `DurationParser::with_time_units` can be used. ```rust use fundu::{Duration, DurationParser}; let input = "3m"; assert_eq!( DurationParser::with_all_time_units().parse(input).unwrap(), Duration::positive(180, 0) ); ``` When no time units are configured, seconds is assumed. ```rust use fundu::{Duration, DurationParser}; let input = "1.0e2"; assert_eq!( DurationParser::without_time_units().parse(input).unwrap(), Duration::positive(100, 0) ); ``` However, the following will return an error because `y` (Years) is not a default time unit: ```rust use fundu::DurationParser; let input = "3y"; assert!(DurationParser::new().parse(input).is_err()); ``` The parser is reusable and the set of time units is fully customizable ```rust use fundu::TimeUnit::*; use fundu::{Duration, DurationParser}; let parser = DurationParser::with_time_units(&[NanoSecond, Minute, Hour]); assert_eq!(parser.parse("9e3ns").unwrap(), Duration::positive(0, 9000)); assert_eq!(parser.parse("10m").unwrap(), Duration::positive(600, 0)); assert_eq!(parser.parse("1.1h").unwrap(), Duration::positive(3960, 0)); assert_eq!(parser.parse("7").unwrap(), Duration::positive(7, 0)); ``` Setting the default time unit (if no time unit is given in the input string) to something different than seconds is also easily possible ```rust use fundu::TimeUnit::*; use fundu::{Duration, DurationParser}; assert_eq!( DurationParser::without_time_units() .default_unit(MilliSecond) .parse("1000") .unwrap(), Duration::positive(1, 0) ); ``` The identifiers for time units can be fully customized with any number of valid [utf-8](https://en.wikipedia.org/wiki/UTF-8) sequences if the `custom` feature is activated: ```rust use fundu::TimeUnit::*; use fundu::{CustomTimeUnit, CustomDurationParser, Duration}; let parser = CustomDurationParser::with_time_units(&[ CustomTimeUnit::with_default(MilliSecond, &["χιλιοστό του δευτερολέπτου"]), CustomTimeUnit::with_default(Second, &["s", "secs"]), CustomTimeUnit::with_default(Hour, &["⏳"]), ]); assert_eq!(parser.parse(".3χιλιοστό του δευτερολέπτου"), Ok(Duration::positive(0, 300_000))); assert_eq!(parser.parse("1e3secs"), Ok(Duration::positive(1000, 0))); assert_eq!(parser.parse("1.1⏳"), Ok(Duration::positive(3960, 0))); ``` The `custom` feature can be used to customize a lot more. See the documentation of the exported items of the `custom` feature (like `CustomTimeUnit`, `TimeKeyword`) for more information. Also, `fundu` tries to give informative error messages ```rust use fundu::DurationParser; assert_eq!( DurationParser::without_time_units() .parse("1y") .unwrap_err() .to_string(), "Time unit error: No time units allowed but found: 'y' at column 1" ); ``` The number format can be easily adjusted to your needs. For example to allow numbers being optional, allow some ascii whitespace between the number and the time unit and restrict the number format to whole numbers, without fractional part and an exponent (Also note that the `DurationParserBuilder` can build a `DurationParser` at compile time in `const` context): ```rust use fundu::TimeUnit::*; use fundu::{Duration, DurationParser, ParseError}; const PARSER: DurationParser = DurationParser::builder() .time_units(&[NanoSecond]) .allow_delimiter(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) .number_is_optional() .disable_fraction() .disable_exponent() .build(); assert_eq!(PARSER.parse("ns").unwrap(), Duration::positive(0, 1)); assert_eq!( PARSER.parse("1000\t\n\r ns").unwrap(), Duration::positive(0, 1000) ); assert_eq!( PARSER.parse("1.0ns").unwrap_err(), ParseError::Syntax(1, "No fraction allowed".to_string()) ); assert_eq!( PARSER.parse("1e9ns").unwrap_err(), ParseError::Syntax(1, "No exponent allowed".to_string()) ); ``` It's also possible to parse multiple durations at once with `parse_multiple`. The different durations can be separated by an optional `delimiter` (a closure matching a `u8`) defined with `parse_multiple`. If the delimiter is not encountered, a number can also indicate a new duration. ```rust use fundu::{Duration, DurationParser}; let parser = DurationParser::builder() .default_time_units() .parse_multiple(|byte| matches!(byte, b' ' | b'\t'), Some(&["and"])) .build(); assert_eq!( parser.parse("1.5h 2e+2ns"), Ok(Duration::positive(5400, 200)) ); assert_eq!( parser.parse("55s500ms"), Ok(Duration::positive(55, 500_000_000)) ); assert_eq!(parser.parse("1\t1"), Ok(Duration::positive(2, 0))); assert_eq!( parser.parse("1. .1"), Ok(Duration::positive(1, 100_000_000)) ); assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0))); assert_eq!( parser.parse("300ms20s 5d"), Ok(Duration::positive(5 * 60 * 60 * 24 + 20, 300_000_000)) ); assert_eq!( parser.parse("300.0ms and 5d"), Ok(Duration::positive(5 * 60 * 60 * 24, 300_000_000)) ); ``` See also the [examples folder](examples) for common recipes and integration with other crates. Run an example with ```shell cargo run --example $FILE_NAME_WITHOUT_FILETYPE_SUFFIX ``` like the systemd time span parser example ```shell # For some of the examples a help is available. To pass arguments to the example itself separate # the arguments for cargo and the example with `--` $ cargo run --example systemd --features custom --no-default-features -- --help ... # To actually run the example execute $ cargo run --example systemd --features custom --no-default-features '300ms20s 5day' Original: 300ms20s 5day μs: 432020300000 Human: 5d 20s 300ms ``` # Time units `Second` is the default time unit (if not specified otherwise for example with [`DurationParser::default_unit`]) which is applied when no time unit was encountered in the input string. The table below gives an overview of the constructor methods and which time units are available. If a custom set of time units is required, `DurationParser::with_time_units` can be used. TimeUnit | Default identifier | Calculation | Default time unit ---:| ---:| ---:|:---: `Nanosecond` | ns | `1e-9s` | ☑ `Microsecond` | Ms | `1e-6s` | ☑ `Millisecond` | ms | `1e-3s` | ☑ `Second` | s | SI definition | ☑ `Minute` | m | `60s` | ☑ `Hour` | h | `60m` | ☑ `Day` | d | `24h` | ☑ `Week` | w | `7d` | ☑ `Month` | M | `Year / 12` | ☐ `Year` | y | `365.25d` | ☐ Note that `Months` and `Years` are not included in the default set of time units. The current implementation uses an approximate calculation of `Months` and `Years` in seconds and if they are included in the final configuration, the [Julian year](https://en.wikipedia.org/wiki/Julian_year_(astronomy)) based calculation is used. (See table above) With the `CustomDurationParser` from the `custom` feature, the identifiers for time units can be fully customized. # Customization Unlike other crates, `fundu` does not try to establish a standard for time units and their identifiers or a specific number format. A lot of these aspects can be adjusted when initializing or building the parser. Here's an incomplete example for possible customizations of the number format: ```rust use fundu::TimeUnit::*; use fundu::{Duration, DurationParser, ParseError}; let parser = DurationParser::builder() // Use a custom set of time units. For demonstration purposes just NanoSecond .time_units(&[NanoSecond]) // Allow some whitespace characters as delimiter between the number and the time unit .allow_delimiter(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) // Makes the number optional. If no number was encountered `1` is assumed .number_is_optional() // Disable parsing the fractional part of the number => 1.0 will return an error .disable_fraction() // Disable parsing the exponent => 1e0 will return an error .disable_exponent() // Finally, build a reusable DurationParser .build(); // Some valid input assert_eq!(parser.parse("ns").unwrap(), Duration::positive(0, 1)); assert_eq!( parser.parse("1000\t\n\r ns").unwrap(), Duration::positive(0, 1000) ); // Some invalid input assert_eq!( parser.parse("1.0ns").unwrap_err(), ParseError::Syntax(1, "No fraction allowed".to_string()) ); assert_eq!( parser.parse("1e9ns").unwrap_err(), ParseError::Syntax(1, "No exponent allowed".to_string()) ); ``` Here's an example for fully-customizable time units which uses the `CustomDurationParser` from the `custom` feature: ```rust use fundu::TimeUnit::*; use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier, TimeKeyword}; // Let's define a custom time unit `fortnight` which is worth 2 weeks. Note the creation // of `CustomTimeUnits` and `TimeKeywords` can be `const` and moved to compile time: const FORTNIGHT: CustomTimeUnit = CustomTimeUnit::new( Week, &["f", "fortnight", "fortnights"], Some(Multiplier(2, 0)), ); let parser = CustomDurationParser::builder() .time_units(&[ CustomTimeUnit::with_default(Second, &["s", "secs", "seconds"]), CustomTimeUnit::with_default(Minute, &["min"]), CustomTimeUnit::with_default(Hour, &["ώρα"]), FORTNIGHT, ]) // Additionally, define `tomorrow`, a keyword of time which is worth `1 day` in the future. // In contrast to a `CustomTimeUnit`, a `TimeKeyword` doesn't accept a number in front of it // in the source string. .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .build(); assert_eq!( parser.parse("42e-1ώρα").unwrap(), Duration::positive(15120, 0) ); assert_eq!( parser.parse("tomorrow").unwrap(), Duration::positive(60 * 60 * 24, 0) ); assert_eq!( parser.parse("1fortnight").unwrap(), Duration::positive(60 * 60 * 24 * 7 * 2, 0) ); ``` # Benchmarks To run the benchmarks on your machine, clone the repository ```shell git clone https://github.com/Joining7943/fundu.git cd fundu ``` and then run all benchmarks with ```shell cargo bench --all-features ``` The `iai-callgrind` (feature = `with-iai`) and `flamegraph` (feature = `with-flamegraph`) benchmarks can only be run on unix. Use the `--features` option of cargo to run the benchmarks for specific features: ```shell cargo bench --features standard,custom ``` The above won't run the `flamegraph` and `iai-callgrind` benchmarks. Benchmarks can be further filtered for example with ```shell cargo bench --bench benchmarks_standard cargo bench --bench benchmarks_standard -- 'parsing speed' cargo bench --features custom --no-default-features --bench benchmarks_custom ``` For more infos, see the help with ```shell cargo bench --help # The cargo help for bench cargo bench --bench benchmarks_standard -- --help # The criterion help ``` To get a rough idea about the parsing times, here the average parsing speed of some inputs on a comparatively slow machine (Quad core 3000Mhz, 8GB DDR3, Linux) Input | avg parsing time | ~ samples / s --- | ---:| ---: `1` | `37.925 ns` | `26_367_831.245` `123456789.123456789` | `50.473 ns` | `19_812_573.058` `format!("{}.{}e-1022", "1".repeat(1022), "1".repeat(1022))` | `371.02 ns` | `2_695_272.492` For comparison, the precision and additional features of `fundu` result in a very low performance overhead due to the initial setup of structures, etc., and quickly catches up. Fundu even becomes more performant than the reference function from the `stdlib` as the input gets larger (the reference function is `Duration::from_secs_f64(input.parse().unwrap())`): Input | avg parsing time | ~ samples / s --- | ---:| ---: `1` | `25.630 ns` | `39_016_777.214` `123456789.123456789` | `45.007 ns` | `22_218_765.969` `format!("{}.{}e-1022", "1".repeat(1022), "1".repeat(1022))` | `1.7457 µs` | `572_836.111` # Comparison `fundu` vs `Duration::from_secs_f64` Here's a short incomplete overview of differences and advantages of `fundu` over using `Duration::from_secs_f64(input.parse().unwrap())` (and `Duration::try_from_secs_f64(input.parse().unwrap())`) Input | `fundu` | `Duration::(try_)from_secs_f64` ---:| --- | --- `01271480964981728917.1` | `Duration::new(1_271_480_964_981_728_917, 100_000_000)` | `Duration::new(1_271_480_964_981_729_024, 0)` `1.11111111111e10` | `Duration::new(11_111_111_111, 100_000_000)` | `Duration::new(11_111_111_111, 100_000_381)` `1ns` | `Duration::new(0, 1)` | cannot parse time units `"1 2e-3 3e-9"`, `1s2ms3ns` | can parse multiple durations as one `Duration::new(1, 2_000_003)` | not possible `1000` | When changing the default unit to `MilliSecond` -> `Duration::new(1, 0)` | is always seconds based `1e20` | `Duration::MAX` | panics or returns an error due to: `can not convert float seconds to Duration: value is either too big or NaN` `infinity` | `Duration::MAX` | panics or returns an error due to: `can not convert float seconds to Duration: value is either too big or NaN` `-1`, `-1s`, ... | can parse negative durations if enabled | panics or returns an error # Platform support Since `fundu` is purely built on top of the rust `stdlib` without platform specific code, this library should be compatible with all platforms. Please open an issue if you find any unsupported platforms which `rust` itself supports. See also the [CI](https://github.com/Joining7943/fundu/actions/workflows/cicd.yml) for platforms on which `fundu` is tested. # License MIT license ([LICENSE](LICENSE) or ) [`std::time::Duration`]: https://doc.rust-lang.org/std/time/struct.Duration.html [`chrono::Duration`]: https://docs.rs/chrono/0.4.24/chrono/struct.Duration.html [`time::Duration`]: https://docs.rs/time/latest/time/struct.Duration.html [`Duration::MAX`]: https://doc.rust-lang.org/std/time/struct.Duration.html#associatedconstant.MAX [`f64::from_str`]: https://doc.rust-lang.org/std/primitive.f64.html#impl-FromStr-for-f64 [`DurationParser::default_unit`]: https://docs.rs/fundu/latest/fundu/struct.DurationParser.html#method.default_unit fundu-1.0.0/benches/benchmarks_custom.rs000064400000000000000000000045561046102023000164410ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use std::time::Duration; use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use fundu::{CustomDurationParser, DEFAULT_ALL_TIME_UNITS, SYSTEMD_TIME_UNITS}; fn criterion_config() -> Criterion { Criterion::default() .nresamples(500_000) .sample_size(100) .measurement_time(Duration::from_secs(5)) } fn benchmark_initialization(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("custom duration parser initialization"); group.bench_function("without time units", |b| b.iter(CustomDurationParser::new)); group.bench_with_input( "default time units", &DEFAULT_ALL_TIME_UNITS, |b, time_units| b.iter(|| CustomDurationParser::with_time_units(time_units)), ); group.bench_with_input( "systemd time units", &SYSTEMD_TIME_UNITS, |b, time_units| b.iter(|| CustomDurationParser::with_time_units(time_units)), ); group.finish(); } fn benchmark_parsing(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("custom duration parser parsing speed time units"); let default_parser = CustomDurationParser::with_time_units(&DEFAULT_ALL_TIME_UNITS); let systemd_parser = CustomDurationParser::with_time_units(&SYSTEMD_TIME_UNITS); for input in ["1", "1s", "1ns", "1y"] { group.bench_with_input( BenchmarkId::new("default time units", input), input, |b, input| b.iter(|| black_box(&default_parser).parse(input).unwrap()), ); group.bench_with_input( BenchmarkId::new("systemd time units", input), input, |b, input| b.iter(|| black_box(&systemd_parser).parse(input).unwrap()), ); } let input = "1years"; group.bench_with_input( BenchmarkId::new("systemd time units", input), input, |b, input| b.iter(|| black_box(&systemd_parser).parse(input).unwrap()), ); group.finish(); } criterion_group!( name = initialization; config = criterion_config(); targets = benchmark_initialization ); criterion_group!( name = parsing; config = criterion_config(); targets = benchmark_parsing ); criterion_main!(initialization, parsing); fundu-1.0.0/benches/benchmarks_standard.rs000064400000000000000000000113001046102023000167100ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use std::time::Duration; use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use fundu::DurationParser; use fundu::TimeUnit::*; fn criterion_config() -> Criterion { Criterion::default() .nresamples(500_000) .sample_size(100) .measurement_time(Duration::from_secs(5)) } fn get_parsing_speed_inputs() -> Vec<(String, String)> { vec![ ("single digit".to_string(), "1".to_string()), ("mixed digits 7".to_string(), "1234567.1234567".to_string()), ( "mixed digits 8".to_string(), "12345678.12345678".to_string(), ), ( "mixed digits 9".to_string(), "123456789.123456789".to_string(), ), ( "large input".to_string(), format!("{}.{}e-1022", "1".repeat(1022), "1".repeat(1022)), ), ] } fn benchmark_initialization(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("standard duration parser initialization"); group.bench_function("without time units", |b| { b.iter(DurationParser::without_time_units) }); group.bench_function("default time units", |b| b.iter(DurationParser::new)); let input = &[ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year, ]; group.bench_with_input("custom time units", input, |b, input| { b.iter(|| DurationParser::with_time_units(input)) }); group.bench_function("all time units", |b| { b.iter(DurationParser::with_all_time_units) }); group.finish(); } fn benchmark_parsing(criterion: &mut Criterion) { let inputs = get_parsing_speed_inputs(); let parser = DurationParser::with_all_time_units(); let mut group = criterion.benchmark_group("parsing speed"); for (parameter, input) in inputs { group.bench_with_input( BenchmarkId::new("input without time units", parameter), &input, |b, input| b.iter(|| black_box(&parser).parse(input).unwrap()), ); } group.finish(); } fn benchmark_parsing_infinity(criterion: &mut Criterion) { let inputs = vec!["inf", "infinity"]; let parser = DurationParser::with_all_time_units(); let mut group = criterion.benchmark_group("parsing speed infinity"); for input in inputs { group.bench_with_input(input, input, |b, input| { b.iter(|| black_box(&parser).parse(input).unwrap()) }); } group.finish(); } fn benchmark_parsing_with_time_units(criterion: &mut Criterion) { let inputs = [(NanoSecond, "ns"), (Second, "s"), (Year, "y")]; let mut parser = DurationParser::with_all_time_units(); let mut group = criterion.benchmark_group("parsing speed time units"); for (unit, input) in inputs { parser.default_unit(unit); group.bench_with_input( BenchmarkId::new(format!("input without time unit (default = {unit:?})"), "1"), "1", |b, input| b.iter(|| black_box(&parser).parse(input).unwrap()), ); let input = format!("1{input}"); group.bench_with_input( BenchmarkId::new( format!("input with time units (default = {unit:?})"), &input, ), &input, |b, input| b.iter(|| black_box(&parser).parse(input).unwrap()), ); } group.finish(); } fn reference_benchmark(criterion: &mut Criterion) { let inputs = get_parsing_speed_inputs(); let mut group = criterion.benchmark_group("reference speed"); for (parameter, input) in inputs { group.bench_with_input( BenchmarkId::new("reference function", parameter), &input, |b, input| b.iter(|| Duration::from_secs_f64(input.parse().unwrap())), ); } group.finish(); } criterion_group!( name = initialization; config = criterion_config(); targets = benchmark_initialization ); criterion_group!( name = parsing; config = criterion_config(); targets = benchmark_parsing ); criterion_group!( name = parsing_infinity; config = criterion_config(); targets = benchmark_parsing_infinity ); criterion_group!( name = parsing_time_units; config = criterion_config(); targets = benchmark_parsing_with_time_units ); criterion_group!( name = reference; config = criterion_config(); targets = reference_benchmark ); criterion_main!( initialization, parsing, parsing_infinity, reference, parsing_time_units ); fundu-1.0.0/benches/flamegraph_custom.rs000064400000000000000000000046511046102023000164260ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! Flamegraphs for the custom module and the CustomDurationParser use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use fundu::{CustomDurationParser, DEFAULT_ALL_TIME_UNITS, SYSTEMD_TIME_UNITS}; use pprof::criterion::{Output, PProfProfiler}; use pprof::flamegraph::Options as FlamegraphOptions; fn flamegraph_initialization(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("custom duration parser initialization"); group.bench_function("without time units", |b| { b.iter(|| CustomDurationParser::new()) }); group.bench_with_input( "default time units", &DEFAULT_ALL_TIME_UNITS, |b, time_units| b.iter(|| CustomDurationParser::with_time_units(time_units)), ); group.bench_with_input( "systemd time units", &SYSTEMD_TIME_UNITS, |b, time_units| b.iter(|| CustomDurationParser::with_time_units(time_units)), ); group.finish(); } fn flamegraph_parsing(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("custom duration parser parsing"); for &input in &["1", "1ns", "1y", "1years"] { group.bench_with_input( BenchmarkId::new("systemd time units", input), &( CustomDurationParser::with_time_units(&SYSTEMD_TIME_UNITS), input, ), |b, (parser, input)| b.iter(|| parser.parse(input)), ); } group.finish(); } criterion_group!( name = initialization; config = Criterion::default().with_profiler(PProfProfiler::new(1_000_000, Output::Flamegraph({ let mut options = FlamegraphOptions::default(); options.title = "Flame graph for custom duration parser".to_string(); options.subtitle = Some("Initialization".to_string()); Some(options) }))); targets = flamegraph_initialization ); criterion_group!( name = parsing; config = Criterion::default().with_profiler(PProfProfiler::new(1_000_000, Output::Flamegraph({ let mut options = FlamegraphOptions::default(); options.title = "Flame graph for custom duration parser".to_string(); options.subtitle = Some("Parsing".to_string()); Some(options) }))); targets = flamegraph_parsing ); criterion_main!(initialization, parsing); fundu-1.0.0/benches/flamegraph_standard.rs000064400000000000000000000052311046102023000167070ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! Flamegraphs for the standard module and the DurationParser use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use fundu::DurationParser; use fundu::TimeUnit::*; use pprof::criterion::{Output, PProfProfiler}; use pprof::flamegraph::Options as FlamegraphOptions; fn flamegraph_initialization(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("standard duration parser initialization"); group.bench_function("without time units", |b| { b.iter(|| DurationParser::without_time_units()) }); group.bench_function("default time units", |b| b.iter(|| DurationParser::new())); let input = &[ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year, ]; group.bench_with_input("custom time units", input, |b, input| { b.iter(|| DurationParser::with_time_units(input)) }); group.bench_function("all time units", |b| { b.iter(|| DurationParser::with_all_time_units()) }); group.finish(); } fn flamegraph_parsing(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("standard duration parser parsing"); for &input in &[ "1", "1s", "1ns", "1y", "1234567.1234567", "12345678.12345678", &format!("{}.{}e-1022", "1".repeat(1022), "1".repeat(1022)), ] { group.bench_with_input( BenchmarkId::new("all default time units", input), &(DurationParser::with_all_time_units(), input), |b, (parser, input)| b.iter(|| parser.parse(input)), ); } group.finish(); } criterion_group!( name = initialization; config = Criterion::default().with_profiler(PProfProfiler::new(1_000_000, Output::Flamegraph({ let mut options = FlamegraphOptions::default(); options.title = "Flame graph for standard duration parser".to_string(); options.subtitle = Some("Initialization".to_string()); Some(options) }))); targets = flamegraph_initialization ); criterion_group!( name = parsing; config = Criterion::default().with_profiler(PProfProfiler::new(1_000_000, Output::Flamegraph({ let mut options = FlamegraphOptions::default(); options.title = "Flame graph for standard duration parser".to_string(); options.subtitle = Some("Parsing".to_string()); Some(options) }))); targets = flamegraph_parsing ); criterion_main!(initialization, parsing); fundu-1.0.0/benches/iai_bench_custom.rs000064400000000000000000000017171046102023000162210ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use fundu::{CustomDurationParser, DEFAULT_ALL_TIME_UNITS, SYSTEMD_TIME_UNITS}; use iai_callgrind::{black_box, main}; #[inline(never)] fn initialization_without_time_units<'a>() -> CustomDurationParser<'a> { CustomDurationParser::new() } #[inline(never)] fn initialization_with_default_time_units<'a>() -> CustomDurationParser<'a> { CustomDurationParser::with_time_units(black_box(&DEFAULT_ALL_TIME_UNITS)) } #[inline(never)] fn initialization_with_systemd_time_units<'a>() -> CustomDurationParser<'a> { CustomDurationParser::with_time_units(black_box(&SYSTEMD_TIME_UNITS)) } main!( callgrind_args = "toggle-collect=iai_callgrind::black_box"; functions = initialization_without_time_units, initialization_with_default_time_units, initialization_with_systemd_time_units ); fundu-1.0.0/benches/iai_bench_parsing.rs000064400000000000000000000032131046102023000163430ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use fundu::{Duration, DurationParser, ParseError}; use iai_callgrind::{black_box, main}; type Result = std::result::Result; const SMALL_INPUT: &str = "1"; const MIXED_INPUT_7: &str = "1234567.1234567"; const MIXED_INPUT_8: &str = "12345678.12345678"; #[inline(never)] #[export_name = "__iai_setup::setup_parser"] fn setup_parser<'a>() -> DurationParser<'a> { DurationParser::without_time_units() } #[inline(never)] #[export_name = "__iai_setup::generate_large_input"] fn generate_large_input() -> String { let ones = "1".repeat(1022); format!("{}.{}e-1022", &ones, &ones) } #[inline(never)] fn small_input() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(SMALL_INPUT)) } #[inline(never)] fn mixed_input_7() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(MIXED_INPUT_7)) } #[inline(never)] fn mixed_input_8() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(MIXED_INPUT_8)) } #[inline(never)] fn large_input() -> Result { let parser = setup_parser(); let input = generate_large_input(); black_box(parser).parse(black_box(&input)) } main!( callgrind_args = "toggle-collect=iai_callgrind::black_box", "toggle-collect=__iai_setup::setup_parser", "toggle-collect=__iai_setup::generate_large_input"; functions = small_input, mixed_input_7, mixed_input_8, large_input, ); fundu-1.0.0/benches/iai_bench_parsing_infinity.rs000064400000000000000000000017311046102023000202570ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use fundu::{Duration, DurationParser, ParseError}; use iai_callgrind::{black_box, main}; type Result = std::result::Result; const SHORT_INF: &str = "inf"; const LONG_INFINITY: &str = "infinity"; #[inline(never)] #[export_name = "__iai_setup::setup_parser"] fn setup_parser<'a>() -> DurationParser<'a> { DurationParser::without_time_units() } #[inline(never)] fn short_inf() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(SHORT_INF)) } #[inline(never)] fn long_infinity() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(LONG_INFINITY)) } main!( callgrind_args = "toggle-collect=iai_callgrind::black_box", "toggle-collect=__iai_setup::setup_parser"; functions = short_inf, long_infinity, ); fundu-1.0.0/benches/iai_bench_parsing_negative.rs000064400000000000000000000034331046102023000202310ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use fundu::{Duration, DurationParser, ParseError}; use iai_callgrind::{black_box, main}; type Result = std::result::Result; const SMALL_NEGATIVE_INPUT: &str = "-1"; const MIXED_NEGATIVE_INPUT_7: &str = "-1234567.1234567"; const MIXED_NEGATIVE_INPUT_8: &str = "-12345678.12345678"; #[inline(never)] #[export_name = "__iai_setup::setup_parser"] fn setup_parser<'a>() -> DurationParser<'a> { DurationParser::builder().allow_negative().build() } #[inline(never)] #[export_name = "__iai_setup::generate_large_input"] fn generate_large_input() -> String { let ones = "1".repeat(1022); format!("-{}.{}e-1022", &ones, &ones) } #[inline(never)] fn small_negative_input() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(SMALL_NEGATIVE_INPUT)) } #[inline(never)] fn mixed_negative_input_7() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(MIXED_NEGATIVE_INPUT_7)) } #[inline(never)] fn mixed_negative_input_8() -> Result { let parser = setup_parser(); black_box(parser).parse(black_box(MIXED_NEGATIVE_INPUT_8)) } #[inline(never)] fn large_negative_input() -> Result { let parser = setup_parser(); let input = generate_large_input(); black_box(parser).parse(black_box(&input)) } main!( callgrind_args = "toggle-collect=iai_callgrind::black_box", "toggle-collect=__iai_setup::setup_parser", "toggle-collect=__iai_setup::generate_large_input"; functions = small_negative_input, mixed_negative_input_7, mixed_negative_input_8, large_negative_input, ); fundu-1.0.0/benches/iai_bench_parsing_time_units.rs000064400000000000000000000050421046102023000206050ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use fundu::TimeUnit::{self, *}; use fundu::{Duration, DurationParser, ParseError}; use iai_callgrind::{black_box, main}; type Result = std::result::Result; const INPUT_NO_TIME_UNIT: &str = "1"; const INPUT_NANO_SECOND: &str = "1ns"; const INPUT_SECOND: &str = "1y"; const INPUT_YEAR: &str = "1y"; #[inline(never)] #[export_name = "__iai_setup::setup_parser_with_all_time_units"] fn setup_parser_with_all_time_units<'a>(default_unit: TimeUnit) -> DurationParser<'a> { let mut parser = DurationParser::with_all_time_units(); parser.default_unit(default_unit); parser } #[inline(never)] fn parsing_nano_second_when_no_time_unit() -> Result { let time_unit = black_box(NanoSecond); let parser = setup_parser_with_all_time_units(time_unit); black_box(parser).parse(black_box(INPUT_NO_TIME_UNIT)) } #[inline(never)] fn parsing_nano_second_when_time_unit() -> Result { let time_unit = black_box(NanoSecond); let parser = setup_parser_with_all_time_units(time_unit); black_box(parser).parse(black_box(INPUT_NANO_SECOND)) } #[inline(never)] fn parsing_second_when_no_time_unit() -> Result { let time_unit = black_box(Second); let parser = setup_parser_with_all_time_units(time_unit); black_box(parser).parse(black_box(INPUT_NO_TIME_UNIT)) } #[inline(never)] fn parsing_second_when_time_unit() -> Result { let time_unit = black_box(Second); let parser = setup_parser_with_all_time_units(time_unit); black_box(parser).parse(black_box(INPUT_SECOND)) } #[inline(never)] fn parsing_year_when_no_time_unit() -> Result { let time_unit = black_box(Year); let parser = setup_parser_with_all_time_units(time_unit); black_box(parser).parse(black_box(INPUT_NO_TIME_UNIT)) } #[inline(never)] fn parsing_year_when_time_unit() -> Result { let time_unit = black_box(Year); let parser = setup_parser_with_all_time_units(time_unit); black_box(parser).parse(black_box(INPUT_YEAR)) } main!( callgrind_args = "toggle-collect=iai_callgrind::black_box", "toggle-collect=__iai_setup::setup_parser_with_all_time_units"; functions = parsing_nano_second_when_no_time_unit, parsing_nano_second_when_time_unit, parsing_second_when_no_time_unit, parsing_second_when_time_unit, parsing_year_when_no_time_unit, parsing_year_when_time_unit ); fundu-1.0.0/benches/iai_bench_reference.rs000064400000000000000000000015611046102023000166420ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use std::time::Duration; use iai_callgrind::{black_box, main}; const SMALL_INPUT: &str = "1"; #[inline(never)] #[export_name = "__iai_setup::generate_large_input"] fn generate_large_input() -> String { let ones = "1".repeat(1022); format!("{}.{}e-1022", &ones, &ones) } #[inline(never)] fn small_reference() -> Duration { Duration::from_secs_f64(black_box(SMALL_INPUT).parse().unwrap()) } #[inline(never)] fn large_reference() -> Duration { Duration::from_secs_f64(black_box(generate_large_input()).parse().unwrap()) } main!( callgrind_args = "toggle-collect=iai_callgrind::black_box", "toggle-collect=__iai_setup::generate_large_input"; functions = small_reference, large_reference ); fundu-1.0.0/benches/iai_bench_standard.rs000064400000000000000000000026111046102023000165010ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use fundu::DurationParser; use fundu::TimeUnit::{self, *}; use iai_callgrind::{black_box, main}; #[inline(never)] #[export_name = "__iai_setup::get_all_time_units"] fn get_all_time_units<'a>() -> &'a [TimeUnit] { &[ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year, ] } #[inline(never)] fn initialization_without_time_units<'a>() -> DurationParser<'a> { DurationParser::without_time_units() } #[inline(never)] fn initialization_with_default_time_units<'a>() -> DurationParser<'a> { DurationParser::new() } #[inline(never)] fn initialization_with_all_time_units<'a>() -> DurationParser<'a> { DurationParser::with_all_time_units() } #[inline(never)] fn initialization_with_custom_time_units<'a>() -> DurationParser<'a> { DurationParser::with_time_units(black_box(get_all_time_units())) } main!( callgrind_args = "toggle-collect=iai_callgrind::black_box", "toggle-collect=__iai_setup::get_all_time_units"; functions = initialization_without_time_units, initialization_with_default_time_units, initialization_with_all_time_units, initialization_with_custom_time_units, ); fundu-1.0.0/deny.toml000064400000000000000000000227561046102023000126130ustar 00000000000000# This template contains all of the possible sections and their default values # Note that all fields that take a lint level have these possible values: # * deny - An error will be produced and the check will fail # * warn - A warning will be produced, but the check will not fail # * allow - No warning or error will be produced, though in some cases a note # will be # The values provided in this template are the default values that will be used # when any section or field is not specified in your own configuration # If 1 or more target triples (and optionally, target_features) are specified, # only the specified targets will be checked when running `cargo deny check`. # This means, if a particular package is only ever used as a target specific # dependency, such as, for example, the `nix` crate only being used via the # `target_family = "unix"` configuration, that only having windows targets in # this list would mean the nix crate, as well as any of its exclusive # dependencies not shared by any other crates, would be ignored, as the target # list here is effectively saying which targets you are building for. targets = [ # The triple can be any string, but only the target triples built in to # rustc (as of 1.40) can be checked against actual config expressions #{ triple = "x86_64-unknown-linux-musl" }, # You can also specify which target_features you promise are enabled for a # particular target. target_features are currently not validated against # the actual valid features supported by the target architecture. #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, ] # This section is considered when running `cargo deny check advisories` # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] # The path where the advisory database is cloned/fetched into db-path = "~/.cargo/advisory-db" # The url(s) of the advisory databases to use db-urls = ["https://github.com/rustsec/advisory-db"] # The lint level for security vulnerabilities vulnerability = "deny" # The lint level for unmaintained crates unmaintained = "warn" # The lint level for crates that have been yanked from their source registry yanked = "warn" # The lint level for crates with security notices. Note that as of # 2019-12-17 there are no security notice advisories in # https://github.com/rustsec/advisory-db notice = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ #"RUSTSEC-0000-0000", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories # will still output a note when they are encountered. # * None - CVSS Score 0.0 # * Low - CVSS Score 0.1 - 3.9 # * Medium - CVSS Score 4.0 - 6.9 # * High - CVSS Score 7.0 - 8.9 # * Critical - CVSS Score 9.0 - 10.0 #severity-threshold = # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. # See Git Authentication for more information about setting up git authentication. #git-fetch-with-cli = true # This section is considered when running `cargo deny check licenses` # More documentation for the licenses section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] # The lint level for crates which do not have a detectable license unlicensed = "deny" # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. allow = [ "MIT", "Apache-2.0", "BSD-3-Clause", "Unicode-DFS-2016", "CDDL-1.0" ] # List of explicitly disallowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. deny = [ #"Nokia", ] # Lint level for licenses considered copyleft copyleft = "warn" # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses # * both - The license will be approved if it is both OSI-approved *AND* FSF # * either - The license will be approved if it is either OSI-approved *OR* FSF # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved # * neither - This predicate is ignored and the default lint level is used allow-osi-fsf-free = "neither" # Lint level used when no other predicates are matched # 1. License isn't in the allow or deny lists # 2. License isn't copyleft # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" default = "deny" # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the # canonical license text of a valid SPDX license file. # [possible values: any between 0.0 and 1.0]. confidence-threshold = 0.8 # Allow 1 or more licenses on a per-crate basis, so that particular licenses # aren't accepted for every possible crate as with the normal allow list exceptions = [ # Each entry is the crate and version constraint, and its specific allow # list #{ allow = ["Zlib"], name = "adler32", version = "*" }, ] # Some crates don't have (easily) machine readable licensing information, # adding a clarification entry for it allows you to manually specify the # licensing information #[[licenses.clarify]] # The name of the crate the clarification applies to #name = "ring" # The optional version constraint for the crate #version = "*" # The SPDX expression for the license requirements of the crate #expression = "MIT AND ISC AND OpenSSL" # One or more files in the crate's source used as the "source of truth" for # the license expression. If the contents match, the clarification will be used # when running the license check, otherwise the clarification will be ignored # and the crate will be checked normally, which may produce warnings or errors # depending on the rest of your configuration #license-files = [ # Each entry is a crate relative path, and the (opaque) hash of its contents #{ path = "LICENSE", hash = 0xbd0eed23 } #] [licenses.private] # If true, ignores workspace crates that aren't published, or are only # published to private registries. # To see how to mark a crate as unpublished (to the official registry), # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. ignore = false # One or more private registries that you might publish crates to, if a crate # is only published to private registries, and ignore is true, the crate will # not have its license(s) checked registries = [ #"https://sekretz.com/registry ] # This section is considered when running `cargo deny check bans`. # More documentation about the 'bans' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html [bans] # Lint level for when multiple versions of the same crate are detected multiple-versions = "warn" # Lint level for when a crate version requirement is `*` wildcards = "allow" # The graph highlighting used when creating dotgraphs for crates # with multiple versions # * lowest-version - The path to the lowest versioned duplicate is highlighted # * simplest-path - The path to the version with the fewest edges is highlighted # * all - Both lowest-version and simplest-path are used highlight = "all" # List of crates that are allowed. Use with care! allow = [ #{ name = "ansi_term", version = "=0.11.0" }, ] # List of crates to deny deny = [ # Each entry the name of a crate and a version range. If version is # not specified, all versions will be matched. #{ name = "ansi_term", version = "=0.11.0" }, # # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, ] # Certain crates/versions that will be skipped when doing duplicate detection. skip = [ # { name = "hermit-abi" }, ] # Similarly to `skip` allows you to skip certain crates during duplicate # detection. Unlike skip, it also includes the entire tree of transitive # dependencies starting at the specified crate, up to a certain depth, which is # by default infinite skip-tree = [ #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, ] # This section is considered when running `cargo deny check sources`. # More documentation about the 'sources' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html [sources] # Lint level for what to happen when a crate from a crate registry that is not # in the allow list is encountered unknown-registry = "warn" # Lint level for what to happen when a crate from a git repository that is not # in the allow list is encountered unknown-git = "warn" # List of URLs for allowed crate registries. Defaults to the crates.io index # if not specified. If it is specified but empty, no registries are allowed. allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories allow-git = [] [sources.allow-org] # 1 or more github.com organizations to allow git sources for # github = [""] # 1 or more gitlab.com organizations to allow git sources for # gitlab = [""] # 1 or more bitbucket.org organizations to allow git sources for # bitbucket = [""] fundu-1.0.0/examples/clap_builder.rs000064400000000000000000000017111046102023000155540ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use clap::{arg, command}; use fundu::DurationParser; fn main() { let matches = command!() .arg(arg!([DURATION1] "A duration")) .arg( arg!([DURATION2] "An optional duration to sum with the first duration").required(false), ) .get_matches(); let parser = DurationParser::new(); if let Some(arg2) = matches.get_one::("DURATION2") { let sum = parser .parse(matches.get_one::("DURATION1").unwrap()) .unwrap() .saturating_add(parser.parse(arg2).unwrap()); println!("The sum of the two durations: {sum:?}"); } else { let duration = parser .parse(matches.get_one::("DURATION1").unwrap()) .unwrap(); println!("The duration is: {duration:?}"); } } fundu-1.0.0/examples/clap_derive.rs000064400000000000000000000022511046102023000154040ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! Command line application with clap_derive which prints the sum of two durations if two //! positional arguments were given or else just prints the parsed duration from the first //! positional argument. use clap::Parser; use fundu::DurationParser; #[derive(Parser)] #[clap(author, version, about, long_about = None, allow_negative_numbers = true)] struct Args { #[clap(value_name = "DURATION1")] duration_1: String, #[clap(value_name = "DURATION2")] duration_2: Option, } fn main() { let args = &Args::parse(); let parser = DurationParser::new(); match args.duration_2.as_deref() { Some(arg2) => { let sum = parser .parse(&args.duration_1) .unwrap() .saturating_add(parser.parse(arg2).unwrap()); // println!("The sum of the two durations: {sum:?}"); } None => { let duration = parser.parse(&args.duration_1).unwrap(); println!("The duration is: {duration:?}"); } } } fundu-1.0.0/examples/convert.rs000064400000000000000000000057071046102023000146200ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! fundu's Duration converted to std::time::Duration, chrono::Duration and time::Duration use clap::{command, Arg}; use fundu::{DurationParser, SaturatingInto}; const PARSER: DurationParser = DurationParser::builder() .all_time_units() .allow_negative() .build(); fn main() { let matches = command!() .about("Conversion of fundu's duration to other durations") .allow_negative_numbers(true) .allow_hyphen_values(true) .arg( Arg::new("DURATION") .action(clap::ArgAction::Set) .help("A duration"), ) .get_matches(); let input: &String = matches .get_one("DURATION") .expect("One argument must be present"); match PARSER.parse(input.trim()) { Ok(duration) => { println!("Input was: '{}'", input); println!("fundu::Duration: {:?}", duration); println!("fundu::Duration as human readable string: {}\n", duration); match std::time::Duration::try_from(duration) { Ok(duration) => { println!("Conversion to std::time::Duration: {:?}", duration) } Err(error) => println!( "Error converting to std::time::Duration: The original error message was: '{}'", error ), }; println!( "Saturating conversion with SaturatingInto::: {:?}\n", SaturatingInto::::saturating_into(duration) ); match chrono::Duration::try_from(duration) { Ok(duration) => { println!("Conversion to chrono::Duration: {:?}", duration) } Err(error) => println!( "Error converting to chrono::Duration: The original error message was: '{}'", error ), }; println!( "Saturating conversion with SaturatingInto::: {:?}\n", SaturatingInto::::saturating_into(duration) ); match time::Duration::try_from(duration) { Ok(duration) => { println!("Conversion to time::Duration: {:?}", duration) } Err(error) => println!( "Error converting to time::Duration: The original error message was: '{}'", error ), }; println!( "Saturating conversion with SaturatingInto::: {:?}", SaturatingInto::::saturating_into(duration) ); } Err(error) => println!("Error parsing duration '{}': {}", input, error), } } fundu-1.0.0/examples/custom.rs000064400000000000000000000104451046102023000144450ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! A simple calculator to calculate an infinite amount of durations collected from user provided //! input arguments to seconds. The default time unit is set to nano seconds and parsing an exponent //! is disabled. The default month and year calculation uses the Julian Year. For demonstration //! purposes, we use the sidereal year and month. For fun, there's also the `fortnight` as //! additional custom time unit defined. //! //! To reduce run-time costs a little bit, as much as possible is defined as global const. //! //! Here's some sample output of this example: //! //! ```text //! cargo run --example custom --features custom --no-default-features -- 10 100ns 1.09y 1.42mon 1week '1 fortnight' 1e20 -1.2 '1 µs' //! A simple calculator to calculate the input duration to seconds (Default time unit is the nano second): //! //! Input| Result in seconds //! --------------------|------------------------------- //! 10| 0.000000010 //! 100ns| 0.000000100 //! 1.09y| 34398382.959360000 //! 1.42mon| 3352039.944768000 //! 1week| 604800.000000000 //! 1 fortnight| 1209600.000000000 //! Error parsing '1e20': Syntax error: No exponent allowed at column 1 //! Error parsing '-1.2': Number was negative //! 1 µs| 0.000001000 //! ``` use clap::{command, Arg}; use fundu::TimeUnit::*; use fundu::{CustomDurationParser, CustomTimeUnit, Multiplier}; const CUSTOM_TIME_UNITS: [CustomTimeUnit; 11] = [ CustomTimeUnit::with_default(NanoSecond, &["ns", "nano", "nanos"]), CustomTimeUnit::with_default(MicroSecond, &["µs", "Ms", "micro", "micros"]), CustomTimeUnit::with_default(MilliSecond, &["ms", "milli", "millis"]), CustomTimeUnit::with_default(Second, &["s", "sec", "secs", "second", "seconds"]), CustomTimeUnit::with_default(Minute, &["m", "min", "mins", "minutes"]), CustomTimeUnit::with_default(Hour, &["h", "hr", "hrs", "hour", "hours"]), CustomTimeUnit::with_default(Day, &["d", "day", "days"]), CustomTimeUnit::with_default(Week, &["w", "week", "weeks"]), // The fortnight (=2 weeks) CustomTimeUnit::new( Week, &["f", "fortnight", "fortnights"], Some(Multiplier(2, 0)), ), // The sidereal month CustomTimeUnit::new( Second, &["M", "mon", "month", "months"], Some(Multiplier(236_059_151, -2)), ), // The sidereal year CustomTimeUnit::new( Second, &["y", "yr", "year", "years"], Some(Multiplier(3_155_814_976, -2)), ), ]; fn main() { let matches = command!() .allow_negative_numbers(true) .arg( Arg::new("DURATION") .action(clap::ArgAction::Append) .multiple_values(true), ) .get_matches(); let parser = CustomDurationParser::builder() .time_units(&CUSTOM_TIME_UNITS) .default_unit(NanoSecond) .disable_exponent() .allow_delimiter(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) .build(); // The headline println!( "A simple calculator to calculate the input duration to seconds (Default time unit is the \ nano second):\n" ); // The table header println!("{:>20}|{:>31}", "Input", "Result in seconds"); // The table separator from the content println!("{}|{}", "-".repeat(20), "-".repeat(31)); // Now let's parse and print the output for input in matches .get_many("DURATION") .expect("At least one argument must be present") .cloned() .collect::>() { match parser.parse(input.trim()) { Ok(duration) => { let duration: std::time::Duration = duration.try_into().unwrap(); println!( "{:>20}|{:21}.{:09}", &input, duration.as_secs(), duration.subsec_nanos() ) } Err(error) => eprintln!("Error parsing '{}': {}", &input, error), } } } fundu-1.0.0/examples/gnu.rs000064400000000000000000000130431046102023000137210ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! A gnu relative time parser as specified here //! `https://www.gnu.org/software/coreutils/manual/html_node/Relative-items-in-date-strings.html`. use clap::{command, Arg}; use fundu::TimeUnit::*; use fundu::{ CustomDurationParserBuilder, CustomTimeUnit, Duration, Multiplier, SaturatingInto, TimeKeyword, }; const GNU_TIME_UNITS: [CustomTimeUnit<'static>; 8] = [ CustomTimeUnit::with_default(Second, &["sec", "secs", "second", "seconds"]), CustomTimeUnit::with_default(Minute, &["min", "mins", "minute", "minutes"]), CustomTimeUnit::with_default(Hour, &["hour", "hours"]), CustomTimeUnit::with_default(Day, &["day", "days"]), CustomTimeUnit::with_default(Week, &["week", "weeks"]), CustomTimeUnit::new(Week, &["fortnight", "fortnights"], Some(Multiplier(2, 0))), CustomTimeUnit::with_default(Month, &["month", "months"]), CustomTimeUnit::with_default(Year, &["year", "years"]), ]; const GNU_KEYWORDS: [TimeKeyword<'static>; 3] = [ TimeKeyword::new(Day, &["yesterday"], Some(Multiplier(-1, 0))), TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0))), TimeKeyword::new(Day, &["now", "today"], Some(Multiplier(0, 0))), ]; // Whitespace as defined in the POSIX locale (and used by gnu). In contrast to rust's definition of // whitespace the POSIX definition includes the VERTICAL TAB: // SPACE, HORIZONTAL TAB, LINE FEED, FORM FEED, CARRIAGE RETURN, VERTICAL TAB const DELIMITER: fn(u8) -> bool = |byte| matches!(byte, b' ' | b'\t' | b'\n' | b'\x0c' | b'\r' | b'\x0b'); const PARSER_BUILDER: CustomDurationParserBuilder = CustomDurationParserBuilder::new() .allow_ago(DELIMITER) .allow_delimiter(DELIMITER) .allow_negative() .disable_exponent() .disable_fraction() .disable_infinity() .number_is_optional() .parse_multiple(DELIMITER, None); fn make_plural(time: u64, singular: &str) -> String { if time > 1 { format!("{}s", singular) } else { singular.to_string() } } /// Create a human readable string like `100years 2hours 1min 40secs` from a `Duration` fn make_human(duration: Duration) -> String { const YEAR: u64 = Year.multiplier().0.unsigned_abs(); const MONTH: u64 = Month.multiplier().0.unsigned_abs(); const WEEK: u64 = Week.multiplier().0.unsigned_abs(); const DAY: u64 = Day.multiplier().0.unsigned_abs(); const HOUR: u64 = Hour.multiplier().0.unsigned_abs(); const MINUTE: u64 = Minute.multiplier().0.unsigned_abs(); if duration.is_zero() { return "0sec".to_string(); } let std_duration: std::time::Duration = duration.abs().saturating_into(); let mut result = Vec::with_capacity(10); let mut secs = std_duration.as_secs(); if secs > 0 { if secs >= YEAR { let years = secs / YEAR; result.push(format!("{}{}", years, make_plural(years, "year"))); secs %= YEAR; } if secs >= MONTH { let months = secs / MONTH; result.push(format!("{}{}", months, make_plural(months, "month"))); secs %= MONTH; } if secs >= WEEK { let weeks = secs / WEEK; result.push(format!("{}{}", weeks, make_plural(weeks, "week"))); secs %= WEEK; } if secs >= DAY { let days = secs / DAY; result.push(format!("{}{}", days, make_plural(days, "day"))); secs %= DAY; } if secs >= HOUR { let hours = secs / HOUR; result.push(format!("{}{}", hours, make_plural(hours, "hour"))); secs %= HOUR; } if secs >= MINUTE { let minutes = secs / MINUTE; result.push(format!("{}{}", minutes, make_plural(minutes, "min"))); secs %= MINUTE; } if secs >= 1 { result.push(format!("{}{}", secs, make_plural(secs, "sec"))); } } if duration.is_negative() { format!("-{}", &result.join(" -")) } else { result.join(" ") } } fn main() { let matches = command!() .about( "A gnu relative time parser as specified in `info '(coreutils) Relative items in \ date'`.", ) .allow_negative_numbers(true) .allow_hyphen_values(true) .arg( Arg::new("GNU_RELATIVE_TIME") .action(clap::ArgAction::Set) .help( "A relative time as specified in `info '(coreutils) Relative items in date \ strings'`", ), ) .get_matches(); let parser = PARSER_BUILDER .time_units(&GNU_TIME_UNITS) .keywords(&GNU_KEYWORDS) .build(); let input: &String = matches .get_one("GNU_RELATIVE_TIME") .expect("One argument must be present"); match parser.parse(input.trim()) { Ok(duration) => { let std_duration: std::time::Duration = duration.abs().saturating_into(); println!("{:>8}: {}", "Original", input); println!( "{:>8}: {}", "Seconds", if duration.is_negative() { format!("-{}", std_duration.as_secs()) } else { std_duration.as_secs().to_string() } ); println!("{:>8}: {}", "Human", make_human(duration)); } Err(error) => eprintln!("Failed to parse relative time '{}': {}", &input, error), } } fundu-1.0.0/examples/simple.rs000064400000000000000000000006341046102023000144230ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use fundu::{Duration, DurationParser, TimeUnit}; fn main() { let input = "100y"; let duration = DurationParser::with_time_units(&[TimeUnit::Year]) .parse(input) .unwrap(); assert_eq!(duration, Duration::positive(3_155_760_000, 0)) } fundu-1.0.0/examples/systemd.rs000064400000000000000000000074531046102023000146300ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! A systemd time span parser as specified in `man systemd.time`. The output of this example is //! imitating the output of `systemd-analyze timespan SYSTEMD_TIME_SPAN` use std::time::Duration; use clap::{command, Arg}; use fundu::TimeUnit::*; use fundu::{CustomDurationParser, SYSTEMD_TIME_UNITS}; /// Create a human readable string like `100y 2h 46min 40s 123us 456ns` from a `Duration` fn make_human(duration: Duration) -> String { const YEAR: u64 = Year.multiplier().0.unsigned_abs(); const MONTH: u64 = Month.multiplier().0.unsigned_abs(); const WEEK: u64 = Week.multiplier().0.unsigned_abs(); const DAY: u64 = Day.multiplier().0.unsigned_abs(); const HOUR: u64 = Hour.multiplier().0.unsigned_abs(); const MINUTE: u64 = Minute.multiplier().0.unsigned_abs(); const MILLIS_PER_NANO: u32 = 1_000_000; const MICROS_PER_NANO: u32 = 1_000; if duration.is_zero() { return "0".to_string(); } let mut result = Vec::with_capacity(10); let mut secs = duration.as_secs(); if secs > 0 { if secs >= YEAR { result.push(format!("{}y", secs / YEAR)); secs %= YEAR; } if secs >= MONTH { result.push(format!("{}month", secs / MONTH)); secs %= MONTH; } if secs >= WEEK { result.push(format!("{}w", secs / WEEK)); secs %= WEEK; } if secs >= DAY { result.push(format!("{}d", secs / DAY)); secs %= DAY; } if secs >= HOUR { result.push(format!("{}h", secs / HOUR)); secs %= HOUR; } if secs >= MINUTE { result.push(format!("{}min", secs / MINUTE)); secs %= MINUTE; } if secs >= 1 { result.push(format!("{}s", secs)); } } let mut nanos = duration.subsec_nanos(); if nanos > 0 { if nanos >= MILLIS_PER_NANO { result.push(format!("{}ms", nanos / MILLIS_PER_NANO)); nanos %= MILLIS_PER_NANO; } if nanos >= MICROS_PER_NANO { result.push(format!("{}us", nanos / MICROS_PER_NANO)); nanos %= MICROS_PER_NANO; } if nanos >= 1 { result.push(format!("{}ns", nanos)); } } result.join(" ") } fn main() { let matches = command!() .about( "A systemd time span parser as specified in `man systemd.time`. The output of this \ example is imitating the output of `systemd-analyze timespan SYSTEMD_TIME_SPAN`", ) .allow_negative_numbers(true) .arg( Arg::new("SYSTEMD_TIME_SPAN") .action(clap::ArgAction::Set) .help("A time span as specified in `man systemd.time`"), ) .get_matches(); let delimiter = |byte| matches!(byte, b' ' | b'\t' | b'\n' | b'\r'); let parser = CustomDurationParser::builder() .time_units(&SYSTEMD_TIME_UNITS) .disable_exponent() .disable_fraction() .disable_infinity() .allow_delimiter(delimiter) .parse_multiple(delimiter, None) .build(); let input: &String = matches .get_one("SYSTEMD_TIME_SPAN") .expect("At least one argument must be present"); match parser.parse(input.trim()) { Ok(duration) => { let duration: std::time::Duration = duration.try_into().unwrap(); println!("{:>8}: {}", "Original", input); println!("{:>8}: {}", "μs", duration.as_micros()); println!("{:>8}: {}", "Human", make_human(duration)); } Err(error) => eprintln!("Failed to parse time span '{}': {}", &input, error), } } fundu-1.0.0/rustfmt.toml000064400000000000000000000006561046102023000133530ustar 00000000000000newline_style = "unix" use_field_init_shorthand = true use_try_shorthand = true # Unstable features below unstable_features = true version = "Two" comment_width = 100 error_on_line_overflow = true format_code_in_doc_comments = true format_macro_bodies = true format_macro_matchers = true format_strings = true imports_granularity = "Module" group_imports = "StdExternalCrate" normalize_doc_attributes = true wrap_comments = true fundu-1.0.0/src/config.rs000064400000000000000000000066031046102023000133520ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use crate::time::{Multiplier, DEFAULT_TIME_UNIT}; use crate::TimeUnit; pub(crate) const DEFAULT_CONFIG: Config = Config::new(); /// An ascii delimiter defined as closure. /// /// The [`Delimiter`] is a type alias for a closure taking a `u8` byte and returning a `bool`. Most /// likely, the [`Delimiter`] is used to define some whitespace but whitespace definitions differ, /// so a closure provides the most flexible definition of a delimiter. For example the definition of /// whitespace from rust [`u8::is_ascii_whitespace`]: /// /// ```text /// Checks if the value is an ASCII whitespace character: U+0020 SPACE, U+0009 HORIZONTAL TAB, /// U+000A LINE FEED, U+000C FORM FEED, or U+000D CARRIAGE RETURN. /// /// Rust uses the WhatWG Infra Standard’s definition of ASCII whitespace. There are several other /// definitions in wide use. For instance, the POSIX locale includes U+000B VERTICAL TAB as well /// as all the above characters, but—from the very same specification—the default rule for “field /// splitting” in the Bourne shell considers only SPACE, HORIZONTAL TAB, and LINE FEED as /// whitespace. /// ``` /// /// # Problems /// /// The delimiter takes a `u8` as input, but matching any non-ascii (`0x80 - 0xff`) bytes may lead /// to a [`crate::ParseError`] if the input string contains multi-byte utf-8 characters. /// /// # Examples /// /// ```rust /// use fundu::Delimiter; /// /// fn is_delimiter(delimiter: Delimiter, byte: u8) -> bool { /// delimiter(byte) /// } /// /// assert!(is_delimiter( /// |byte| matches!(byte, b' ' | b'\n' | b'\t'), /// b' ' /// )); /// assert!(!is_delimiter( /// |byte| matches!(byte, b' ' | b'\n' | b'\t'), /// b'\r' /// )); /// assert!(is_delimiter(|byte| byte.is_ascii_whitespace(), b'\r')); /// ``` pub type Delimiter = fn(u8) -> bool; #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) struct Config<'a> { pub(crate) allow_delimiter: Option, pub(crate) default_unit: TimeUnit, pub(crate) default_multiplier: Multiplier, pub(crate) disable_exponent: bool, pub(crate) disable_fraction: bool, pub(crate) disable_infinity: bool, pub(crate) number_is_optional: bool, pub(crate) max_exponent: i16, pub(crate) min_exponent: i16, pub(crate) parse_multiple_delimiter: Option, pub(crate) parse_multiple_conjunctions: Option<&'a [&'a str]>, pub(crate) allow_negative: bool, pub(crate) allow_ago: Option, } impl<'a> Default for Config<'a> { fn default() -> Self { Self::new() } } impl<'a> Config<'a> { pub(crate) const fn new() -> Self { Self { allow_delimiter: None, default_unit: DEFAULT_TIME_UNIT, default_multiplier: Multiplier(1, 0), disable_exponent: false, disable_fraction: false, number_is_optional: false, max_exponent: i16::MAX, min_exponent: i16::MIN, disable_infinity: false, parse_multiple_delimiter: None, parse_multiple_conjunctions: None, allow_negative: false, allow_ago: None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_for_config() { assert_eq!(Config::default(), Config::new()); } } fundu-1.0.0/src/custom/builder.rs000064400000000000000000000556221046102023000150520ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use super::time_units::{CustomTimeUnits, TimeKeyword}; use crate::config::{Config, DEFAULT_CONFIG}; use crate::parse::Parser; use crate::{CustomDurationParser, CustomTimeUnit, Delimiter, TimeUnit}; /// Like [`crate::DurationParserBuilder`] for [`crate::DurationParser`], this is a builder for a /// [`CustomDurationParser`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomDurationParserBuilder, CustomTimeUnit, Duration}; /// /// let parser = CustomDurationParserBuilder::new() /// .time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]) /// .default_unit(MicroSecond) /// .allow_delimiter(|byte| byte == b' ') /// .build(); /// /// assert_eq!(parser.parse("1 ns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// /// // instead of /// /// let mut parser = /// CustomDurationParser::with_time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]); /// parser /// .default_unit(MicroSecond) /// .allow_delimiter(Some(|byte| byte == b' ')); /// /// assert_eq!(parser.parse("1 ns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// ``` #[derive(Debug, PartialEq, Eq)] pub struct CustomDurationParserBuilder<'a> { config: Config<'a>, time_units: Vec>, keywords: Vec>, } impl<'a> Default for CustomDurationParserBuilder<'a> { /// Construct a new [`CustomDurationParserBuilder`] without any time units. fn default() -> Self { Self::new() } } impl<'a> CustomDurationParserBuilder<'a> { /// Construct a new [`CustomDurationParserBuilder`]. /// /// Per default there are no time units configured in the builder. Use for example /// [`CustomDurationParserBuilder::time_units`] to add new time units. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParser, CustomDurationParserBuilder}; /// /// assert_eq!( /// CustomDurationParserBuilder::new().build(), /// CustomDurationParser::new() /// ); /// ``` pub const fn new() -> Self { Self { config: DEFAULT_CONFIG, time_units: vec![], keywords: vec![], } } /// Add a [`CustomTimeUnit`] to the current set of time units. /// /// See also [`CustomDurationParser::time_unit`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Duration, Multiplier}; /// /// let parser = CustomDurationParserBuilder::new() /// .time_unit(CustomTimeUnit::new( /// Week, /// &["fortnight", "fortnights"], /// Some(Multiplier(2, 0)), /// )) /// .build(); /// assert_eq!( /// parser.parse("1fortnight").unwrap(), /// Duration::positive(2 * 7 * 24 * 60 * 60, 0), /// ); /// ``` pub fn time_unit(mut self, time_unit: CustomTimeUnit<'a>) -> Self { self.time_units.push(time_unit); self } /// Add multiple [`CustomTimeUnit`]s to the current set of time units /// /// # Example /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Duration, Multiplier}; /// /// const CUSTOM_TIME_UNITS: [CustomTimeUnit; 2] = [ /// CustomTimeUnit::new(Week, &["fortnight", "fortnights"], Some(Multiplier(2, 0))), /// CustomTimeUnit::new(Second, &["shake", "shakes"], Some(Multiplier(1, -8))), /// ]; /// /// let parser = CustomDurationParserBuilder::new() /// .time_units(&CUSTOM_TIME_UNITS) /// .build(); /// /// assert_eq!( /// parser.parse("1fortnight").unwrap(), /// Duration::positive(2 * 7 * 24 * 60 * 60, 0), /// ); /// ``` pub fn time_units(mut self, time_units: &[CustomTimeUnit<'a>]) -> Self { self.time_units.reserve_exact(time_units.len()); for unit in time_units { self.time_units.push(*unit); } self } /// Add a [`TimeKeyword`] to the current set of keywords /// /// See also [`CustomDurationParser::keyword`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{ /// CustomDurationParserBuilder, CustomTimeUnit, Duration, Multiplier, ParseError, TimeKeyword, /// }; /// /// let parser = CustomDurationParserBuilder::new() /// .time_unit(CustomTimeUnit::with_default(NanoSecond, &["ns"])) /// .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) /// .build(); /// /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// assert_eq!( /// parser.parse("tomorrow"), /// Ok(Duration::positive(60 * 60 * 24, 0)) /// ); /// /// // but not /// assert_eq!( /// parser.parse("123tomorrow"), /// Err(ParseError::TimeUnit( /// 3, /// "Invalid time unit: 'tomorrow'".to_string() /// )) /// ); /// ``` pub fn keyword(mut self, keyword: TimeKeyword<'a>) -> Self { self.keywords.push(keyword); self } /// Add multiple [`TimeKeyword`]s to the current set of keywords /// /// See also [`CustomDurationParser::keywords`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, Duration, Multiplier, ParseError, TimeKeyword}; /// /// let mut parser = CustomDurationParserBuilder::new() /// .allow_negative() /// .keywords(&[ /// TimeKeyword::new(Day, &["yesterday"], Some(Multiplier(-1, 0))), /// TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0))), /// ]) /// .build(); /// /// assert_eq!( /// parser.parse("yesterday"), /// Ok(Duration::negative(60 * 60 * 24, 0)) /// ); /// assert_eq!( /// parser.parse("tomorrow"), /// Ok(Duration::positive(60 * 60 * 24, 0)) /// ); /// ``` pub fn keywords(mut self, keywords: &[TimeKeyword<'a>]) -> Self { self.keywords.reserve_exact(keywords.len()); for keyword in keywords { self.keywords.push(*keyword); } self } /// Set the default time unit to a [`TimeUnit`] different from [`TimeUnit::Second`] /// /// See also [`crate::DurationParser::default_unit`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, Duration}; /// /// assert_eq!( /// CustomDurationParserBuilder::new() /// .default_unit(NanoSecond) /// .build() /// .parse("42") /// .unwrap(), /// Duration::positive(0, 42) /// ); /// ``` pub const fn default_unit(mut self, unit: TimeUnit) -> Self { self.config.default_unit = unit; self } /// Allow one or more delimiters between the number and the [`TimeUnit`]. /// /// See also [`crate::DurationParser::allow_delimiter`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Duration}; /// /// let parser = CustomDurationParserBuilder::new() /// .time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]) /// .allow_delimiter(|byte| byte == b' ') /// .build(); /// /// assert_eq!(parser.parse("123 ns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123 ns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// ``` pub const fn allow_delimiter(mut self, delimiter: Delimiter) -> Self { self.config.allow_delimiter = Some(delimiter); self } /// If set, parsing negative durations is possible /// /// See also [`crate::DurationParser::allow_negative`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Duration, Multiplier, TimeKeyword}; /// /// let parser = CustomDurationParserBuilder::new() /// .allow_negative() /// .time_units(&[ /// CustomTimeUnit::with_default(NanoSecond, &["ns"]), /// CustomTimeUnit::new(Second, &["neg"], Some(Multiplier(-1, 0))), /// ]) /// .keyword(TimeKeyword::new( /// Day, /// &["yesterday"], /// Some(Multiplier(-1, 0)), /// )) /// .build(); /// /// assert_eq!(parser.parse("-123ns"), Ok(Duration::negative(0, 123))); /// assert_eq!(parser.parse("1.23e-7neg"), Ok(Duration::negative(0, 123))); /// assert_eq!( /// parser.parse("yesterday"), /// Ok(Duration::negative(60 * 60 * 24, 0)) /// ); /// ``` pub const fn allow_negative(mut self) -> Self { self.config.allow_negative = true; self } /// If set, the `ago` keyword can be appended to a time unit to denote a negative duration /// /// See also [`CustomDurationParser::allow_ago`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Duration, Multiplier, TimeKeyword}; /// /// let parser = CustomDurationParserBuilder::new() /// .allow_ago(|byte| byte.is_ascii_whitespace()) /// .time_units(&[ /// CustomTimeUnit::with_default(NanoSecond, &["ns"]), /// CustomTimeUnit::new(Second, &["neg"], Some(Multiplier(-1, 0))), /// ]) /// .keyword(TimeKeyword::new( /// Day, /// &["yesterday"], /// Some(Multiplier(-1, 0)), /// )) /// .build(); /// /// assert_eq!(parser.parse("123ns ago"), Ok(Duration::negative(0, 123))); /// assert_eq!(parser.parse("-123ns ago"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123neg ago"), Ok(Duration::positive(123, 0))); /// ``` /// /// And some illegal usages of `ago` /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Multiplier, ParseError, TimeKeyword}; /// /// let parser = CustomDurationParserBuilder::new() /// .allow_ago(|byte| byte.is_ascii_whitespace()) /// .time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]) /// .keyword(TimeKeyword::new( /// Day, /// &["yesterday"], /// Some(Multiplier(-1, 0)), /// )) /// .build(); /// /// // Error because no time unit was specified /// assert_eq!( /// parser.parse("123 ago"), /// Err(ParseError::Syntax( /// 3, /// "Expected end of input but found: ' '".to_string() /// )) /// ); /// /// // Error because ago was specified multiple times /// assert_eq!( /// parser.parse("123ns ago ago"), /// Err(ParseError::Syntax( /// 9, /// "Expected end of input but found: ' '".to_string() /// )) /// ); /// /// // Error because `yesterday` is a [`TimeKeyword`] /// assert_eq!( /// parser.parse("yesterday ago"), /// Err(ParseError::Syntax( /// 0, /// "Invalid input: 'yesterday ago'".to_string() /// )) /// ); /// ``` pub const fn allow_ago(mut self, delimiter: Delimiter) -> Self { self.config.allow_ago = Some(delimiter); self.config.allow_negative = true; self } /// Disable parsing an exponent. /// /// See also [`crate::DurationParser::disable_exponent`]. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParserBuilder, ParseError}; /// /// assert_eq!( /// CustomDurationParserBuilder::new() /// .disable_exponent() /// .build() /// .parse("123e+1"), /// Err(ParseError::Syntax(3, "No exponent allowed".to_string())) /// ); /// ``` pub const fn disable_exponent(mut self) -> Self { self.config.disable_exponent = true; self } /// Disable parsing a fraction in the number part of the source string. /// /// See also [`crate::DurationParser::disable_fraction`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Duration, ParseError}; /// /// let parser = CustomDurationParserBuilder::new() /// .time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]) /// .disable_fraction() /// .build(); /// /// assert_eq!( /// parser.parse("123.456"), /// Err(ParseError::Syntax(3, "No fraction allowed".to_string())) /// ); /// /// assert_eq!( /// parser.parse("123e-2"), /// Ok(Duration::positive(1, 230_000_000)) /// ); /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// ``` pub const fn disable_fraction(mut self) -> Self { self.config.disable_fraction = true; self } /// Disable parsing infinity values /// /// See also [`crate::DurationParser::disable_infinity`] /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParserBuilder, ParseError}; /// /// let parser = CustomDurationParserBuilder::new() /// .disable_infinity() /// .build(); /// /// assert_eq!( /// parser.parse("inf"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'inf'"))) /// ); /// assert_eq!( /// parser.parse("infinity"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'infinity'"))) /// ); /// assert_eq!( /// parser.parse("+inf"), /// Err(ParseError::Syntax(1, format!("Invalid input: 'inf'"))) /// ); /// ``` pub const fn disable_infinity(mut self) -> Self { self.config.disable_infinity = true; self } /// This setting makes a number in the source string optional. /// /// See also [`crate::DurationParser::number_is_optional`]. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParserBuilder, Duration, DEFAULT_TIME_UNITS}; /// /// let parser = CustomDurationParserBuilder::new() /// .time_units(&DEFAULT_TIME_UNITS) /// .number_is_optional() /// .build(); /// /// assert_eq!(parser.parse("ns"), Ok(Duration::positive(0, 1))); /// assert_eq!(parser.parse(".001e-6s"), Ok(Duration::positive(0, 1))); /// assert_eq!(parser.parse("+ns"), Ok(Duration::positive(0, 1))); /// ``` pub const fn number_is_optional(mut self) -> Self { self.config.number_is_optional = true; self } /// Parse possibly multiple durations and sum them up. /// /// The durations can be separated from each other by a [`Delimiter`] or one or more /// conjunctions, like `and`. See also [`crate::DurationParser::parse_multiple`]. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParserBuilder, Duration, DEFAULT_TIME_UNITS}; /// /// let parser = CustomDurationParserBuilder::new() /// .time_units(&DEFAULT_TIME_UNITS) /// .parse_multiple(|byte| matches!(byte, b' ' | b'\t'), Some(&["and"])) /// .build(); /// /// assert_eq!( /// parser.parse("1.5h 2e+2ns"), /// Ok(Duration::positive(5400, 200)) /// ); /// assert_eq!( /// parser.parse("55s500ms"), /// Ok(Duration::positive(55, 500_000_000)) /// ); /// assert_eq!(parser.parse("1m and 1ns"), Ok(Duration::positive(60, 1))); /// assert_eq!( /// parser.parse("1. .1"), /// Ok(Duration::positive(1, 100_000_000)) /// ); /// assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0))); /// assert_eq!( /// parser.parse("300ms20s 5d"), /// Ok(Duration::positive(5 * 60 * 60 * 24 + 20, 300_000_000)) /// ); /// ``` pub const fn parse_multiple( mut self, delimiter: Delimiter, conjunctions: Option<&'a [&'a str]>, ) -> Self { self.config.parse_multiple_delimiter = Some(delimiter); self.config.parse_multiple_conjunctions = conjunctions; self } /// Build the [`CustomDurationParser`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParserBuilder, CustomTimeUnit, Duration}; /// /// let parser = CustomDurationParserBuilder::new() /// .time_units(&[ /// CustomTimeUnit::with_default(Minute, &["min"]), /// CustomTimeUnit::with_default(Hour, &["h", "hr"]), /// ]) /// .allow_delimiter(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) /// .build(); /// /// for input in &["60 min", "1h", "1\t\n hr"] { /// assert_eq!(parser.parse(input).unwrap(), Duration::positive(60 * 60, 0)); /// } /// ``` pub fn build(self) -> CustomDurationParser<'a> { CustomDurationParser { time_units: CustomTimeUnits::with_time_units(&self.time_units), inner: Parser::with_config(self.config), keywords: CustomTimeUnits::with_keywords(&self.keywords), } } } #[cfg(test)] mod tests { use super::*; use crate::config::Config; use crate::time::TimeUnitsLike; use crate::TimeUnit::*; use crate::{CustomTimeUnit, Multiplier}; #[test] fn test_custom_duration_parser_builder_when_default() { assert_eq!( CustomDurationParserBuilder::default(), CustomDurationParserBuilder::new() ); } #[test] fn test_custom_duration_parser_builder_when_new() { let builder = CustomDurationParserBuilder::new(); assert_eq!(builder.config, Config::new()); assert!(builder.time_units.is_empty()); } #[test] fn test_custom_duration_parser_builder_when_default_unit() { let mut expected = Config::new(); expected.default_unit = MicroSecond; let builder = CustomDurationParserBuilder::new().default_unit(MicroSecond); assert_eq!(builder.config, expected); } #[test] fn test_custom_duration_parser_builder_when_allow_delimiter() { let builder = CustomDurationParserBuilder::new().allow_delimiter(|byte| byte == b' '); assert!(builder.config.allow_delimiter.unwrap()(b' ')); } #[test] fn test_custom_duration_parser_builder_when_disable_exponent() { let mut expected = Config::new(); expected.disable_exponent = true; let builder = CustomDurationParserBuilder::new().disable_exponent(); assert_eq!(builder.config, expected); } #[test] fn test_custom_duration_parser_builder_when_disable_fraction() { let mut expected = Config::new(); expected.disable_fraction = true; let builder = CustomDurationParserBuilder::new().disable_fraction(); assert_eq!(builder.config, expected); } #[test] fn test_custom_duration_parser_builder_when_disable_infinity() { let mut expected = Config::new(); expected.disable_infinity = true; let builder = CustomDurationParserBuilder::new().disable_infinity(); assert_eq!(builder.config, expected); } #[test] fn test_custom_duration_parser_builder_when_number_is_optional() { let mut expected = Config::new(); expected.number_is_optional = true; let builder = CustomDurationParserBuilder::new().number_is_optional(); assert_eq!(builder.config, expected); } #[test] fn test_custom_duration_parser_builder_when_parse_multiple() { let builder = CustomDurationParserBuilder::new().parse_multiple(|byte| byte == 0xff, None); assert!(builder.config.parse_multiple_delimiter.unwrap()(0xff)); } #[test] fn test_custom_duration_parser_builder_when_build_with_regular_time_unit() { let mut expected = Config::new(); expected.number_is_optional = true; let parser = CustomDurationParserBuilder::new() .time_unit(CustomTimeUnit::with_default(Second, &["s", "secs"])) .time_unit(CustomTimeUnit::new(Hour, &["h"], Some(Multiplier(3, 0)))) .time_units(&[ CustomTimeUnit::new(Minute, &["m", "min"], Some(Multiplier(2, 0))), CustomTimeUnit::new(Day, &["d"], Some(Multiplier(4, 0))), ]) .number_is_optional() .build(); assert!(!parser.is_empty()); assert_eq!( parser.get_time_unit_by_id("s"), Some((Second, Multiplier(1, 0))) ); assert_eq!( parser.get_time_unit_by_id("secs"), Some((Second, Multiplier(1, 0))) ); assert_eq!( parser.get_time_unit_by_id("h"), Some((Hour, Multiplier(3, 0))) ); assert_eq!( parser.get_time_unit_by_id("m"), Some((Minute, Multiplier(2, 0))) ); assert_eq!( parser.get_time_unit_by_id("min"), Some((Minute, Multiplier(2, 0))) ); assert_eq!( parser.get_time_unit_by_id("d"), Some((Day, Multiplier(4, 0))) ); } #[test] fn test_custom_duration_parser_builder_when_build_without_regular_time_units() { let mut expected = Config::new(); expected.number_is_optional = true; let parser = CustomDurationParserBuilder::new() .time_units(&[ CustomTimeUnit::new(Minute, &["m", "min"], Some(Multiplier(2, 0))), CustomTimeUnit::new(Day, &["d"], Some(Multiplier(4, 0))), ]) .number_is_optional() .build(); assert!(!parser.is_empty()); assert_eq!( parser.get_time_unit_by_id("m"), Some((Minute, Multiplier(2, 0))) ); assert_eq!( parser.get_time_unit_by_id("min"), Some((Minute, Multiplier(2, 0))) ); assert_eq!( parser.get_time_unit_by_id("d"), Some((Day, Multiplier(4, 0))) ); } #[test] fn test_custom_duration_parser_builder_when_keywords() { let parser = CustomDurationParserBuilder::new() .keywords(&[ TimeKeyword::new(Second, &["sec"], None), TimeKeyword::new(Second, &["secs"], Some(Multiplier(2, 0))), ]) .build(); assert_eq!( parser.keywords.get("sec").unwrap(), (Second, Multiplier(1, 0)) ); assert_eq!( parser.keywords.get("secs").unwrap(), (Second, Multiplier(2, 0)) ); } } fundu-1.0.0/src/custom/mod.rs000064400000000000000000000003371046102023000141740ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT pub(crate) mod builder; pub(crate) mod parser; pub(crate) mod time_units; fundu-1.0.0/src/custom/parser.rs000064400000000000000000001003441046102023000147100ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use super::builder::CustomDurationParserBuilder; use super::time_units::{CustomTimeUnit, CustomTimeUnits, TimeKeyword}; use crate::parse::Parser; use crate::time::{Duration, Multiplier, TimeUnitsLike}; use crate::{Delimiter, ParseError, TimeUnit}; /// A parser with a customizable set of [`TimeUnit`]s and customizable identifiers. /// /// See also [`CustomDurationParser::with_time_units`]. /// /// # Problems /// /// It's possible to choose identifiers very freely as long as they are valid utf-8. However, some /// identifiers interact badly with the parser and may lead to problems if they start with: /// /// * `e` or `E` which is also indicating an exponent. If [`CustomDurationParser::disable_exponent`] /// is set to true this problem does not occur. /// * `inf` and in consequence `infinity` case-insensitive. These are reserved words as /// long as [`CustomDurationParser::disable_infinity`] isn't set to true. /// * ascii digits from `0` to `9` /// * `.` which is also indicating a fraction. If [`CustomDurationParser::disable_fraction`] is set /// to true, this problem does not occur /// * `+`, `-` which are in use for signs. /// * whitespace characters #[derive(Debug, PartialEq, Eq)] pub struct CustomDurationParser<'a> { pub(super) time_units: CustomTimeUnits<'a>, pub(super) keywords: CustomTimeUnits<'a>, pub(super) inner: Parser<'a>, } impl<'a> CustomDurationParser<'a> { /// Create a new empty [`CustomDurationParser`] without any time units. /// /// There's also [`CustomDurationParser::with_time_units`] which initializes the parser with a /// set of [`CustomTimeUnit`]s. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParser, Duration}; /// /// assert_eq!( /// CustomDurationParser::new().parse("100.0").unwrap(), /// Duration::positive(100, 0) /// ); /// ``` pub fn new() -> Self { Self { time_units: CustomTimeUnits::new(), keywords: CustomTimeUnits::new(), inner: Parser::new(), } } /// Create a new [`CustomDurationParser`] with an initial set of [`CustomTimeUnit`]s. /// /// Not all time units need to be defined, so if there is no intention to include a specific /// [`TimeUnit`] just leave it out of the `units`. Be aware, that this method does not check the /// validity of identifiers, so besides the need to be a valid `utf-8` sequence there are no /// other hard limitations but see also the `Problems` section in [`CustomDurationParser`]. /// There is also no check for duplicate `ids`, however [`CustomTimeUnit`]s with empty `ids` are /// ignored. Note the ids for time units are case sensitive. /// /// You may find it helpful to start with a pre-defined custom sets of [`TimeUnit`]: /// * [`crate::SYSTEMD_TIME_UNITS`]: This is the set of time units as specified in the [`systemd.time`](https://www.man7.org/linux/man-pages/man7/systemd.time.7.html) /// documentation /// * [`crate::DEFAULT_TIME_UNITS`]: This is the complete set of time units with their default /// ids as used the standard crate by [`crate::DurationParser`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier}; /// /// let parser = CustomDurationParser::with_time_units(&[ /// CustomTimeUnit::with_default(Second, &["s"]), /// CustomTimeUnit::with_default(Minute, &["Min"]), /// CustomTimeUnit::with_default(Hour, &["ώρα"]), /// ]); /// assert_eq!( /// parser.get_time_unit_by_id("s"), /// Some((Second, Multiplier(1, 0))) /// ); /// assert_eq!( /// parser.get_time_unit_by_id("Min"), /// Some((Minute, Multiplier(1, 0))) /// ); /// assert_eq!( /// parser.get_time_unit_by_id("ώρα"), /// Some((Hour, Multiplier(1, 0))) /// ); /// /// assert!(parser.parse("42.0min").is_err()); // Note the small letter `m` instead of `M` /// /// assert_eq!( /// parser.parse("42e-1ώρα").unwrap(), /// Duration::positive(15120, 0) /// ); /// ``` pub fn with_time_units(units: &[CustomTimeUnit<'a>]) -> Self { Self { time_units: CustomTimeUnits::with_time_units(units), keywords: CustomTimeUnits::new(), inner: Parser::new(), } } /// Use the [`CustomDurationParserBuilder`] to construct a [`CustomDurationParser`]. /// /// The [`CustomDurationParserBuilder`] is more ergonomic in some use cases than using /// [`CustomDurationParser`] directly. Using this method is the same like invoking /// [`CustomDurationParserBuilder::default`]. /// /// See [`CustomDurationParserBuilder`] for more details. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{Duration, DurationParser}; /// /// let parser = DurationParser::builder() /// .all_time_units() /// .default_unit(MicroSecond) /// .allow_delimiter(|b| b == b' ') /// .build(); /// /// assert_eq!(parser.parse("1 ns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// /// // instead of /// /// let mut parser = DurationParser::with_all_time_units(); /// parser /// .default_unit(MicroSecond) /// .allow_delimiter(Some(|b| b == b' ')); /// /// assert_eq!(parser.parse("1 ns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// ``` pub const fn builder() -> CustomDurationParserBuilder<'a> { CustomDurationParserBuilder::new() } /// Add a [`CustomTimeUnit`] to the current set of time units. /// /// [`CustomTimeUnit`]s have a base [`TimeUnit`] and a [`Multiplier`] in addition to their /// identifiers. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier}; /// /// let mut parser = CustomDurationParser::new(); /// parser /// .time_unit(CustomTimeUnit::new( /// Week, /// &["fortnight", "fortnights"], /// Some(Multiplier(2, 0)), /// )) /// .time_unit(CustomTimeUnit::new( /// Second, /// &["kilosecond", "kiloseconds", "kilos"], /// Some(Multiplier(1000, 0)), /// )) /// .time_unit(CustomTimeUnit::new( /// Second, /// &["shakes"], /// Some(Multiplier(1, -8)), /// )); /// assert_eq!( /// parser.parse("1fortnights").unwrap(), /// Duration::positive(2 * 7 * 24 * 60 * 60, 0), /// ); /// ``` /// /// The `base_unit` is only used to calculate the final duration and does not need to be /// unique in the set of time units. It's even possible to define an own time unit for /// example for a definition of a [`TimeUnit::Year`] either in addition or as a /// replacement of the year definition of this crate (Julian Year = `365.25` days). /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier, DEFAULT_TIME_UNITS}; /// /// let mut parser = CustomDurationParser::with_time_units(&DEFAULT_TIME_UNITS); /// /// // The common year is usually defined as 365 days instead of the Julian Year with `365.25` /// // days. /// /// parser.time_unit(CustomTimeUnit::new( /// Day, /// &["y", "year", "years"], /// Some(Multiplier(365, 0)), /// )); /// assert_eq!( /// parser.parse("1year").unwrap(), /// Duration::positive(365 * 24 * 60 * 60, 0), /// ); /// ``` pub fn time_unit(&mut self, time_unit: CustomTimeUnit<'a>) -> &mut Self { self.time_units.add_custom_time_unit(time_unit); self } /// Add multiple [`CustomTimeUnit`]s at once. /// /// See also [`CustomDurationParser::time_unit`] /// /// # Example /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier}; /// /// const CUSTOM_TIME_UNITS: [CustomTimeUnit; 2] = [ /// CustomTimeUnit::new(Week, &["fortnight", "fortnights"], Some(Multiplier(2, 0))), /// CustomTimeUnit::new(Second, &["shake", "shakes"], Some(Multiplier(1, -8))), /// ]; /// /// let mut parser = CustomDurationParser::new(); /// parser.time_units(&CUSTOM_TIME_UNITS); /// /// assert_eq!( /// parser.parse("1fortnight").unwrap(), /// Duration::positive(2 * 7 * 24 * 60 * 60, 0), /// ); /// ``` pub fn time_units(&mut self, time_units: &[CustomTimeUnit<'a>]) -> &mut Self { for time_unit in time_units { self.time_unit(*time_unit); } self } /// Add a [`TimeKeyword`] to the current set of keywords /// /// [`TimeKeyword`]s are almost the same like [`CustomTimeUnit`]s and can be defined in the same /// way like [`CustomTimeUnit`]s. However, they are parsed differently and don't accept a number /// in front of them. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{ /// CustomDurationParser, CustomTimeUnit, Duration, Multiplier, ParseError, TimeKeyword, /// }; /// /// let mut parser = /// CustomDurationParser::with_time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]); /// parser.keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))); /// /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// assert_eq!( /// parser.parse("tomorrow"), /// Ok(Duration::positive(60 * 60 * 24, 0)) /// ); /// /// // but not /// assert_eq!( /// parser.parse("123tomorrow"), /// Err(ParseError::TimeUnit( /// 3, /// "Invalid time unit: 'tomorrow'".to_string() /// )) /// ); /// ``` pub fn keyword(&mut self, keyword: TimeKeyword<'a>) -> &mut Self { self.keywords .add_custom_time_unit(keyword.to_custom_time_unit()); self } /// Add multiple [`TimeKeyword`]s to the current set of keywords /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, Duration, Multiplier, ParseError, TimeKeyword}; /// /// let mut parser = CustomDurationParser::new(); /// parser.allow_negative(true).keywords(&[ /// TimeKeyword::new(Day, &["yesterday"], Some(Multiplier(-1, 0))), /// TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0))), /// ]); /// /// assert_eq!( /// parser.parse("yesterday"), /// Ok(Duration::negative(60 * 60 * 24, 0)) /// ); /// assert_eq!( /// parser.parse("tomorrow"), /// Ok(Duration::positive(60 * 60 * 24, 0)) /// ); /// ``` pub fn keywords(&mut self, keywords: &[TimeKeyword<'a>]) -> &mut Self { for keyword in keywords { self.keyword(*keyword); } self } /// Parse the `source` string into a [`crate::Duration`]. /// /// See the [module level documentation](crate) for more information on the format. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParser, Duration}; /// /// assert_eq!( /// CustomDurationParser::new().parse("1.2e-1").unwrap(), /// Duration::positive(0, 120_000_000), /// ); /// ``` #[inline] pub fn parse(&self, source: &str) -> Result { self.inner.parse( source, &self.time_units, if self.keywords.is_empty() { None } else { Some(&self.keywords) }, ) } /// Set the default [`TimeUnit`] to `unit`. /// /// The default time unit is applied when no time unit was given in the input string. If the /// default time unit is not set with this method the parser defaults to [`TimeUnit::Second`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, Duration}; /// /// assert_eq!( /// CustomDurationParser::new() /// .default_unit(NanoSecond) /// .parse("42") /// .unwrap(), /// Duration::positive(0, 42) /// ); /// ``` pub fn default_unit(&mut self, unit: TimeUnit) -> &mut Self { self.inner.config.default_unit = unit; self } /// If `Some`, allow one or more [`Delimiter`] between the number and the [`TimeUnit`]. /// /// See also [`crate::DurationParser::allow_delimiter`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, ParseError}; /// /// let mut parser = /// CustomDurationParser::with_time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]); /// assert_eq!( /// parser.parse("123 ns"), /// Err(ParseError::TimeUnit( /// 3, /// "Invalid time unit: ' ns'".to_string() /// )) /// ); /// /// parser.allow_delimiter(Some(|byte| byte == b' ')); /// assert_eq!(parser.parse("123 ns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123 ns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// /// parser.allow_delimiter(Some(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' '))); /// assert_eq!(parser.parse("123\t\n\r ns"), Ok(Duration::positive(0, 123))); /// ``` pub fn allow_delimiter(&mut self, delimiter: Option) -> &mut Self { self.inner.config.allow_delimiter = delimiter; self } /// If true, parsing negative durations is possible /// /// This setting must be enabled if a [`CustomTimeUnit`] has a negative [`Multiplier`], a /// [`CustomDurationParser::keyword`] is negative or the [`CustomDurationParser::allow_ago`] /// setting is enabled. If `allow_negative` is disabled and a negative time unit, keyword etc. /// is encountered a [`ParseError::NegativeNumber`] is returned. /// /// See also [`crate::DurationParser::allow_negative`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier, TimeKeyword}; /// /// let mut parser = CustomDurationParser::with_time_units(&[ /// CustomTimeUnit::with_default(NanoSecond, &["ns"]), /// CustomTimeUnit::new(Second, &["neg"], Some(Multiplier(-1, 0))), /// ]); /// parser.allow_negative(true).keyword(TimeKeyword::new( /// Day, /// &["yesterday"], /// Some(Multiplier(-1, 0)), /// )); /// /// assert_eq!(parser.parse("-123ns"), Ok(Duration::negative(0, 123))); /// assert_eq!(parser.parse("1.23e-7neg"), Ok(Duration::negative(0, 123))); /// assert_eq!( /// parser.parse("yesterday"), /// Ok(Duration::negative(60 * 60 * 24, 0)) /// ); /// ``` pub fn allow_negative(&mut self, value: bool) -> &mut Self { self.inner.config.allow_negative = value; self } /// If the delimiter is set, the `ago` keyword can follow a time unit and a [`Delimiter`] to /// denote a negative duration /// /// The `ago` keyword is allowed in the source string after a time unit and only if a time unit /// was encountered. The time unit and `ago` must be delimited by the specified delimiter. In /// contrast to [`CustomTimeUnit`]'s, an encountered [`TimeKeyword`] doesn't allow for the `ago` /// keyword. Note that setting this option automatically sets /// [`CustomDurationParser::allow_negative`] to true. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier, TimeKeyword}; /// /// let mut parser = CustomDurationParser::with_time_units(&[ /// CustomTimeUnit::with_default(NanoSecond, &["ns"]), /// CustomTimeUnit::new(Second, &["neg"], Some(Multiplier(-1, 0))), /// ]); /// parser /// .allow_ago(Some(|byte: u8| byte.is_ascii_whitespace())) /// .keyword(TimeKeyword::new( /// Day, /// &["yesterday"], /// Some(Multiplier(-1, 0)), /// )); /// /// assert_eq!(parser.parse("123ns ago"), Ok(Duration::negative(0, 123))); /// assert_eq!(parser.parse("-123ns ago"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123neg ago"), Ok(Duration::positive(123, 0))); /// ``` /// /// And some illegal usages of `ago` /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Multiplier, ParseError, TimeKeyword}; /// /// let mut parser = /// CustomDurationParser::with_time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]); /// parser /// .allow_ago(Some(|byte: u8| byte.is_ascii_whitespace())) /// .keyword(TimeKeyword::new( /// Day, /// &["yesterday"], /// Some(Multiplier(-1, 0)), /// )); /// /// // Error because no time unit was specified /// assert_eq!( /// parser.parse("123 ago"), /// Err(ParseError::Syntax( /// 3, /// "Expected end of input but found: ' '".to_string() /// )) /// ); /// /// // Error because ago was specified multiple times /// assert_eq!( /// parser.parse("123ns ago ago"), /// Err(ParseError::Syntax( /// 9, /// "Expected end of input but found: ' '".to_string() /// )) /// ); /// /// // Error because `yesterday` is a [`TimeKeyword`] /// assert_eq!( /// parser.parse("yesterday ago"), /// Err(ParseError::Syntax( /// 0, /// "Invalid input: 'yesterday ago'".to_string() /// )) /// ); /// ``` pub fn allow_ago(&mut self, delimiter: Option) -> &mut Self { self.inner.config.allow_ago = delimiter; if delimiter.is_some() { self.inner.config.allow_negative = true; } self } /// Disable parsing the exponent. /// /// See also [`crate::DurationParser::disable_exponent`]. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParser, ParseError, DEFAULT_TIME_UNITS}; /// /// let mut parser = CustomDurationParser::with_time_units(&DEFAULT_TIME_UNITS); /// parser.disable_exponent(true); /// assert_eq!( /// parser.parse("123e+1"), /// Err(ParseError::Syntax(3, "No exponent allowed".to_string())) /// ); /// ``` pub fn disable_exponent(&mut self, value: bool) -> &mut Self { self.inner.config.disable_exponent = value; self } /// Disable parsing a fraction in the source string. /// /// See also [`crate::DurationParser::disable_fraction`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, ParseError}; /// /// let mut parser = /// CustomDurationParser::with_time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]); /// parser.disable_fraction(true); /// /// assert_eq!( /// parser.parse("123.456"), /// Err(ParseError::Syntax(3, "No fraction allowed".to_string())) /// ); /// /// assert_eq!( /// parser.parse("123e-2"), /// Ok(Duration::positive(1, 230_000_000)) /// ); /// /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// ``` pub fn disable_fraction(&mut self, value: bool) -> &mut Self { self.inner.config.disable_fraction = value; self } /// If true, disable parsing infinity /// /// See also [`crate::DurationParser::disable_infinity`]. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParser, ParseError}; /// /// let mut parser = CustomDurationParser::new(); /// parser.disable_infinity(true); /// /// assert_eq!( /// parser.parse("inf"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'inf'"))) /// ); /// assert_eq!( /// parser.parse("infinity"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'infinity'"))) /// ); /// assert_eq!( /// parser.parse("+inf"), /// Err(ParseError::Syntax(1, format!("Invalid input: 'inf'"))) /// ); /// ``` pub fn disable_infinity(&mut self, value: bool) -> &mut Self { self.inner.config.disable_infinity = value; self } /// This setting makes a number in the source string optional. /// /// See also [`crate::DurationParser::number_is_optional`]. /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParser, Duration, DEFAULT_TIME_UNITS}; /// /// let mut parser = CustomDurationParser::with_time_units(&DEFAULT_TIME_UNITS); /// parser.number_is_optional(true); /// /// assert_eq!(parser.parse("ns"), Ok(Duration::positive(0, 1))); /// assert_eq!(parser.parse(".001e-6s"), Ok(Duration::positive(0, 1))); /// assert_eq!(parser.parse("+ns"), Ok(Duration::positive(0, 1))); /// ``` pub fn number_is_optional(&mut self, value: bool) -> &mut Self { self.inner.config.number_is_optional = value; self } /// If set to some [`Delimiter`], parse possibly multiple durations and sum them up. /// /// See also [`crate::DurationParser::parse_multiple`] /// /// # Examples /// /// ```rust /// use fundu::{CustomDurationParser, Duration, DEFAULT_TIME_UNITS}; /// /// let mut parser = CustomDurationParser::with_time_units(&DEFAULT_TIME_UNITS); /// parser.parse_multiple(Some(|byte| matches!(byte, b' ' | b'\t')), Some(&["and"])); /// /// assert_eq!( /// parser.parse("1.5h 2e+2ns"), /// Ok(Duration::positive(5400, 200)) /// ); /// assert_eq!( /// parser.parse("55s500ms"), /// Ok(Duration::positive(55, 500_000_000)) /// ); /// assert_eq!(parser.parse("1m and 1ns"), Ok(Duration::positive(60, 1))); /// assert_eq!( /// parser.parse("1. .1"), /// Ok(Duration::positive(1, 100_000_000)) /// ); /// assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0))); /// assert_eq!( /// parser.parse("300ms20s 5d"), /// Ok(Duration::positive(5 * 60 * 60 * 24 + 20, 300_000_000)) /// ); /// ``` pub fn parse_multiple( &mut self, delimiter: Option, conjunctions: Option<&'a [&'a str]>, ) -> &mut Self { self.inner.config.parse_multiple_delimiter = delimiter; self.inner.config.parse_multiple_conjunctions = conjunctions; self } /// Try to find the [`TimeUnit`] with it's associate [`Multiplier`] by id /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Multiplier}; /// /// let parser = CustomDurationParser::with_time_units(&[ /// CustomTimeUnit::with_default(NanoSecond, &["ns"]), /// CustomTimeUnit::with_default(MicroSecond, &["Ms"]), /// ]); /// /// assert_eq!(parser.get_time_unit_by_id("does_not_exist"), None); /// /// for (time_unit, id) in &[(NanoSecond, "ns"), (MicroSecond, "Ms")] { /// assert_eq!( /// parser.get_time_unit_by_id(id), /// Some((*time_unit, Multiplier(1, 0))) /// ); /// } /// ``` pub fn get_time_unit_by_id(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> { self.time_units.get(identifier) } /// Return true if there are haven't been any time units added, yet. /// /// # Examples /// /// ```rust /// use fundu::CustomDurationParser; /// /// let parser = CustomDurationParser::new(); /// assert!(parser.is_empty()) /// ``` pub fn is_empty(&self) -> bool { self.time_units.is_empty() } } impl<'a> Default for CustomDurationParser<'a> { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use rstest::rstest; use super::*; use crate::config::Config; use crate::custom::builder::CustomDurationParserBuilder; use crate::custom::time_units::DEFAULT_ALL_TIME_UNITS; use crate::time::Duration; use crate::TimeUnit::*; const YEAR: u64 = 60 * 60 * 24 * 365 + 60 * 60 * 24 / 4; const MONTH: u64 = YEAR / 12; #[test] fn test_custom_duration_parser_init_new() { let parser = CustomDurationParser::new(); assert_eq!(parser.inner.config.default_unit, Second); assert!(parser.time_units.is_empty()); assert_eq!(parser.parse("1.0"), Ok(Duration::positive(1, 0))); assert_eq!( parser.parse("1.0s"), Err(ParseError::TimeUnit( 3, "No time units allowed but found: 's'".to_string() )) ); } #[test] fn test_custom_duration_parser_init_with_time_units() { let parser = CustomDurationParser::with_time_units(&DEFAULT_ALL_TIME_UNITS); assert_eq!(parser.inner.config.default_unit, Second); for unit in DEFAULT_ALL_TIME_UNITS { let CustomTimeUnit { base_unit, multiplier: _, identifiers, } = unit; for id in identifiers { assert_eq!( parser.get_time_unit_by_id(id), Some((base_unit, Multiplier::default())) ); } } assert_eq!(parser.parse("1.0"), Ok(Duration::positive(1, 0))); } #[test] fn test_custom_duration_parser_init_default() { let parser = CustomDurationParser::default(); assert!(parser.time_units.is_empty()); } #[test] fn test_custom_duration_parser_when_add_custom_time_units() { let ids = ["century", "centuries"]; let mut parser = CustomDurationParser::new(); parser.time_unit(CustomTimeUnit::new(Year, &ids, Some(Multiplier(100, 0)))); for id in ids { assert_eq!( parser.get_time_unit_by_id(id), Some((Year, Multiplier(100, 0))) ); } } #[test] fn test_custom_duration_parser_when_setting_default_time_unit() { let mut parser = CustomDurationParser::new(); parser.default_unit(NanoSecond); assert_eq!(parser.inner.config.default_unit, NanoSecond); assert_eq!(parser.parse("1"), Ok(Duration::positive(0, 1))); } #[rstest] #[case::nano_second("1ns", Duration::positive(0, 1))] #[case::micro_second("1Ms", Duration::positive(0, 1000))] #[case::milli_second("1ms", Duration::positive(0, 1_000_000))] #[case::second("1s", Duration::positive(1, 0))] #[case::minute("1m", Duration::positive(60, 0))] #[case::hour("1h", Duration::positive(60 * 60, 0))] #[case::day("1d", Duration::positive(60 * 60 * 24, 0))] #[case::week("1w", Duration::positive(60 * 60 * 24 * 7, 0))] #[case::month("1M", Duration::positive(MONTH, 0))] #[case::year("1y", Duration::positive(YEAR, 0))] fn test_custom_duration_parser_parse_when_default_time_units( #[case] input: &str, #[case] expected: Duration, ) { let parser = CustomDurationParser::with_time_units(&DEFAULT_ALL_TIME_UNITS); assert_eq!(parser.parse(input), Ok(expected)); } #[test] fn test_custom_duration_parser_parse_when_non_ascii() { let parser = CustomDurationParser::with_time_units(&[CustomTimeUnit::with_default( MilliSecond, &["мілісекунда"], )]); assert_eq!( parser.parse("1мілісекунда"), Ok(Duration::positive(0, 1_000_000)) ); } #[test] fn test_custom_duration_parser_setting_allow_spaces() { let mut parser = CustomDurationParser::new(); parser.allow_delimiter(Some(|b| b == b' ')); assert!(parser.inner.config.allow_delimiter.unwrap()(b' ')); } #[test] fn test_custom_duration_parser_setting_disable_fraction() { let mut parser = CustomDurationParser::new(); parser.disable_fraction(true); assert!(parser.inner.config.disable_fraction); } #[test] fn test_custom_duration_parser_setting_disable_exponent() { let mut parser = CustomDurationParser::new(); parser.disable_exponent(true); assert!(parser.inner.config.disable_exponent); } #[test] fn test_custom_duration_parser_setting_disable_infinity() { let mut parser = CustomDurationParser::new(); parser.disable_infinity(true); assert!(parser.inner.config.disable_infinity); } #[test] fn test_custom_duration_parser_setting_number_is_optional() { let mut parser = CustomDurationParser::new(); parser.number_is_optional(true); assert!(parser.inner.config.number_is_optional); } #[test] fn test_custom_duration_parser_setting_parse_multiple() { let mut parser = CustomDurationParser::new(); parser.parse_multiple(Some(|byte| byte == 0xff), None); assert!(parser.inner.config.parse_multiple_delimiter.unwrap()(0xff)); } #[test] fn test_custom_duration_parser_method_builder() { assert_eq!( CustomDurationParser::builder(), CustomDurationParserBuilder::new() ); } #[test] fn test_custom_duration_parser_when_adding_custom_time_units() { let time_units = [ CustomTimeUnit::new(Second, &["sec"], Some(Multiplier(1, 0))), CustomTimeUnit::new(Second, &["secs"], Some(Multiplier(2, 0))), ]; let mut custom = CustomDurationParser::new(); custom.time_units(&time_units); assert_eq!( custom.get_time_unit_by_id("sec"), Some((Second, Multiplier(1, 0))) ); assert_eq!( custom.get_time_unit_by_id("secs"), Some((Second, Multiplier(2, 0))) ); } #[test] fn test_custom_duration_parser_when_calling_custom_time_units_with_empty_collection() { let mut custom = CustomDurationParser::new(); assert!(custom.is_empty()); custom.time_units(&[]); assert!(custom.is_empty()); } #[test] fn test_custom_duration_parser_when_keyword() { let mut custom = CustomDurationParser::new(); custom.keyword(TimeKeyword::new(Second, &["sec"], Some(Multiplier(2, 0)))); assert_eq!( custom.keywords.get("sec").unwrap(), (Second, Multiplier(2, 0)) ); } #[test] fn test_custom_duration_parser_when_keywords() { let mut custom = CustomDurationParser::new(); custom.keywords(&[ TimeKeyword::new(Second, &["sec"], Some(Multiplier(1, 0))), TimeKeyword::new(Second, &["secs"], Some(Multiplier(2, 0))), ]); assert_eq!( custom.keywords.get("sec").unwrap(), (Second, Multiplier(1, 0)) ); assert_eq!( custom.keywords.get("secs").unwrap(), (Second, Multiplier(2, 0)) ); } #[test] fn test_custom_duration_parser_allow_negative() { let mut expected = Config::new(); expected.allow_negative = true; let mut parser = CustomDurationParser::new(); parser.allow_negative(true); assert_eq!(parser.inner.config, expected); } #[test] #[cfg_attr(miri, ignore)] fn test_custom_duration_parser_allow_ago() { let delimiter = |byte: u8| byte.is_ascii_whitespace(); let mut expected = Config::new(); expected.allow_ago = Some(delimiter); expected.allow_negative = true; let mut parser = CustomDurationParser::new(); parser.allow_ago(Some(delimiter)); assert_eq!(parser.inner.config, expected); } } fundu-1.0.0/src/custom/time_units.rs000064400000000000000000000716321046102023000156030ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use std::hash::{Hash, Hasher}; use crate::time::TimeUnitsLike; use crate::TimeUnit::*; use crate::{ Multiplier, TimeUnit, DEFAULT_ID_DAY, DEFAULT_ID_HOUR, DEFAULT_ID_MICRO_SECOND, DEFAULT_ID_MILLI_SECOND, DEFAULT_ID_MINUTE, DEFAULT_ID_MONTH, DEFAULT_ID_NANO_SECOND, DEFAULT_ID_SECOND, DEFAULT_ID_WEEK, DEFAULT_ID_YEAR, }; /// The identifiers as defined in /// [`systemd.time`](https://www.man7.org/linux/man-pages/man7/systemd.time.7.html) pub const SYSTEMD_TIME_UNITS: [CustomTimeUnit<'static>; 10] = [ CustomTimeUnit::with_default(NanoSecond, &["ns", "nsec"]), CustomTimeUnit::with_default(MicroSecond, &["us", "µs", "usec"]), CustomTimeUnit::with_default(MilliSecond, &["ms", "msec"]), CustomTimeUnit::with_default(Second, &["s", "sec", "second", "seconds"]), CustomTimeUnit::with_default(Minute, &["m", "min", "minute", "minutes"]), CustomTimeUnit::with_default(Hour, &["h", "hr", "hour", "hours"]), CustomTimeUnit::with_default(Day, &["d", "day", "days"]), CustomTimeUnit::with_default(Week, &["w", "week", "weeks"]), CustomTimeUnit::with_default(Month, &["M", "month", "months"]), CustomTimeUnit::with_default(Year, &["y", "year", "years"]), ]; /// The default identifiers taken from the `standard` feature (without `Month` and /// `Year`) pub const DEFAULT_TIME_UNITS: [CustomTimeUnit<'static>; 8] = [ CustomTimeUnit::with_default(NanoSecond, &[DEFAULT_ID_NANO_SECOND]), CustomTimeUnit::with_default(MicroSecond, &[DEFAULT_ID_MICRO_SECOND]), CustomTimeUnit::with_default(MilliSecond, &[DEFAULT_ID_MILLI_SECOND]), CustomTimeUnit::with_default(Second, &[DEFAULT_ID_SECOND]), CustomTimeUnit::with_default(Minute, &[DEFAULT_ID_MINUTE]), CustomTimeUnit::with_default(Hour, &[DEFAULT_ID_HOUR]), CustomTimeUnit::with_default(Day, &[DEFAULT_ID_DAY]), CustomTimeUnit::with_default(Week, &[DEFAULT_ID_WEEK]), ]; /// All identifiers taken from the `standard` feature with `Month` and `Year` pub const DEFAULT_ALL_TIME_UNITS: [CustomTimeUnit<'static>; 10] = [ CustomTimeUnit::with_default(NanoSecond, &[DEFAULT_ID_NANO_SECOND]), CustomTimeUnit::with_default(MicroSecond, &[DEFAULT_ID_MICRO_SECOND]), CustomTimeUnit::with_default(MilliSecond, &[DEFAULT_ID_MILLI_SECOND]), CustomTimeUnit::with_default(Second, &[DEFAULT_ID_SECOND]), CustomTimeUnit::with_default(Minute, &[DEFAULT_ID_MINUTE]), CustomTimeUnit::with_default(Hour, &[DEFAULT_ID_HOUR]), CustomTimeUnit::with_default(Day, &[DEFAULT_ID_DAY]), CustomTimeUnit::with_default(Week, &[DEFAULT_ID_WEEK]), CustomTimeUnit::with_default(Month, &[DEFAULT_ID_MONTH]), CustomTimeUnit::with_default(Year, &[DEFAULT_ID_YEAR]), ]; pub(super) type IdentifiersLookupData<'a> = (LookupData, Vec<&'a str>); #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(super) struct LookupData { min_length: usize, max_length: usize, time_unit: TimeUnit, multiplier: Multiplier, } impl LookupData { fn new(time_unit: TimeUnit, multiplier: Multiplier) -> Self { Self { min_length: usize::MAX, max_length: 0, time_unit, multiplier, } } fn update(&mut self, identifier: &str) { let len = identifier.len(); if self.min_length > len { self.min_length = len; } if self.max_length < len { self.max_length = len; } } fn check(&self, identifier: &str) -> bool { let len = identifier.len(); self.min_length <= len && self.max_length >= len } } /// A [`CustomTimeUnit`] is a completely customizable [`TimeUnit`] using an additional /// [`Multiplier`]. /// /// Custom time units have a base [`TimeUnit`] (which has an inherent [`Multiplier`]) and an /// optional [`Multiplier`] which acts as an additional [`Multiplier`] in addition to the multiplier /// of the `base_unit`. Using a multiplier with `Multiplier(1, 0)` is equivalent to using no /// multiplier at all but see also the `Problems` section. A [`CustomTimeUnit`] also consists of /// identifiers which are used to identify the [`CustomTimeUnit`] during the parsing process. /// /// # Panics /// /// For `base_unit`s other than `Second` and `multipliers` other than `Multiplier(1,0)` there may /// occur a panic during the creation of a [`CustomTimeUnit`]. The Multiplier boundaries are chosen /// as high as possible but if the `base_unit` multiplier multiplied with `multiplier` exceeds /// `(u64::MAX, i16::MIN)` or `(u64::MAX, i16::MAX)` this multiplication overflows. By example, with /// `Multiplier(m, e)` two multipliers x, y are multiplied as follows : `m = x.m * y.m, e = x.e + /// y.e`: /// /// If the `base_unit` is `Year`, which has a multiplier of `m = 31557600, e = 0`, then this /// restricts the `multiplier` to `m = u64::MAX / 31557600 = 584_542_046_090, e = 32767 or e = /// -32768`. /// /// If the `base_unit` is `NanoSecond`, which has a multiplier of `m = 1, e = -9`, then this /// restricts the `multiplier` to `m = u64::MAX, e = -32768 + 9 = -32,759 or e = i16::MAX = 32767`. /// /// The `base_unit`s are `Second`s based what results in the following table with limits for the /// `multiplier`: /// /// | `base_unit` | `base_unit Multiplier` | Limit m | Limit -e | Limit +e /// | --------------- | ----------:| -------:| --------:| ------:| /// | Nanosecond | Multiplier(1, -9) | u64::MAX | -32,759 | i16::MAX /// | Microsecond | Multiplier(1, -6) | u64::MAX | -32,762 | i16::MAX /// | Millisecond | Multiplier(1, -3) | u64::MAX | -32,765 | i16::MAX /// | Second | Multiplier(1, 0) | u64::MAX | i16::MIN | i16::MAX /// | Minute | Multiplier(60, 0) | 307_445_734_561_825_860 | i16::MIN | i16::MAX /// | Hour | Multiplier(3600, 0) | 5_124_095_576_030_431 | i16::MIN | i16::MAX /// | Day | Multiplier(86400, 0) | 213_503_982_334_601 | i16::MIN | i16::MAX /// | Week | Multiplier(604800, 0) | 30_500_568_904_943 | i16::MIN | i16::MAX /// | Month | Multiplier(2629800, 0) | 7_014_504_553_087 | i16::MIN | i16::MAX /// | Year | Multiplier(31557600, 0) | 584_542_046_090 | i16::MIN | i16::MAX /// /// # Examples /// /// To create a [`CustomTimeUnit`] representing two weeks there are multiple solutions. Just to show /// two very obvious examples: /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomTimeUnit, Multiplier}; /// /// assert_ne!( /// (CustomTimeUnit::new(Week, &["fortnight", "fortnights"], Some(Multiplier(2, 0)))), /// (CustomTimeUnit::new(Day, &["fortnight", "fortnights"], Some(Multiplier(14, 0)))) /// ); /// ``` /// /// Both would actually be equal in the sense, that they would resolve to the same result when /// multiplying the `base_unit` with the `multiplier`, however they are treated as not equal and /// it's possible to choose freely between the definitions. Using both of the definitions in /// parallel within the [`crate::CustomDurationParser`] would be possible and produces the desired /// result, although it does not provide any benefits. /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier}; /// /// let parser = CustomDurationParser::builder() /// .time_units(&[ /// CustomTimeUnit::new(Week, &["fortnight", "fortnights"], Some(Multiplier(2, 0))), /// CustomTimeUnit::new(Day, &["fortnight", "fortnights"], Some(Multiplier(14, 0))), /// ]) /// .build(); /// /// assert_eq!( /// parser.parse("1fortnight").unwrap(), /// Duration::positive(1209600, 0) /// ); /// ``` /// /// In summary, the best choice is to use the [`CustomTimeUnit`] with a `base_unit` having the /// lowest [`Multiplier`] but see also `Problems` below. /// /// Equality of two [`CustomTimeUnit`] is defined as /// /// ```ignore /// base_unit == other.base_unit && multiplier == other.multiplier /// ``` #[derive(Debug, Eq, Clone, Copy)] pub struct CustomTimeUnit<'a> { pub(super) base_unit: TimeUnit, pub(super) multiplier: Multiplier, pub(super) identifiers: &'a [&'a str], } impl<'a> CustomTimeUnit<'a> { /// Create a new [`CustomTimeUnit`] /// /// See also the documentation for [`CustomTimeUnit`]. /// /// # Panics /// /// If the [`Multiplier`] of the `base_unit` multiplied with the optional [`Multiplier`] /// parameter overflows. See also [`CustomTimeUnit`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{CustomDurationParser, CustomTimeUnit, Duration, Multiplier}; /// /// let time_unit = CustomTimeUnit::new(Second, &["shake", "shakes"], Some(Multiplier(1, -8))); /// let parser = CustomDurationParser::builder().time_unit(time_unit).build(); /// /// assert_eq!(parser.parse("1shake").unwrap(), Duration::positive(0, 10)); /// ``` pub const fn new( base_unit: TimeUnit, identifiers: &'a [&'a str], multiplier: Option, ) -> Self { Self { base_unit, multiplier: match multiplier { Some(m) => { // expect is stable yet in const context so we use panic! here if base_unit.multiplier().checked_mul(m).is_none() { panic!( "The time unit multiplier multiplied with the multiplier parameter \ may not overflow" ) } m } None => Multiplier(1, 0), }, identifiers, } } pub const fn with_default(base_unit: TimeUnit, identifiers: &'a [&'a str]) -> Self { Self::new(base_unit, identifiers, None) } } /// Two [`CustomTimeUnit]s are equal if their `base_unit`s and their `multipliers` are equal /// /// The identifiers don't influence equality impl<'a> PartialEq for CustomTimeUnit<'a> { fn eq(&self, other: &Self) -> bool { self.base_unit == other.base_unit && self.multiplier == other.multiplier } } /// Two hashes of a [`CustomTimeUnit`] are equal if their `base_unit`s and their `multipliers` are /// equal /// /// The identifiers don't have an influence on the hash impl<'a> Hash for CustomTimeUnit<'a> { fn hash(&self, state: &mut H) { self.base_unit.hash(state); self.multiplier.hash(state); } } #[derive(Debug, PartialEq, Eq, Clone)] pub(super) struct CustomTimeUnits<'a> { min_length: usize, max_length: usize, time_units: Vec>, } impl<'a> CustomTimeUnits<'a> { pub(super) fn new() -> Self { Self::with_capacity(0) } pub(super) fn with_time_units(units: &[CustomTimeUnit<'a>]) -> Self { let mut time_units = Self::with_capacity(units.len()); for unit in units { time_units.add_custom_time_unit(*unit); } time_units } pub(super) fn with_keywords(keywords: &[TimeKeyword<'a>]) -> Self { let mut time_units = Self::with_capacity(keywords.len()); for keyword in keywords { time_units.add_custom_time_unit(keyword.to_custom_time_unit()); } time_units } pub(super) fn with_capacity(capacity: usize) -> Self { Self { min_length: usize::MAX, max_length: 0, time_units: Vec::with_capacity(capacity), } } pub(super) fn add_custom_time_unit(&mut self, time_unit: CustomTimeUnit<'a>) { if time_unit.identifiers.is_empty() { return; } let CustomTimeUnit { base_unit, multiplier, identifiers, } = time_unit; let (min_length, max_length) = match self.lookup_mut(base_unit, multiplier) { Some((data, ids)) => { for &identifier in identifiers.iter().filter(|&&id| !id.is_empty()) { ids.push(identifier); data.update(identifier); } (data.min_length, data.max_length) } None => { let mut data = LookupData::new(base_unit, multiplier); let mut ids = Vec::with_capacity(identifiers.len()); for &identifier in identifiers.iter().filter(|&&id| !id.is_empty()) { ids.push(identifier); data.update(identifier); } if ids.is_empty() { return; } let lengths = (data.min_length, data.max_length); self.time_units.push((data, ids)); lengths } }; self.update_lengths(min_length, max_length); } pub(super) fn lookup_mut( &'_ mut self, unit: TimeUnit, multiplier: Multiplier, ) -> Option<&'_ mut (LookupData, Vec<&'a str>)> { self.time_units .iter_mut() .find(|(data, _)| data.time_unit == unit && data.multiplier == multiplier) } #[allow(dead_code)] pub(super) fn lookup( &self, unit: TimeUnit, multiplier: Multiplier, ) -> Option<&(LookupData, Vec<&'a str>)> { self.time_units .iter() .find(|(data, _)| data.time_unit == unit && data.multiplier == multiplier) } pub(super) fn find_id(&self, id: &str) -> Option<(TimeUnit, Multiplier)> { self.time_units.iter().find_map(|(data, v)| { if data.check(id) && v.contains(&id) { Some((data.time_unit, data.multiplier)) } else { None } }) } pub(super) fn update_lengths(&mut self, min_length: usize, max_length: usize) { if self.min_length > min_length { self.min_length = min_length; } if self.max_length < max_length { self.max_length = max_length; } } } impl<'a> TimeUnitsLike for CustomTimeUnits<'a> { #[inline] fn is_empty(&self) -> bool { self.time_units.is_empty() } #[inline] fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> { let len = identifier.len(); if self.min_length > len || self.max_length < len { return None; } self.find_id(identifier) } } /// A [`TimeKeyword`] represents a complete duration without the need for a number /// /// With `TimeKeywords` something like `yesterday`, which is worth `-1 day`, can be constructed. A /// [`TimeKeyword`] has the same basic structure as a [`CustomTimeUnit`], but is treated /// differently, so that `TimeKeywords` do not accept a preceding number or, if enabled, the `ago` /// modifier after them. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{ /// CustomDurationParser, CustomTimeUnit, Duration, Multiplier, ParseError, TimeKeyword, /// }; /// /// let mut parser = /// CustomDurationParser::with_time_units(&[CustomTimeUnit::with_default(NanoSecond, &["ns"])]); /// parser.keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))); /// /// assert_eq!( /// parser.parse("tomorrow"), /// Ok(Duration::positive(60 * 60 * 24, 0)) /// ); /// /// // but not /// assert_eq!( /// parser.parse("123tomorrow"), /// Err(ParseError::TimeUnit( /// 3, /// "Invalid time unit: 'tomorrow'".to_string() /// )) /// ); /// ``` #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct TimeKeyword<'a> { time_unit: CustomTimeUnit<'a>, } impl<'a> TimeKeyword<'a> { /// Construct a new `TimeKeyword` pub const fn new( base_unit: TimeUnit, identifiers: &'a [&'a str], multiplier: Option, ) -> Self { Self { time_unit: CustomTimeUnit::new(base_unit, identifiers, multiplier), } } /// Convert this keyword to a [`CustomTimeUnit`] pub(super) const fn to_custom_time_unit(self) -> CustomTimeUnit<'a> { self.time_unit } } #[cfg(test)] mod tests { use std::collections::hash_map::DefaultHasher; use rstest::rstest; use super::*; fn make_lookup_result( min_length: usize, max_length: usize, time_unit: TimeUnit, multiplier: Multiplier, identifiers: Vec<&str>, ) -> (LookupData, Vec<&str>) { ( LookupData { min_length, max_length, time_unit, multiplier, }, identifiers, ) } #[test] fn test_custom_time_units_init_new() { let custom = CustomTimeUnits::new(); assert!(custom.time_units.is_empty()); assert!(custom.is_empty()); } #[rstest] #[case::nano_second(CustomTimeUnit::with_default(NanoSecond, &["some"]), 4, 4)] #[case::nano_second_with_multiple_ids( CustomTimeUnit::with_default(NanoSecond, &["some", "other", "деякі"]), 4, 10 )] #[case::micro_second( CustomTimeUnit::with_default(MicroSecond, &["some"]), 4, 4 )] #[case::micro_second_with_multiple_ids( CustomTimeUnit::with_default(MicroSecond, &["some", "other", "деякі"]), 4, 10 )] #[case::milli_second( CustomTimeUnit::with_default(MilliSecond, &["some"]), 4, 4 )] #[case::milli_second_with_multiple_ids( CustomTimeUnit::with_default(MilliSecond, &["some", "other", "деякі"]), 4, 10 )] #[case::second( CustomTimeUnit::with_default(Second, &["some"]), 4, 4 )] #[case::second_with_multiple_ids( CustomTimeUnit::with_default(Second, &["some", "other", "деякі"]), 4, 10 )] #[case::minute( CustomTimeUnit::with_default(Minute, &["some"]), 4, 4 )] #[case::minute_with_multiple_ids( CustomTimeUnit::with_default(Minute, &["some", "other", "деякі"]), 4, 10 )] #[case::hour( CustomTimeUnit::with_default(Hour, &["some"]), 4, 4 )] #[case::hour_with_multiple_ids( CustomTimeUnit::with_default(Hour, &["some", "other", "деякі"]), 4, 10 )] #[case::day( CustomTimeUnit::with_default(Day, &["some"]), 4, 4 )] #[case::day_with_multiple_ids( CustomTimeUnit::with_default(Day, &["some", "other", "деякі"]), 4, 10 )] #[case::week( CustomTimeUnit::with_default(Week, &["some"]), 4, 4 )] #[case::week_with_multiple_ids( CustomTimeUnit::with_default(Week, &["some", "other", "деякі"]), 4, 10 )] #[case::month( CustomTimeUnit::with_default(Month, &["some"]), 4, 4 )] #[case::month_with_multiple_ids( CustomTimeUnit::with_default(Month, &["some", "other", "деякі"]), 4, 10 )] #[case::year( CustomTimeUnit::with_default(Year, &["some"]), 4, 4 )] #[case::year_with_multiple_ids( CustomTimeUnit::with_default(Year, &["some", "other", "деякі"]), 4, 10 )] fn test_custom_time_units_init_with_time_units( #[case] time_unit: CustomTimeUnit, #[case] min_length: usize, #[case] max_length: usize, ) { let custom = CustomTimeUnits::with_time_units(&[(time_unit)]); assert!(!custom.is_empty()); assert_eq!( custom.lookup(time_unit.base_unit, Multiplier::default()), Some(&make_lookup_result( min_length, max_length, time_unit.base_unit, Multiplier::default(), Vec::from(time_unit.identifiers) )) ); } #[test] fn test_custom_time_units_init_with_time_units_when_multiple_equal_ids() { let custom = CustomTimeUnits::with_time_units(&[(CustomTimeUnit::with_default( NanoSecond, &["same", "same"], ))]); assert!(!custom.is_empty()); assert_eq!( custom.lookup(NanoSecond, Multiplier::default()), Some(&make_lookup_result( 4, 4, NanoSecond, Multiplier::default(), vec!["same", "same"] )) ); assert_eq!( custom.get("same"), Some((NanoSecond, Multiplier::default())) ); } #[test] fn test_custom_time_units_when_add_custom_time_unit() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::with_default(MicroSecond, &["some", "ids"])); assert!( custom .time_units .iter() .filter(|(data, _)| data.time_unit != MicroSecond) .all(|(_, v)| v.is_empty()) ); assert_eq!( custom.lookup(MicroSecond, Multiplier::default()).unwrap().1, vec!["some", "ids"] ); assert_eq!( custom.get("some"), Some((MicroSecond, Multiplier::default())) ); assert_eq!( custom.get("ids"), Some((MicroSecond, Multiplier::default())) ); assert_eq!(custom.get("does not exist"), None); assert!(!custom.is_empty()); } #[test] fn test_custom_time_units_when_adding_time_unit_with_empty_slice_then_not_added() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::with_default(MicroSecond, &[])); assert!(custom.is_empty()); assert_eq!(custom.lookup(MicroSecond, Multiplier::default()), None); } #[test] fn test_custom_time_units_when_adding_time_unit_with_empty_id_then_not_added() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::with_default(MicroSecond, &[""])); assert!(custom.is_empty()); assert_eq!(custom.lookup(MicroSecond, Multiplier::default()), None); } #[test] fn test_custom_time_units_adding_custom_time_unit_with_empty_id_then_not_added() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::new(Second, &[""], Some(Multiplier(2, 0)))); assert!(custom.is_empty()); } #[test] fn test_custom_time_units_adding_custom_time_unit() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::new( Second, &["sec"], Some(Multiplier(2, 0)), )); assert!(!custom.is_empty()); assert_eq!( custom.lookup(Second, Multiplier(2, 0)), Some(&make_lookup_result( 3, 3, Second, Multiplier(2, 0), vec!["sec"] )) ); assert_eq!(custom.get("sec"), Some((Second, Multiplier(2, 0)))); } #[test] fn test_custom_time_units_adding_multiple_custom_time_unit() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::new( Second, &["sec"], Some(Multiplier(1, 0)), )); custom.add_custom_time_unit(CustomTimeUnit::new( Second, &["secs"], Some(Multiplier(2, 0)), )); assert_eq!( custom.lookup(Second, Multiplier::default()), Some(&make_lookup_result( 3, 3, Second, Multiplier::default(), vec!["sec"] )) ); assert_eq!( custom.lookup(Second, Multiplier(2, 0)), Some(&make_lookup_result( 4, 4, Second, Multiplier(2, 0), vec!["secs"] )) ); assert_eq!(custom.get("sec"), Some((Second, Multiplier(1, 0)))); assert_eq!(custom.get("secs"), Some((Second, Multiplier(2, 0)))); } #[test] fn test_custom_time_units_adding_custom_time_unit_when_normal_time_unit_already_exists() { let mut custom = CustomTimeUnits::with_time_units(&[CustomTimeUnit::with_default(Second, &["s"])]); custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["ss"], Some(Multiplier(2, 0)))); assert_eq!( custom.lookup(Second, Multiplier::default()), Some(&make_lookup_result( 1, 1, Second, Multiplier::default(), vec!["s"] )) ); assert_eq!( custom.lookup(Second, Multiplier(2, 0)), Some(&make_lookup_result( 2, 2, Second, Multiplier(2, 0), vec!["ss"] )) ); assert_eq!(custom.get("s"), Some((Second, Multiplier(1, 0)))); assert_eq!(custom.get("ss"), Some((Second, Multiplier(2, 0)))); } #[test] fn test_custom_time_units_adding_custom_time_unit_when_normal_time_unit_with_same_id() { let mut custom = CustomTimeUnits::with_time_units(&[CustomTimeUnit::with_default(Second, &["s"])]); custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2, 0)))); assert_eq!( custom.lookup(Second, Multiplier::default()), Some(&make_lookup_result( 1, 1, Second, Multiplier::default(), vec!["s"] )) ); assert_eq!( custom.lookup(Second, Multiplier(2, 0)), Some(&make_lookup_result( 1, 1, Second, Multiplier(2, 0), vec!["s"] )) ); assert_eq!(custom.get("s"), Some((Second, Multiplier(1, 0)))); } #[test] fn test_custom_time_units_adding_custom_time_unit_when_identifiers_is_empty() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::new(Second, &[], Some(Multiplier(2, 0)))); assert!(custom.is_empty()); assert_eq!(custom.lookup(Second, Multiplier(2, 0)), None); } #[test] fn test_custom_time_units_adding_custom_time_unit_when_adding_same_unit_twice() { let mut custom = CustomTimeUnits::new(); custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2, 0)))); custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2, 0)))); assert_eq!( custom.lookup(Second, Multiplier(2, 0)), Some(&make_lookup_result( 1, 1, Second, Multiplier(2, 0), vec!["s", "s"] )) ); } #[test] #[should_panic = "The time unit multiplier multiplied with the multiplier parameter may not \ overflow"] fn test_custom_time_unit_new_when_overflow_then_panic() { CustomTimeUnit::new(Year, &["year"], Some(Multiplier(i64::MAX, 0))); } #[rstest] #[case::none_and_default_multiplier_when_same_ids( CustomTimeUnit::new(Second, &["s"], None), CustomTimeUnit::new(Second, &["s"], Some(Multiplier::default())) )] #[case::none_and_default_multiplier_when_different_ids( CustomTimeUnit::new(Second, &["s"], None), CustomTimeUnit::new(Second, &["secs"], Some(Multiplier::default())) )] #[case::same_multipliers( CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2,0))), CustomTimeUnit::new(Second, &["secs"], Some(Multiplier(2,0))) )] fn test_custom_time_unit_equality_when_equal( #[case] time_unit: CustomTimeUnit, #[case] other: CustomTimeUnit, ) { assert_eq!(time_unit, other); } #[rstest] #[case::different_time_units( CustomTimeUnit::new(MilliSecond, &["ms"], Some(Multiplier(1,0))), CustomTimeUnit::new(Second, &["ms"], Some(Multiplier(1,0))) )] #[case::different_multipliers( CustomTimeUnit::new(MilliSecond, &["ms"], Some(Multiplier(1000,0))), CustomTimeUnit::new(Second, &["ms"], Some(Multiplier(1, 0))) )] fn test_custom_time_unit_equality_when_not_equal( #[case] time_unit: CustomTimeUnit, #[case] other: CustomTimeUnit, ) { assert_ne!(time_unit, other); } #[rstest] #[case::second( CustomTimeUnit::new(Second, &["some"], Some(Multiplier(1, 0))), CustomTimeUnit::new(Second, &["other"], Some(Multiplier(1, 0))) )] #[case::day( CustomTimeUnit::new(Day, &["some"], None), CustomTimeUnit::with_default(Day, &["other"]) )] #[case::day_with_multiplier( CustomTimeUnit::new(Day, &["some"], Some(Multiplier(123, 4))), CustomTimeUnit::new(Day, &["other"], Some(Multiplier(123, 4))) )] fn test_custom_time_unit_hash_when_equal( #[case] time_unit: CustomTimeUnit, #[case] other: CustomTimeUnit, ) { assert_eq!(time_unit, other); assert_eq!(other, time_unit); let mut hasher = DefaultHasher::new(); time_unit.hash(&mut hasher); let mut other_hasher = DefaultHasher::new(); other.hash(&mut other_hasher); assert_eq!(hasher.finish(), other_hasher.finish()); } } fundu-1.0.0/src/error.rs000064400000000000000000000157641046102023000132460ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! Provide the [`ParseError`] use std::error::Error; use std::fmt::Display; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; /// Error type emitted during the parsing #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[non_exhaustive] pub enum ParseError { /// Returned, if the input was empty. Empty, /// A syntax error. Syntax errors report the position (column) where it was encountered and a /// reason. Syntax(usize, String), /// Currently only used internally for overflows of the maximum Duration. Overflow, /// An error concerning time units. Like [`ParseError::Syntax`] the position where the error /// occurred is included. TimeUnit(usize, String), /// The exponent exceeded the minimum negative exponent (`-32768`) NegativeExponentOverflow, /// The exponent exceeded the maximum positive exponent (`+32767`) PositiveExponentOverflow, /// The input number was negative. Note that numbers close to `0` (`< 1e-18`) are not negative /// but resolve to `0` NegativeNumber, /// A generic error if no other error type fits InvalidInput(String), } impl Error for ParseError {} impl Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let msg = match self { ParseError::Syntax(column, reason) => { format!("Syntax error: {reason} at column {column}") } ParseError::Overflow => "Number overflow".to_string(), ParseError::TimeUnit(pos, reason) => { format!("Time unit error: {reason} at column {pos}") } ParseError::NegativeExponentOverflow => { "Negative exponent overflow: Minimum is -32768".to_string() } ParseError::PositiveExponentOverflow => { "Positive exponent overflow: Maximum is +32767".to_string() } ParseError::NegativeNumber => "Number was negative".to_string(), ParseError::InvalidInput(reason) => format!("Invalid input: {reason}"), ParseError::Empty => "Empty input".to_string(), }; f.write_str(&msg) } } /// This error may occur when converting a [`crate::Duration`] to a different duration like /// [`std::time::Duration`] #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TryFromDurationError { /// The duration was negative and the destination duration doesn't support negative durations NegativeDuration, /// The duration was higher than the maximum of the destination duration PositiveOverflow, /// The duration was lower than the minimum of the destination duration NegativeOverflow, } impl From for ParseError { fn from(error: TryFromDurationError) -> Self { match error { TryFromDurationError::NegativeDuration => ParseError::NegativeNumber, TryFromDurationError::PositiveOverflow => ParseError::Overflow, TryFromDurationError::NegativeOverflow => ParseError::Overflow, } } } impl Error for TryFromDurationError {} impl Display for TryFromDurationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let description = match self { TryFromDurationError::NegativeDuration => { "Error converting duration: value is negative" } TryFromDurationError::PositiveOverflow => { "Error converting duration: value overflows the positive value range" } TryFromDurationError::NegativeOverflow => { "Error converting duration: value overflows the negative value range" } }; description.fmt(f) } } #[cfg(test)] mod tests { use rstest::rstest; #[cfg(feature = "serde")] use serde_test::{assert_tokens, Token}; use super::*; #[rstest] #[case::syntax_error( ParseError::Syntax(10, "Invalid character".to_string()), "Syntax error: Invalid character at column 10" )] #[case::overflow(ParseError::Overflow, "Number overflow")] #[case::time_unit_error( ParseError::TimeUnit(10, "Found invalid 'y'".to_string()), "Time unit error: Found invalid 'y' at column 10" )] #[case::negative_exponent_overflow_error( ParseError::NegativeExponentOverflow, "Negative exponent overflow: Minimum is -32768" )] #[case::positive_exponent_overflow_error( ParseError::PositiveExponentOverflow, "Positive exponent overflow: Maximum is +32767" )] #[case::negative_number_error(ParseError::NegativeNumber, "Number was negative")] #[case::invalid_input( ParseError::InvalidInput("Unexpected".to_string()), "Invalid input: Unexpected" )] #[case::empty(ParseError::Empty, "Empty input")] fn test_error_messages_parse_error(#[case] error: ParseError, #[case] expected: &str) { assert_eq!(error.to_string(), expected); } #[rstest] #[case::negative_overflow(TryFromDurationError::NegativeOverflow, ParseError::Overflow)] #[case::positive_overflow(TryFromDurationError::PositiveOverflow, ParseError::Overflow)] #[case::negative_number(TryFromDurationError::NegativeDuration, ParseError::NegativeNumber)] fn test_from_for_parse_error(#[case] from: TryFromDurationError, #[case] expected: ParseError) { assert_eq!(ParseError::from(from), expected); } #[rstest] #[case::negative_number( TryFromDurationError::NegativeDuration, "Error converting duration: value is negative" )] #[case::positive_overflow( TryFromDurationError::PositiveOverflow, "Error converting duration: value overflows the positive value range" )] #[case::positive_overflow( TryFromDurationError::NegativeOverflow, "Error converting duration: value overflows the negative value range" )] fn test_error_messages_try_from_duration_error( #[case] error: TryFromDurationError, #[case] expected: &str, ) { assert_eq!(error.to_string(), expected); } #[cfg(feature = "serde")] #[test] fn test_serde_try_from_duration_error() { let error = TryFromDurationError::NegativeDuration; assert_tokens( &error, &[ Token::Enum { name: "TryFromDurationError", }, Token::Str("NegativeDuration"), Token::Unit, ], ) } #[cfg(feature = "serde")] #[test] fn test_serde_parse_error() { let error = ParseError::Empty; assert_tokens( &error, &[ Token::Enum { name: "ParseError" }, Token::Str("Empty"), Token::Unit, ], ) } } fundu-1.0.0/src/lib.rs000064400000000000000000000351771046102023000126630ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! # Overview //! //! Parse a rust string into a [`crate::Duration`]. //! //! `fundu` is a configurable, precise and blazingly fast duration parser //! //! * with the flexibility to customize [`TimeUnit`]s, or even create own time units with a //! [`CustomTimeUnit`] (the `custom` feature is needed) //! * without floating point calculations. What you put in is what you get out. //! * with sound limit handling. Infinity and numbers larger than [`Duration::MAX`] evaluate to //! [`Duration::MAX`]. Numbers `x` with `abs(x) < 1e-18` evaluate to [`Duration::ZERO`]. //! * with many options to customize the number format and other aspects of the parsing process like //! parsing negative durations //! * and with meaningful error messages //! //! # Fundu's Duration //! //! This [`crate::Duration`] is returned by the parser of this library and can be converted to a //! [`std::time::Duration`] and if the feature is activated into a [`time::Duration`] respectively //! [`chrono::Duration`]. This crates duration is a superset of the aforementioned durations ranging //! from `-Duration::MAX` to `+Duration::MAX` with `Duration::MAX` having `u64::MAX` seconds and //! `999_999_999` nano seconds. Converting to fundu's duration from any of the above durations with //! `From` or `Into` is lossless. Converting from [`crate::Duration`] to any of the other durations //! can overflow or can't be negative, so conversions must be done with `TryFrom` or `TryInto`. //! Additionally, fundu's duration implements [`SaturatingInto`] for the above durations, so //! conversions saturate at the maximum or minimum of these durations. //! //! # Features //! //! ## `standard` //! //! The `standard` feature exposes the [`DurationParser`] and [`DurationParserBuilder`] structs //! with time units which can be customized. However, the `identifier` for each [`TimeUnit`] is //! fixed. //! //! ## `custom` //! //! The `custom` feature provides a [`CustomDurationParser`] and [`CustomDurationParserBuilder`] //! with fully customizable identifiers for each [`TimeUnit`]. With the [`CustomDurationParser`] //! it is also possible to define completely new time units, a [`CustomTimeUnit`]. //! //! ## `chrono` and `time` //! //! The `chrono` feature activates methods of [`Duration`] to convert from and to a //! [`chrono::Duration`]. The `time` feature activates methods of [`Duration`] to convert from and //! to a [`time::Duration`]. Both of these durations allow negative durations. Parsing negative //! numbers can be enabled with [`DurationParser::allow_negative`] or //! [`CustomDurationParser::allow_negative`] independently of the `chrono` or `time` feature. //! //! ## `serde` //! //! Some structs and enums can be serialized and deserialized with `serde` if the feature is //! activated. //! //! # Configuration and Format //! //! The `standard` parser can be configured to accept strings with a default set of time units //! [`DurationParser::new`], with all time units [`DurationParser::with_all_time_units`] or without //! [`DurationParser::without_time_units`]. A custom set of time units is also possible with //! [`DurationParser::with_time_units`]. All these parsers accept strings such as //! //! * `1.41` //! * `42` //! * `2e-8`, `2e+8` (or likewise `2.0e8`) //! * `.5` or likewise `0.5` //! * `3.` or likewise `3.0` //! * `inf`, `+inf`, `infinity`, `+infinity` //! //! All alphabetic characters are matched case-insensitive, so `InFINity` or `2E8` are valid input //! strings. Additionally, depending on the chosen set of time units one of the following time //! units (the first column) is accepted. //! //! | [`TimeUnit`] | default id | is default time unit //! | --------------- | ----------:|:--------------------: //! | Nanosecond | ns | yes //! | Microsecond | Ms | yes //! | Millisecond | ms | yes //! | Second | s | yes //! | Minute | m | yes //! | Hour | h | yes //! | Day | d | yes //! | Week | w | yes //! | Month | M | no //! | Year | y | no //! //! If no time unit is given and not specified otherwise with [`DurationParser::default_unit`] //! then `s` (= [`Second`]) is assumed. Some accepted strings with time units //! //! * `31.2s` //! * `200000Ms` //! * `3.14e8w` //! * ... //! //! Per default there is no whitespace allowed between the number and the [`TimeUnit`], but this //! behavior can be changed with [`DurationParser::allow_delimiter`]. //! //! # Format specification //! //! The `TimeUnit`s and every `Char` is case-sensitive, all other alphabetic characters are //! case-insensitive //! //! ```text //! Durations ::= Duration [ DurationStartingWithDigit //! | ( Delimiter+ ( Duration | Conjunction )) //! ]* ; //! Conjunction ::= ConjunctionWord (( Delimiter+ Duration ) | DurationStartingWithDigit ) ; //! ConjunctionWord ::= Char+ ; //! Duration ::= Sign? ( 'inf' | 'infinity' //! | TimeKeyword //! | Number [ TimeUnit [ Delimiter+ 'ago' ]] //! ) ; //! DurationStartingWithDigit ::= //! ( Digit+ | Digit+ '.' Digit* ) Exp? [ TimeUnit [ Delimiter+ 'ago' ]] ; //! TimeUnit ::= ns | Ms | ms | s | m | h | d | w | M | y | CustomTimeUnit ; //! CustomTimeUnit ::= Char+ ; //! TimeKeyword ::= Char+ ; //! Number ::= ( Digits Exp? ) | Exp ; //! Digits ::= Digit+ | Digit+ '.' Digit* | Digit* '.' Digit+ //! Exp ::= 'e' Sign? Digit+ ; //! Sign ::= [+-] ; //! Digit ::= [0-9] ; //! Char ::= ? a valid UTF-8 character ? ; //! Delimiter ::= ? a closure with the signature u8 -> bool ? ; //! ``` //! //! Special cases which are not displayed in the specification: //! //! * Parsing multiple `Durations` must be enabled with `parse_multiple`. The [`Delimiter`] and //! `ConjunctionWords` can also be defined with the `parse_multiple` method. Multiple `Durations` //! are summed up following the saturation rule below //! * A negative [`Duration`] (`Sign` == `-`), including negative infinity is not allowed as long as //! the `allow_negative` option is not enabled. For exceptions see the next point. //! * Numbers `x` (positive and negative) close to `0` (`abs(x) < 1e-18`) are treated as `0` //! * Positive infinity and numbers exceeding [`Duration::MAX`] saturate at [`Duration::MAX`]. If //! the `allow_negative` option is enabled, negative infinity and numbers falling below //! [`Duration::MIN`] saturate at [`Duration::MIN`]. //! * The exponent must be in the range `-32768 <= Exp <= 32767` //! * If `allow_delimiter` is set then any [`Delimiter`] is allowed between the `Number` and //! `TimeUnit`. //! * If `number_is_optional` is enabled then the `Number` is optional but the `TimeUnit` must be //! present instead. //! * The `ago` keyword must be enabled in the parser with `allow_ago` //! * [`TimeKeyword`] is a `custom` feature which must be enabled by adding a [`TimeKeyword`] to the //! [`CustomDurationParser`] //! * [CustomTimeUnit`] is a `custom` feature which lets you define own time units //! //! # Examples //! //! If only the default configuration is required once, the [`parse_duration`] method can be used. //! //! ```rust //! use std::time::Duration; //! //! use fundu::parse_duration; //! //! let input = "1.0e2s"; //! assert_eq!(parse_duration(input).unwrap(), Duration::new(100, 0)); //! ``` //! //! When a customization of the accepted [`TimeUnit`]s is required, then [`DurationParser`] can be //! used. //! //! ```rust //! use fundu::{Duration, DurationParser}; //! //! let input = "3m"; //! assert_eq!( //! DurationParser::with_all_time_units().parse(input).unwrap(), //! Duration::positive(180, 0) //! ); //! ``` //! //! When no time units are configured, seconds is assumed. //! //! ```rust //! use fundu::{Duration, DurationParser}; //! //! let input = "1.0e2"; //! assert_eq!( //! DurationParser::without_time_units().parse(input).unwrap(), //! Duration::positive(100, 0) //! ); //! ``` //! //! However, the following will return an error because `y` (Years) is not a default time unit: //! //! ```rust //! use fundu::DurationParser; //! //! let input = "3y"; //! assert!(DurationParser::new().parse(input).is_err()); //! ``` //! //! The parser is reusable and the set of time units is fully customizable //! //! ```rust //! use fundu::TimeUnit::*; //! use fundu::{Duration, DurationParser}; //! //! let parser = DurationParser::with_time_units(&[NanoSecond, Minute, Hour]); //! //! assert_eq!(parser.parse("9e3ns").unwrap(), Duration::positive(0, 9000)); //! assert_eq!(parser.parse("10m").unwrap(), Duration::positive(600, 0)); //! assert_eq!(parser.parse("1.1h").unwrap(), Duration::positive(3960, 0)); //! assert_eq!(parser.parse("7").unwrap(), Duration::positive(7, 0)); //! ``` //! //! Setting the default time unit (if no time unit is given in the input string) to something //! different than seconds is also easily possible //! //! ```rust //! use fundu::TimeUnit::*; //! use fundu::{Duration, DurationParser}; //! //! assert_eq!( //! DurationParser::without_time_units() //! .default_unit(MilliSecond) //! .parse("1000") //! .unwrap(), //! Duration::positive(1, 0) //! ); //! ``` //! //! The identifiers for time units can be fully customized with any number of valid //! [utf-8](https://en.wikipedia.org/wiki/UTF-8) sequences if the `custom` feature is activated: //! //! ```rust //! use fundu::TimeUnit::*; //! use fundu::{CustomTimeUnit, CustomDurationParser, Duration}; //! //! let parser = CustomDurationParser::with_time_units(&[ //! CustomTimeUnit::with_default(MilliSecond, &["χιλιοστό του δευτερολέπτου"]), //! CustomTimeUnit::with_default(Second, &["s", "secs"]), //! CustomTimeUnit::with_default(Hour, &["⏳"]), //! ]); //! //! assert_eq!(parser.parse(".3χιλιοστό του δευτερολέπτου"), Ok(Duration::positive(0, 300_000))); //! assert_eq!(parser.parse("1e3secs"), Ok(Duration::positive(1000, 0))); //! assert_eq!(parser.parse("1.1⏳"), Ok(Duration::positive(3960, 0))); //! ``` //! //! The `custom` feature can be used to customize a lot more. See the documentation of the exported //! items of the `custom` feature (like [`CustomTimeUnit`], [`TimeKeyword`]) for more information. //! //! Also, `fundu` tries to give informative error messages //! //! ```rust //! use fundu::DurationParser; //! //! assert_eq!( //! DurationParser::without_time_units() //! .parse("1y") //! .unwrap_err() //! .to_string(), //! "Time unit error: No time units allowed but found: 'y' at column 1" //! ); //! ``` //! //! The number format can be easily adjusted to your needs. For example to allow numbers being //! optional, allow some ascii whitespace between the number and the time unit and restrict the //! number format to whole numbers, without fractional part and an exponent: //! //! ```rust //! use fundu::TimeUnit::*; //! use fundu::{Duration, DurationParser, ParseError}; //! //! const PARSER: DurationParser = DurationParser::builder() //! .time_units(&[NanoSecond]) //! .allow_delimiter(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) //! .number_is_optional() //! .disable_fraction() //! .disable_exponent() //! .build(); //! //! assert_eq!(PARSER.parse("ns").unwrap(), Duration::positive(0, 1)); //! assert_eq!( //! PARSER.parse("1000\t\n\r ns").unwrap(), //! Duration::positive(0, 1000) //! ); //! //! assert_eq!( //! PARSER.parse("1.0ns").unwrap_err(), //! ParseError::Syntax(1, "No fraction allowed".to_string()) //! ); //! assert_eq!( //! PARSER.parse("1e9ns").unwrap_err(), //! ParseError::Syntax(1, "No exponent allowed".to_string()) //! ); //! ``` //! //! [`time::Duration`]: //! [`Duration::MAX`]: [`std::Duration::MAX`] //! [`Duration::ZERO`]: [`std::Duration::ZERO`] //! [`NanoSecond`]: [`TimeUnit::NanoSecond`] //! [`MicroSecond`]: [`TimeUnit::MicroSecond`] //! [`MilliSecond`]: [`TimeUnit::MilliSecond`] //! [`Second`]: [`TimeUnit::Second`] //! [`Minute`]: [`TimeUnit::Minute`] //! [`Hour`]: [`TimeUnit::Hour`] //! [`Day`]: [`TimeUnit::Day`] //! [`Week`]: [`TimeUnit::Week`] //! [`Month`]: [`TimeUnit::Month`] //! [`Year`]: [`TimeUnit::Year`] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc(test(attr(warn(unused))))] #![doc(test(attr(allow(unused_extern_crates))))] mod config; #[cfg(feature = "custom")] mod custom; mod error; mod parse; #[cfg(feature = "standard")] mod standard; mod time; mod util; pub use config::Delimiter; #[cfg(feature = "custom")] pub use custom::{ builder::CustomDurationParserBuilder, parser::CustomDurationParser, time_units::{ CustomTimeUnit, TimeKeyword, DEFAULT_ALL_TIME_UNITS, DEFAULT_TIME_UNITS, SYSTEMD_TIME_UNITS, }, }; pub use error::{ParseError, TryFromDurationError}; #[cfg(test)] pub use rstest_reuse; #[cfg(feature = "standard")] pub use standard::{ builder::DurationParserBuilder, parser::parse_duration, parser::DurationParser, }; pub use crate::time::{ Duration, Multiplier, SaturatingInto, TimeUnit, DEFAULT_ID_DAY, DEFAULT_ID_HOUR, DEFAULT_ID_MICRO_SECOND, DEFAULT_ID_MILLI_SECOND, DEFAULT_ID_MINUTE, DEFAULT_ID_MONTH, DEFAULT_ID_NANO_SECOND, DEFAULT_ID_SECOND, DEFAULT_ID_WEEK, DEFAULT_ID_YEAR, }; #[cfg(test)] mod tests { use super::*; #[test] fn test_send() { fn assert_send() {} assert_send::(); assert_send::(); assert_send::(); assert_send::(); assert_send::(); assert_send::(); #[cfg(feature = "custom")] { assert_send::(); assert_send::(); assert_send::(); assert_send::(); } #[cfg(feature = "standard")] { assert_send::(); assert_send::(); } } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); assert_sync::(); assert_sync::(); assert_sync::(); assert_sync::(); assert_sync::(); #[cfg(feature = "custom")] { assert_sync::(); assert_sync::(); assert_sync::(); assert_sync::(); } #[cfg(feature = "standard")] { assert_sync::(); assert_sync::(); } } } fundu-1.0.0/src/parse.rs000064400000000000000000001535411046102023000132230ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT //! This module is the working horse of the parser. Public interfaces to the parser are located in //! the main library `lib.rs`. use std::cmp::Ordering::{Equal, Greater, Less}; use std::str::Utf8Error; use std::time::Duration as StdDuration; use crate::config::{Config, Delimiter, DEFAULT_CONFIG}; use crate::error::ParseError; use crate::time::{Duration, Multiplier, TimeUnit, TimeUnitsLike}; use crate::util::POW10; const ATTOS_PER_SEC: u64 = 1_000_000_000_000_000_000; const ATTOS_PER_NANO: u64 = 1_000_000_000; #[derive(Debug, PartialEq, Eq)] pub(crate) struct Parser<'a> { pub(crate) config: Config<'a>, } impl<'a> Parser<'a> { pub(crate) const fn new() -> Self { Self { config: DEFAULT_CONFIG, } } pub(crate) const fn with_config(config: Config<'a>) -> Self { Self { config } } fn parse_multiple( &self, source: &str, delimiter: Delimiter, conjunctions: &[&str], time_units: &dyn TimeUnitsLike, keywords: Option<&dyn TimeUnitsLike>, ) -> Result { let mut duration = Duration::ZERO; let mut parser = &mut ReprParserMultiple::new(source, delimiter, conjunctions); loop { let (mut duration_repr, maybe_parser) = parser.parse(&self.config, time_units, keywords)?; let parsed_duration = duration_repr.parse()?; duration = if !self.config.allow_negative && parsed_duration.is_negative() { return Err(ParseError::NegativeNumber); } else if parsed_duration.is_zero() { duration } else if duration.is_zero() { parsed_duration } else { duration.saturating_add(parsed_duration) }; match maybe_parser { Some(p) => parser = p, None => break Ok(duration), } } } fn parse_single( &self, source: &str, time_units: &dyn TimeUnitsLike, keywords: Option<&dyn TimeUnitsLike>, ) -> Result { ReprParserSingle::new(source) .parse(&self.config, time_units, keywords) .and_then(|mut duration_repr| { duration_repr.parse().and_then(|duration| { if !self.config.allow_negative && duration.is_negative() { Err(ParseError::NegativeNumber) } else { Ok(duration) } }) }) } /// Parse the `source` string into a saturating [`crate::time::Duration`] #[inline] pub(crate) fn parse( &self, source: &str, time_units: &dyn TimeUnitsLike, keywords: Option<&dyn TimeUnitsLike>, ) -> Result { if let Some(delimiter) = self.config.parse_multiple_delimiter { self.parse_multiple( source, delimiter, self.config.parse_multiple_conjunctions.unwrap_or_default(), time_units, keywords, ) } else { self.parse_single(source, time_units, keywords) } } } trait Parse8Digits { // This method is based on the work of Johnny Lee and his blog post // https://johnnylee-sde.github.io/Fast-numeric-string-to-int unsafe fn parse_8_digits(digits: &[u8]) -> u64 { // cov:excl-start debug_assert!( digits.len() >= 8, "Call this method only if digits has length >= 8" ); // cov:excl-stop let ptr = digits.as_ptr() as *const u64; let mut num = u64::from_le(ptr.read_unaligned()); num = ((num & 0x0F0F0F0F0F0F0F0F).wrapping_mul(2561)) >> 8; num = ((num & 0x00FF00FF00FF00FF).wrapping_mul(6553601)) >> 16; num = ((num & 0x0000FFFF0000FFFF).wrapping_mul(42949672960001)) >> 32; num } } #[derive(Debug, PartialEq, Eq, Default)] struct Whole(usize, usize); impl Parse8Digits for Whole {} impl Whole { #[inline] fn parse_slice(mut seconds: u64, digits: &[u8]) -> Result { if digits.len() >= 8 { let mut iter = digits.chunks_exact(8); for digits in iter.by_ref() { // SAFETY: We have chunks of exactly 8 bytes match seconds .checked_mul(100_000_000) .and_then(|s| s.checked_add(unsafe { Self::parse_8_digits(digits) })) { Some(s) => seconds = s, None => { return Err(ParseError::Overflow); } } } for num in iter.remainder() { match seconds .checked_mul(10) .and_then(|s| s.checked_add((*num - b'0') as u64)) { Some(s) => seconds = s, None => { return Err(ParseError::Overflow); } } } } else { for num in digits { match seconds .checked_mul(10) .and_then(|s| s.checked_add((*num - b'0') as u64)) { Some(s) => seconds = s, None => { return Err(ParseError::Overflow); } } } } Ok(seconds) } fn parse( &self, digits: &[u8], append: Option<&[u8]>, zeros: Option, ) -> Result { if digits.is_empty() && append.is_none() { return Ok(0); } let seconds = Self::parse_slice(0, digits).and_then(|s| match append { Some(append) => Self::parse_slice(s, append), None => Ok(s), })?; if seconds == 0 { Ok(0) } else { match zeros { Some(num_zeros) if num_zeros > 0 => match POW10.get(num_zeros) { Some(pow) => Ok(seconds.saturating_mul(*pow)), None => Err(ParseError::Overflow), }, Some(_) | None => Ok(seconds), } } } #[inline] fn len(&self) -> usize { self.1 - self.0 } } #[derive(Debug, PartialEq, Eq, Default)] struct Fract(usize, usize); impl Parse8Digits for Fract {} impl Fract { #[inline] fn parse_slice(mut multi: u64, num_skip: usize, digits: &[u8]) -> (u64, u64) { let mut attos = 0; let len = digits.len(); if multi >= 100_000_000 && len >= 8 { let max = 18usize.saturating_sub(num_skip); let mut iter = digits .get(0..if len > max { max } else { len }) .unwrap() .chunks_exact(8); for digits in iter.by_ref() { multi /= 100_000_000; // SAFETY: The length of digits is exactly 8 attos += unsafe { Self::parse_8_digits(digits) } * multi; } for num in iter.remainder() { multi /= 10; attos += (*num - b'0') as u64 * multi; } } else if multi > 0 && len > 0 { for num in digits { multi /= 10; if multi == 0 { return (0, attos); } attos += (*num - b'0') as u64 * multi; } // else would be reached if multi or len are zero but these states are already handled // in parse } // cov:excl-line (multi, attos) } fn parse(&self, digits: &[u8], prepend: Option<&[u8]>, zeros: Option) -> u64 { if digits.is_empty() && prepend.is_none() { return 0; } let num_zeros = zeros.unwrap_or_default(); let pow = match POW10.get(num_zeros) { Some(pow) => pow, None => return 0, }; let multi = ATTOS_PER_SEC / pow; if multi == 0 { return 0; } match prepend { Some(prepend) if num_zeros + prepend.len() >= 18 => { let (_, attos) = Self::parse_slice(multi, num_zeros, prepend); attos } Some(prepend) if !prepend.is_empty() => { let (multi, attos) = Self::parse_slice(multi, num_zeros, prepend); let (_, remainder) = Self::parse_slice(multi, num_zeros + prepend.len(), digits); attos + remainder } Some(_) | None => { let (_, attos) = Self::parse_slice(multi, num_zeros, digits); attos } } } #[inline] fn len(&self) -> usize { self.1 - self.0 } } #[derive(Debug, Default)] pub(crate) struct DurationRepr<'input> { unit: TimeUnit, number_is_optional: bool, is_negative: bool, is_infinite: bool, whole: Option, fract: Option, input: &'input [u8], exponent: i16, multiplier: Multiplier, } impl<'input> DurationRepr<'input> { pub(crate) fn parse(&mut self) -> Result { if self.is_infinite { return Ok(Duration::from_std(self.is_negative, StdDuration::MAX)); } let (digits, whole, fract) = match (self.whole.take(), self.fract.take()) { (None, None) if self.number_is_optional => { let digits = Some(vec![0x31]); (digits, Whole(0, 1), Fract(1, 1)) } (None, None) => unreachable!(), // cov:excl-line (None, Some(fract)) => (None, Whole(fract.0, fract.0), fract), (Some(whole), None) => { let fract_start_and_end = whole.1; (None, whole, Fract(fract_start_and_end, fract_start_and_end)) } (Some(whole), Some(fract)) => (None, whole, fract), }; let digits = digits.as_ref().map_or(self.input, |d| d.as_slice()); // Panic on overflow during the multiplication of the multipliers or adding the exponents let Multiplier(coefficient, exponent) = self.unit.multiplier() * self.multiplier; let exponent = exponent as i32 + self.exponent as i32; // The maximum absolute value of the exponent is `2 * abs(i16::MIN)`, so it is safe to cast // to usize let exponent_abs: usize = exponent.unsigned_abs().try_into().unwrap(); // We're operating on slices to minimize runtime costs. Applying the exponent before parsing // to integers is necessary, since the exponent can move digits into the to be considered // final integer domain. let (seconds, attos) = match exponent.cmp(&0) { Less if whole.len() > exponent_abs => { let seconds = whole.parse(&digits[whole.0..whole.1 - exponent_abs], None, None); let attos = if seconds.is_ok() { Some(fract.parse( &digits[fract.0..fract.1], Some(&digits[whole.1 - exponent_abs..whole.1]), None, )) } else { None }; (Some(seconds), attos) } Less => { let attos = Some(fract.parse( &digits[fract.0..fract.1], Some(&digits[whole.0..whole.1]), Some(exponent_abs - whole.len()), )); (None, attos) } Equal => { let seconds = whole.parse(&digits[whole.0..whole.1], None, None); let attos = if seconds.is_ok() { Some(fract.parse(&digits[fract.0..fract.1], None, None)) } else { None }; (Some(seconds), attos) } Greater if fract.len() > exponent_abs => { let seconds = whole.parse( &digits[whole.0..whole.1], Some(&digits[fract.0..fract.0 + exponent_abs]), None, ); let attos = if seconds.is_ok() { Some(fract.parse(&digits[fract.0 + exponent_abs..fract.1], None, None)) } else { None }; (Some(seconds), attos) } Greater => { let seconds = whole.parse( &digits[whole.0..whole.1], Some(&digits[fract.0..fract.1]), Some(exponent_abs - fract.len()), ); (Some(seconds), None) } }; let duration_is_negative = self.is_negative ^ coefficient.is_negative(); // Finally, parse the seconds and atto seconds and interpret a seconds overflow as // maximum `Duration`. let (seconds, attos) = match seconds { Some(result) => match result { Ok(seconds) => (seconds, attos.unwrap_or_default()), Err(ParseError::Overflow) if duration_is_negative => { return Ok(Duration::MIN); } Err(ParseError::Overflow) => { return Ok(Duration::MAX); } // only ParseError::Overflow is returned by `Seconds::parse` Err(_) => unreachable!(), // cov:excl-line }, None => (0, attos.unwrap_or_default()), }; // allow -0 or -0.0 etc., or in general numbers x with abs(x) < 1e-18 and interpret them // as zero duration if seconds == 0 && attos == 0 { Ok(Duration::ZERO) } else if coefficient == 1 || coefficient == -1 { Ok(Duration::from_std( duration_is_negative, StdDuration::new(seconds, (attos / ATTOS_PER_NANO) as u32), )) } else { let unsigned_coefficient = coefficient.unsigned_abs(); let attos = attos as u128 * (unsigned_coefficient as u128); Ok( match seconds .checked_mul(unsigned_coefficient) .and_then(|s| s.checked_add((attos / (ATTOS_PER_SEC as u128)) as u64)) { Some(s) => Duration::from_std( duration_is_negative, StdDuration::new( s, ((attos / (ATTOS_PER_NANO as u128)) % 1_000_000_000) as u32, ), ), None if duration_is_negative => Duration::MIN, None => Duration::MAX, }, ) } } } struct BytesRange(usize, usize); struct Bytes<'a> { current_pos: usize, // keep first. Has better performance. current_byte: Option<&'a u8>, input: &'a [u8], } impl<'a> Bytes<'a> { #[inline] fn new(input: &'a [u8]) -> Self { Self { current_pos: 0, current_byte: input.first(), input, } } #[inline] fn advance(&mut self) { self.current_pos += 1; self.current_byte = self.input.get(self.current_pos); } #[inline] unsafe fn advance_by(&mut self, num: usize) { self.current_pos += num; self.current_byte = self.input.get(self.current_pos); } fn advance_to(&mut self, delimiter: F) -> &'a [u8] where F: Fn(u8) -> bool, { let start = self.current_pos; while let Some(byte) = self.current_byte { if delimiter(*byte) { break; } else { self.advance() } } &self.input[start..self.current_pos] } #[inline] fn peek(&self, num: usize) -> Option<&[u8]> { self.input.get(self.current_pos..self.current_pos + num) } #[inline] fn get_remainder(&self) -> &[u8] { &self.input[self.current_pos..] } #[inline] unsafe fn get_remainder_str_unchecked(&self) -> &str { std::str::from_utf8_unchecked(self.get_remainder()) } #[inline] fn get_current_str(&self, start: usize) -> Result<&str, Utf8Error> { std::str::from_utf8(&self.input[start..self.current_pos]) } #[inline] fn finish(&mut self) { self.current_pos = self.input.len(); self.current_byte = None } #[inline] fn reset(&mut self, position: usize) { self.current_pos = position; self.current_byte = self.input.get(position); } #[inline] fn parse_digit(&mut self) -> Option { self.current_byte.and_then(|byte| { let digit = byte.wrapping_sub(b'0'); if digit < 10 { self.advance(); Some(*byte) } else { None } }) } fn parse_digits_strip_zeros(&mut self) -> BytesRange { debug_assert!( self.current_byte .map_or(false, |byte| byte.is_ascii_digit()) ); // cov:excl-stop const ASCII_EIGHT_ZEROS: u64 = 0x3030303030303030; let mut start = self.current_pos; let mut strip_leading_zeros = true; while let Some(eight) = self.parse_8_digits() { if strip_leading_zeros { if eight == ASCII_EIGHT_ZEROS { start += 8; } else { strip_leading_zeros = false; // eight is little endian so we need to count the trailing zeros let leading_zeros = (eight - ASCII_EIGHT_ZEROS).trailing_zeros() / 8; start += leading_zeros as usize; } } } while let Some(byte) = self.current_byte { let digit = byte.wrapping_sub(b'0'); if digit < 10 { if strip_leading_zeros { if digit == 0 { start += 1; } else { strip_leading_zeros = false; } } self.advance(); } else { break; } } BytesRange(start, self.current_pos) } fn parse_digits(&mut self) -> BytesRange { debug_assert!( self.current_byte .map_or(false, |byte| byte.is_ascii_digit()) ); // cov:excl-stop let start = self.current_pos; while self.parse_8_digits().is_some() {} while self.parse_digit().is_some() {} BytesRange(start, self.current_pos) } /// This method is based on the work of Daniel Lemire and his blog post /// fn parse_8_digits(&mut self) -> Option { self.input .get(self.current_pos..(self.current_pos + 8)) .and_then(|digits| { let ptr = digits.as_ptr() as *const u64; // SAFETY: We just ensured there are 8 bytes let num = u64::from_le(unsafe { ptr.read_unaligned() }); if (num & (num.wrapping_add(0x0606060606060606)) & 0xf0f0f0f0f0f0f0f0) == 0x3030303030303030 { unsafe { self.advance_by(8) } Some(num) } else { None } }) } #[inline] fn next_is_ignore_ascii_case(&self, word: &[u8]) -> bool { self.peek(word.len()) .map_or(false, |bytes| bytes.eq_ignore_ascii_case(word)) } #[inline] fn is_end_of_input(&self) -> bool { self.current_byte.is_none() } #[inline] fn check_end_of_input(&self) -> Result<(), ParseError> { match self.current_byte { Some(byte) => Err(ParseError::Syntax( self.current_pos, format!("Expected end of input but found: '{}'", *byte as char), )), None => Ok(()), } } fn try_consume_delimiter(&mut self, delimiter: Delimiter) -> Result<(), ParseError> { debug_assert!(delimiter(*self.current_byte.unwrap())); // cov:excl-line if self.current_pos == 0 { return Err(ParseError::Syntax( 0, "Input may not start with a delimiter".to_string(), )); } let start = self.current_pos; self.advance(); while let Some(byte) = self.current_byte { if delimiter(*byte) { self.advance() } else { break; } } match self.current_byte { Some(_) => Ok(()), None => Err(ParseError::Syntax( start, "Input may not end with a delimiter".to_string(), )), } } } trait ReprParserTemplate<'a> { type Output; fn bytes(&mut self) -> &mut Bytes<'a>; fn make_output(&'a mut self, duration_repr: DurationRepr<'a>) -> Self::Output; fn parse_infinity_remainder( &'a mut self, duration_repr: DurationRepr<'a>, ) -> Result; fn parse_keyword( &mut self, keywords: Option<&dyn TimeUnitsLike>, ) -> Result, ParseError>; fn parse_time_unit( &mut self, config: &Config, time_units: &dyn TimeUnitsLike, ) -> Result, ParseError>; fn parse_number_time_unit( &mut self, duration_repr: &mut DurationRepr<'a>, config: &Config, time_units: &dyn TimeUnitsLike, ) -> Result; fn finalize(&'a mut self, duration_repr: DurationRepr<'a>) -> Result; #[inline] fn parse_whole(&mut self) -> Whole { let BytesRange(start, end) = self.bytes().parse_digits_strip_zeros(); Whole(start, end) } fn parse( &'a mut self, config: &Config, time_units: &dyn TimeUnitsLike, keywords: Option<&dyn TimeUnitsLike>, ) -> Result { if self.bytes().current_byte.is_none() { return Err(ParseError::Empty); } let mut duration_repr = DurationRepr { unit: config.default_unit, input: self.bytes().input, number_is_optional: config.number_is_optional, ..Default::default() }; self.parse_number_sign(&mut duration_repr)?; // parse infinity, keywords or the whole number part of the input match self.bytes().current_byte.cloned() { Some(byte) if byte.is_ascii_digit() => { duration_repr.whole = Some(self.parse_whole()); } Some(byte) if byte == b'.' => {} Some(_) if !config.disable_infinity && self.bytes().next_is_ignore_ascii_case(b"inf") => { // SAFETY: We just checked that there are at least 3 bytes unsafe { self.bytes().advance_by(3) } return self.parse_infinity_remainder(duration_repr); } Some(_) if keywords.is_some() => { if let Some((unit, multi)) = self.parse_keyword(keywords)? { duration_repr.number_is_optional = true; duration_repr.unit = unit; duration_repr.multiplier = multi; return Ok(self.make_output(duration_repr)); } else if config.number_is_optional { // do nothing } else { // SAFETY: The input str is utf-8 and we have only parsed valid utf-8 so far return Err(ParseError::Syntax( self.bytes().current_pos, format!("Invalid input: '{}'", unsafe { self.bytes().get_remainder_str_unchecked() }), )); } } Some(_) if config.number_is_optional => {} Some(_) => { // SAFETY: The input str is utf-8 and we have only parsed ascii characters so far return Err(ParseError::Syntax( self.bytes().current_pos, format!("Invalid input: '{}'", unsafe { self.bytes().get_remainder_str_unchecked() }), )); } None => { return Err(ParseError::Syntax( self.bytes().current_pos, "Unexpected end of input".to_string(), )); } } if !self.parse_number_fraction(&mut duration_repr, config.disable_fraction)? { return Ok(self.make_output(duration_repr)); } if !self.parse_number_exponent(&mut duration_repr, config.disable_exponent)? { return Ok(self.make_output(duration_repr)); } if !self.parse_number_delimiter(config.allow_delimiter)? { return Ok(self.make_output(duration_repr)); } if !self.parse_number_time_unit(&mut duration_repr, config, time_units)? { // Currently unreachable but let's keep it for clarity and safety especially because // the parse_number_time_unit must be implemented by this traits implementations return Ok(self.make_output(duration_repr)); // cov:excl-line } self.finalize(duration_repr) } /// Parse and consume the sign if present. Return true if sign is negative. fn parse_sign_is_negative(&mut self) -> Result { let bytes = self.bytes(); match bytes.current_byte { Some(byte) if *byte == b'+' => { bytes.advance(); Ok(false) } Some(byte) if *byte == b'-' => { bytes.advance(); Ok(true) } Some(_) => Ok(false), None => Err(ParseError::Syntax( bytes.current_pos, "Unexpected end of input".to_string(), )), } } fn parse_number_sign(&mut self, duration_repr: &mut DurationRepr) -> Result<(), ParseError> { if self.parse_sign_is_negative()? { duration_repr.is_negative = true; } Ok(()) } #[inline] fn parse_fract(&mut self) -> Fract { let BytesRange(start, end) = self.bytes().parse_digits(); Fract(start, end) } fn parse_number_fraction( &mut self, duration_repr: &mut DurationRepr<'a>, disable_fraction: bool, ) -> Result { let bytes = self.bytes(); match bytes.current_byte { Some(byte) if *byte == b'.' && !disable_fraction => { bytes.advance(); let fract = match bytes.current_byte { Some(byte) if byte.is_ascii_digit() => Some(self.parse_fract()), Some(_) | None if duration_repr.whole.is_none() => { // Use the decimal point as anchor for the error position. Subtraction by 1 // is safe since we were advancing by one before. return Err(ParseError::Syntax( bytes.current_pos - 1, "Either the whole number part or the fraction must be present" .to_string(), )); } Some(_) => None, None => return Ok(false), }; duration_repr.fract = fract; Ok(true) } Some(byte) if *byte == b'.' => Err(ParseError::Syntax( bytes.current_pos, "No fraction allowed".to_string(), )), Some(_) => Ok(true), None => Ok(false), } } fn parse_exponent(&mut self) -> Result { let is_negative = self.parse_sign_is_negative()?; let bytes = self.bytes(); let mut exponent = 0i16; let start = bytes.current_pos; while let Some(byte) = bytes.current_byte { let digit = byte.wrapping_sub(b'0'); if digit < 10 { exponent = if is_negative { match exponent .checked_mul(10) .and_then(|e| e.checked_sub(digit as i16)) { Some(exponent) => exponent, None => return Err(ParseError::NegativeExponentOverflow), } } else { match exponent .checked_mul(10) .and_then(|e| e.checked_add(digit as i16)) { Some(exponent) => exponent, None => return Err(ParseError::PositiveExponentOverflow), } }; bytes.advance(); } else { break; } } if bytes.current_pos - start > 0 { Ok(exponent) } else if bytes.is_end_of_input() { Err(ParseError::Syntax( bytes.current_pos, "Expected exponent but reached end of input".to_string(), )) } else { Err(ParseError::Syntax( bytes.current_pos, "The exponent must have at least one digit".to_string(), )) } } fn parse_number_exponent( &mut self, duration_repr: &mut DurationRepr<'a>, disable_exponent: bool, ) -> Result { let bytes = self.bytes(); match bytes.current_byte { Some(byte) if byte.eq_ignore_ascii_case(&b'e') && !disable_exponent => { if duration_repr.whole.is_none() && duration_repr.fract.is_none() { Err(ParseError::Syntax( bytes.current_pos, "Exponent must have a mantissa".to_string(), )) } else { bytes.advance(); duration_repr.exponent = self.parse_exponent()?; Ok(true) } } Some(byte) if byte.eq_ignore_ascii_case(&b'e') => Err(ParseError::Syntax( bytes.current_pos, "No exponent allowed".to_string(), )), Some(_) => Ok(true), None => Ok(false), } } fn parse_number_delimiter(&mut self, delimiter: Option) -> Result { let bytes = self.bytes(); // If allow_delimiter is Some and there are any delimiters between the number and the time // unit, the delimiters are consumed before trying to parse the time units match (bytes.current_byte, delimiter) { (Some(byte), Some(delimiter)) if delimiter(*byte) => { bytes.try_consume_delimiter(delimiter)?; Ok(true) } (Some(_), _) => Ok(true), (None, _) => Ok(false), } } } pub(crate) struct ReprParserSingle<'a> { bytes: Bytes<'a>, } impl<'a> ReprParserSingle<'a> { fn new(input: &'a str) -> Self { Self { bytes: Bytes::new(input.as_bytes()), } } } impl<'a> ReprParserTemplate<'a> for ReprParserSingle<'a> { type Output = DurationRepr<'a>; #[inline] fn bytes(&mut self) -> &mut Bytes<'a> { &mut self.bytes } #[inline] fn make_output(&'a mut self, duration_repr: DurationRepr<'a>) -> Self::Output { duration_repr } #[inline] fn parse_infinity_remainder( &'a mut self, mut duration_repr: DurationRepr<'a>, ) -> Result, ParseError> { if self.bytes.is_end_of_input() { duration_repr.is_infinite = true; return Ok(duration_repr); } let expected = b"inity"; for byte in expected.iter() { match self.bytes.current_byte { Some(current) if current.eq_ignore_ascii_case(byte) => self.bytes.advance(), // wrong character Some(current) => { return Err(ParseError::Syntax( self.bytes.current_pos, format!( "Error parsing infinity: Invalid character '{}'", *current as char ), )); } None => { return Err(ParseError::Syntax( self.bytes.current_pos, "Error parsing infinity: Premature end of input".to_string(), )); } } } duration_repr.is_infinite = true; self.bytes.check_end_of_input().map(|_| duration_repr) } #[inline] fn parse_keyword( &mut self, keywords: Option<&dyn TimeUnitsLike>, ) -> Result, ParseError> { debug_assert!(keywords.is_some()); // cov:excl-line // SAFETY: we've only parsed valid utf-8 up to this point let keyword = unsafe { self.bytes.get_remainder_str_unchecked() }; match keywords.unwrap().get(keyword) { None => Ok(None), some_time_unit => { self.bytes.finish(); Ok(some_time_unit) } } } #[inline] fn parse_time_unit( &mut self, config: &Config, time_units: &dyn TimeUnitsLike, ) -> Result, ParseError> { // cov:excl-start debug_assert!( self.bytes.current_byte.is_some(), "Don't call this function without being sure there's at least 1 byte remaining" ); // cov:excl-stop match config.allow_ago { Some(delimiter) => { let start = self.bytes.current_pos; let string = std::str::from_utf8(self.bytes.advance_to(delimiter)).map_err(|error| { ParseError::TimeUnit( start + error.valid_up_to(), "Invalid utf-8 when applying the delimiter".to_string(), ) })?; let (time_unit, mut multiplier) = if string.is_empty() { return Ok(None); } else { match time_units.get(string) { None => { return Err(ParseError::TimeUnit( start, format!("Invalid time unit: '{string}'"), )); } Some(unit) => unit, } }; // At this point, either there are one or more bytes of which the first is the // delimiter or we've reached the end of input if self.bytes.current_byte.is_some() { self.bytes.try_consume_delimiter(delimiter)?; if self.bytes.next_is_ignore_ascii_case(b"ago") { // SAFETY: We have checked that there are at least 3 bytes unsafe { self.bytes.advance_by(3) }; // We're applying the negation on the multiplier only once so we don't need // the operation to be reflexive and using saturating neg is fine multiplier = multiplier.saturating_neg(); } else { return Err(ParseError::TimeUnit( self.bytes.current_pos, format!("Found unexpected keyword: '{}'", unsafe { self.bytes.get_remainder_str_unchecked() }), )); } }; Ok(Some((time_unit, multiplier))) } None => { // SAFETY: The input of `parse` is &str and therefore valid utf-8 and we have read // only ascii characters up to this point. let string = unsafe { self.bytes.get_remainder_str_unchecked() }; let result = match time_units.get(string) { None => Err(ParseError::TimeUnit( self.bytes.current_pos, format!("Invalid time unit: '{string}'"), )), some_time_unit => Ok(some_time_unit), }; self.bytes.finish(); result } } } #[inline] fn parse_number_time_unit( &mut self, duration_repr: &mut DurationRepr<'a>, config: &Config, time_units: &dyn TimeUnitsLike, ) -> Result { match self.bytes().current_byte.cloned() { Some(_) if !time_units.is_empty() => { if let Some((unit, multi)) = self.parse_time_unit(config, time_units)? { duration_repr.unit = unit; duration_repr.multiplier = multi; } Ok(true) } Some(_) => { // SAFETY: We've parsed only valid utf-8 so far Err(ParseError::TimeUnit( self.bytes.current_pos, format!("No time units allowed but found: '{}'", unsafe { self.bytes.get_remainder_str_unchecked() }), )) } // This branch is excluded from coverage because parsing with parse_number_delimiter // already ensures that there's at least 1 byte. None => Ok(false), // cov:excl-line } } #[inline] fn finalize(&'a mut self, duration_repr: DurationRepr<'a>) -> Result { self.bytes.check_end_of_input().map(|_| duration_repr) } } pub(crate) struct ReprParserMultiple<'a> { bytes: Bytes<'a>, delimiter: Delimiter, conjunctions: &'a [&'a str], } impl<'a> ReprParserMultiple<'a> { fn new(input: &'a str, delimiter: Delimiter, conjunctions: &'a [&'a str]) -> Self { Self { bytes: Bytes::new(input.as_bytes()), delimiter, conjunctions, } } fn try_consume_connection(&mut self) -> Result<(), ParseError> { let delimiter = self.delimiter; debug_assert!(delimiter(*self.bytes.current_byte.unwrap())); self.bytes.try_consume_delimiter(delimiter)?; let start = self.bytes.current_pos; // try_consume_delimiter ensures there's at least one byte here for word in self.conjunctions { if self.bytes.next_is_ignore_ascii_case(word.as_bytes()) { // SAFETY: We're advancing by the amount of bytes of the word we just found unsafe { self.bytes.advance_by(word.len()) }; match self.bytes.current_byte { Some(byte) if delimiter(*byte) => { self.bytes.try_consume_delimiter(delimiter)? } Some(byte) if byte.is_ascii_digit() => {} Some(byte) => { return Err(ParseError::Syntax( self.bytes.current_pos, format!( "A conjunction must be separated by a delimiter or digit but \ found: '{}'", *byte as char ), )); } None => { return Err(ParseError::Syntax( start, format!("Input may not end with a conjunction but found: '{word}'"), )); } } break; } } Ok(()) } } impl<'a> ReprParserTemplate<'a> for ReprParserMultiple<'a> { type Output = (DurationRepr<'a>, Option<&'a mut ReprParserMultiple<'a>>); #[inline] fn bytes(&mut self) -> &mut Bytes<'a> { &mut self.bytes } #[inline] fn make_output(&'a mut self, duration_repr: DurationRepr<'a>) -> Self::Output { (duration_repr, self.bytes().current_byte.map(|_| self)) } #[inline] fn parse_infinity_remainder( &'a mut self, mut duration_repr: DurationRepr<'a>, ) -> Result<(DurationRepr<'a>, Option<&'a mut ReprParserMultiple<'a>>), ParseError> { let delimiter = self.delimiter; match self.bytes.current_byte { Some(byte) if delimiter(*byte) => { duration_repr.is_infinite = true; return self .try_consume_connection() .map(|_| (duration_repr, Some(self))); } Some(_) => {} None => { duration_repr.is_infinite = true; return Ok((duration_repr, None)); } } let expected = "inity"; let start = self.bytes.current_pos; for byte in expected.as_bytes().iter() { match self.bytes.current_byte { Some(current) if current.eq_ignore_ascii_case(byte) => self.bytes.advance(), // wrong character Some(current) => { return Err(ParseError::Syntax( self.bytes.current_pos, format!( "Error parsing infinity: Invalid character '{}'", *current as char ), )); } None => { return Err(ParseError::Syntax( // This subtraction is safe since we're here only if there's at least `inf` // present start - 3, format!( "Error parsing infinity: 'inf{}' is an invalid identifier for infinity", self.bytes.get_current_str(start).unwrap() // unwrap is safe ), )); } } } duration_repr.is_infinite = true; match self.bytes.current_byte { Some(byte) if delimiter(*byte) => { self.try_consume_connection()?; Ok((duration_repr, Some(self))) } Some(byte) => Err(ParseError::Syntax( self.bytes.current_pos, format!( "Error parsing infinity: Expected a delimiter but found '{}'", *byte as char ), )), None => Ok((duration_repr, None)), } } #[inline] fn parse_keyword( &mut self, keywords: Option<&dyn TimeUnitsLike>, ) -> Result, ParseError> { debug_assert!(keywords.is_some()); // cov:excl-line let delimiter = self.delimiter; let start = self.bytes.current_pos; self.bytes .advance_to(|byte: u8| delimiter(byte) || byte.is_ascii_digit()); let keyword = self.bytes.get_current_str(start).map_err(|error| { ParseError::Syntax( start + error.valid_up_to(), "Invalid utf-8 when applying the delimiter".to_string(), ) })?; match keywords.unwrap().get(keyword) { None => { self.bytes.reset(start); Ok(None) } some_time_unit => { if let Some(byte) = self.bytes.current_byte { if delimiter(*byte) { self.try_consume_connection()?; } } Ok(some_time_unit) } } } #[inline] fn parse_time_unit( &mut self, config: &Config, time_units: &dyn TimeUnitsLike, ) -> Result, ParseError> { // cov:excl-start debug_assert!( self.bytes.current_byte.is_some(), "Don't call this function without being sure there's at least 1 byte remaining" ); // cov:excl-stop let start = self.bytes.current_pos; match config.allow_ago { Some(ago_delimiter) => { self.bytes.advance_to(|byte: u8| { ago_delimiter(byte) || (self.delimiter)(byte) || byte.is_ascii_digit() }); } None => { self.bytes .advance_to(|byte: u8| (self.delimiter)(byte) || byte.is_ascii_digit()); } } let string = self.bytes.get_current_str(start).map_err(|error| { ParseError::TimeUnit( start + error.valid_up_to(), "Invalid utf-8 when applying the delimiter".to_string(), ) })?; let (time_unit, mut multiplier) = if string.is_empty() { return Ok(None); } else { match time_units.get(string) { None => { return Err(ParseError::TimeUnit( start, format!("Invalid time unit: '{string}'"), )); } Some(some_time_unit) => some_time_unit, } }; match (self.bytes.current_byte, config.allow_ago) { (Some(byte), Some(delimiter)) if delimiter(*byte) => { let start = self.bytes.current_pos; self.bytes.try_consume_delimiter(delimiter)?; if self.bytes.next_is_ignore_ascii_case(b"ago") { // SAFETY: We know that next is `ago` which has 3 bytes unsafe { self.bytes.advance_by(3) }; // We're applying the negation on the multiplier only once so we don't need // the operation to be reflexive and using saturating neg is fine multiplier = multiplier.saturating_neg(); } else { self.bytes.reset(start); } } _ => {} } if let Some(byte) = self.bytes.current_byte { if (self.delimiter)(*byte) { self.try_consume_connection()?; } } Ok(Some((time_unit, multiplier))) } #[inline] fn parse_number_time_unit( &mut self, duration_repr: &mut DurationRepr<'a>, config: &Config, time_units: &dyn TimeUnitsLike, ) -> Result { match self.bytes().current_byte { Some(_) if !time_units.is_empty() => { if let Some((unit, multi)) = self.parse_time_unit(config, time_units)? { duration_repr.unit = unit; duration_repr.multiplier = multi; } } Some(_) => {} // This branch is excluded from coverage because parse_number_delimiter already ensures // that there's at least 1 byte. None => return Ok(false), // cov:excl-line } Ok(true) } #[inline] fn finalize(&'a mut self, duration_repr: DurationRepr<'a>) -> Result { let delimiter = self.delimiter; match self.bytes().current_byte { Some(byte) if delimiter(*byte) => self .try_consume_connection() .map(|_| (duration_repr, Some(self))), Some(_) => Ok((duration_repr, Some(self))), None => Ok((duration_repr, None)), } } } #[cfg(test)] mod tests { use rstest::rstest; use rstest_reuse::{apply, template}; use super::*; struct TimeUnitsFixture; // cov:excl-start This is just a fixture impl TimeUnitsLike for TimeUnitsFixture { fn is_empty(&self) -> bool { true } fn get(&self, _: &str) -> Option<(TimeUnit, Multiplier)> { None } } // cov:excl-stop #[rstest] #[case::zeros("00000000", Some(0x3030303030303030))] #[case::one("00000001", Some(0x3130303030303030))] #[case::ten_millions("10000000", Some(0x3030303030303031))] #[case::nines("99999999", Some(0x3939393939393939))] fn test_duration_repr_parser_parse_8_digits( #[case] input: &str, #[case] expected: Option, ) { let mut parser = ReprParserSingle::new(input); assert_eq!(parser.bytes.parse_8_digits(), expected); } #[rstest] #[case::empty("", None)] #[case::one_non_digit_char("a0000000", None)] #[case::less_than_8_digits("9999999", None)] fn test_duration_repr_parser_parse_8_digits_when_not_8_digits( #[case] input: &str, #[case] expected: Option, ) { let mut parser = ReprParserSingle::new(input); assert_eq!(parser.bytes.parse_8_digits(), expected); assert_eq!(parser.bytes.get_remainder(), input.as_bytes()); assert_eq!(parser.bytes.current_byte, input.as_bytes().first()); assert_eq!(parser.bytes.current_pos, 0); } #[test] fn test_duration_repr_parser_parse_8_digits_when_more_than_8() { let mut parser = ReprParserSingle::new("00000000a"); assert_eq!(parser.bytes.parse_8_digits(), Some(0x3030303030303030)); assert_eq!(parser.bytes.get_remainder(), &[b'a']); assert_eq!(parser.bytes.current_byte, Some(&b'a')); assert_eq!(parser.bytes.current_pos, 8); } #[template] #[rstest] #[case::zero("0", Whole(1, 1))] #[case::one("1", Whole(0, 1))] #[case::nine("9", Whole(0, 1))] #[case::ten("10", Whole(0, 2))] #[case::eight_leading_zeros("00000000", Whole(8, 8))] #[case::fifteen_leading_zeros("000000000000000", Whole(15, 15))] #[case::ten_with_leading_zeros_when_eight_digits("00000010", Whole(6, 8))] #[case::ten_with_leading_zeros_when_nine_digits("000000010", Whole(7, 9))] #[case::mixed_number("12345", Whole(0, 5))] #[case::max_8_digits("99999999", Whole(0, 8))] #[case::max_8_digits_minus_one("99999998", Whole(0, 8))] #[case::min_nine_digits("100000000", Whole(0, 9))] #[case::min_nine_digits_plus_one("100000001", Whole(0, 9))] #[case::eight_zero_digits_start("0000000011111111", Whole(8, 16))] #[case::eight_zero_digits_end("1111111100000000", Whole(0, 16))] #[case::eight_zero_digits_middle("11111111000000001", Whole(0, 17))] #[case::max_16_digits("9999999999999999", Whole(0, 16))] fn test_duration_repr_parser_parse_whole(#[case] input: &str, #[case] expected: Whole) {} #[apply(test_duration_repr_parser_parse_whole)] fn test_duration_repr_parser_parse_whole_single(input: &str, expected: Whole) { let mut parser = ReprParserSingle::new(input); assert_eq!(parser.parse_whole(), expected); } #[apply(test_duration_repr_parser_parse_whole)] fn test_duration_repr_parser_parse_whole_multiple(input: &str, expected: Whole) { let mut parser = ReprParserMultiple::new(input, |byte| byte == b' ', &[]); // cov:excl-line assert_eq!(parser.parse_whole(), expected); } // TODO: add test case for parse_multiple #[test] fn test_duration_repr_parser_parse_whole_when_more_than_max_exponent() { let config = Config::new(); let input = &"1".repeat(i16::MAX as usize + 100); let mut parser = ReprParserSingle::new(input); let duration_repr = parser.parse(&config, &TimeUnitsFixture, None).unwrap(); assert_eq!(duration_repr.whole, Some(Whole(0, i16::MAX as usize + 100))); assert_eq!(duration_repr.fract, None); } // TODO: use own test case for parse_multiple #[test] fn test_duration_repr_parser_parse_fract_when_more_than_max_exponent() { let input = format!(".{}", "1".repeat(i16::MAX as usize + 100)); let config = Config::new(); let mut parser = ReprParserSingle::new(&input); let duration_repr = parser.parse(&config, &TimeUnitsFixture, None).unwrap(); assert_eq!(duration_repr.whole, None); assert_eq!(duration_repr.fract, Some(Fract(1, i16::MAX as usize + 101))); let mut config = Config::new(); let delimiter = |byte| byte == b' '; config.parse_multiple_delimiter = Some(delimiter); let mut parser = ReprParserMultiple::new(&input, delimiter, &[]); let (duration_repr, maybe_parser) = parser.parse(&config, &TimeUnitsFixture, None).unwrap(); assert!(maybe_parser.is_none()); assert_eq!(duration_repr.whole, None); assert_eq!(duration_repr.fract, Some(Fract(1, i16::MAX as usize + 101))); } #[template] #[rstest] #[case::zero("0", Fract(0, 1))] #[case::one("1", Fract(0, 1))] #[case::nine("9", Fract(0, 1))] #[case::ten("10", Fract(0, 2))] #[case::leading_zero("01", Fract(0, 2))] #[case::leading_zeros("001", Fract(0, 3))] #[case::eight_leading_zeros("000000001", Fract(0, 9))] #[case::mixed_number("12345", Fract(0, 5))] #[case::max_8_digits("99999999", Fract(0, 8))] #[case::max_8_digits_minus_one("99999998", Fract(0, 8))] #[case::nine_digits("123456789", Fract(0, 9))] fn test_duration_repr_parser_parse_fract(#[case] input: &str, #[case] expected: Fract) {} #[apply(test_duration_repr_parser_parse_fract)] fn test_duration_repr_parser_parse_fract_single(input: &str, expected: Fract) { let mut parser = ReprParserSingle::new(input); assert_eq!(parser.parse_fract(), expected); } #[apply(test_duration_repr_parser_parse_fract)] fn test_duration_repr_parser_parse_fract_multiple(input: &str, expected: Fract) { let mut parser = ReprParserMultiple::new(input, |byte| byte == b' ', &[]); // cov:excl-line assert_eq!(parser.parse_fract(), expected); } } fundu-1.0.0/src/standard/builder.rs000064400000000000000000000442161046102023000153350ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use super::time_units::TimeUnits; use crate::config::{Config, DEFAULT_CONFIG}; use crate::parse::Parser; use crate::{Delimiter, DurationParser, TimeUnit}; #[derive(Debug, PartialEq, Eq)] enum TimeUnitsChoice<'a> { Default, All, None, Custom(&'a [TimeUnit]), } /// An ergonomic builder for a [`DurationParser`]. /// /// The [`DurationParserBuilder`] is more ergonomic in some use cases than using [`DurationParser`] /// directly, especially when using the `DurationParser` for parsing multiple inputs. This builder /// can also be used in `const` context, so it's possible to create a configured [`DurationParser`] /// at compilation time. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{Duration, DurationParser, DurationParserBuilder}; /// /// let parser = DurationParserBuilder::new() /// .all_time_units() /// .default_unit(MicroSecond) /// .allow_delimiter(|byte| byte == b' ') /// .build(); /// /// assert_eq!(parser.parse("1 ns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// /// // instead of /// /// let mut parser = DurationParser::with_all_time_units(); /// parser /// .default_unit(MicroSecond) /// .allow_delimiter(Some(|byte| byte == b' ')); /// /// assert_eq!(parser.parse("1 ns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// ``` /// /// The builder in `const` context. /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{DurationParser, DurationParserBuilder}; /// /// const PARSER : DurationParser = DurationParserBuilder::new() /// .time_units(&[Second, Minute, Hour, Day]) /// .allow_negative() /// .parse_multiple(|byte| byte.is_ascii_whitespace(), None) /// .build(); #[derive(Debug, PartialEq, Eq)] pub struct DurationParserBuilder<'a> { time_units_choice: TimeUnitsChoice<'a>, config: Config<'a>, } impl<'a> Default for DurationParserBuilder<'a> { /// Construct a new [`DurationParserBuilder`] without any time units. fn default() -> Self { Self::new() } } impl<'a> DurationParserBuilder<'a> { /// Construct a new reusable [`DurationParserBuilder`]. /// /// This method is the same like invoking [`DurationParserBuilder::default`]. Per default /// there are no time units configured in the builder. Use one of /// /// * [`DurationParserBuilder::default_time_units`] /// * [`DurationParserBuilder::all_time_units`] /// * [`DurationParserBuilder::time_units`] /// /// to add time units. /// /// # Examples /// /// ```rust /// use fundu::{DurationParser, DurationParserBuilder}; /// /// assert_eq!( /// DurationParserBuilder::new().build(), /// DurationParser::without_time_units() /// ); /// ``` pub const fn new() -> Self { Self { time_units_choice: TimeUnitsChoice::None, config: DEFAULT_CONFIG, } } /// Configure [`DurationParserBuilder`] to build the [`DurationParser`] with default time /// units. /// /// Setting the time units with this method overwrites any previous choices with /// /// * [`DurationParserBuilder::all_time_units`] /// * [`DurationParserBuilder::time_units`] /// /// The default time units with their identifiers are: /// /// | [`TimeUnit`] | default id /// | --------------- | ----------: /// | Nanosecond | ns /// | Microsecond | Ms /// | Millisecond | ms /// | Second | s /// | Minute | m /// | Hour | h /// | Day | d /// | Week | w /// /// # Examples /// /// ```rust /// use fundu::DurationParserBuilder; /// use fundu::TimeUnit::*; /// /// assert_eq!( /// DurationParserBuilder::new() /// .default_time_units() /// .build() /// .get_current_time_units(), /// vec![ /// NanoSecond, /// MicroSecond, /// MilliSecond, /// Second, /// Minute, /// Hour, /// Day, /// Week /// ] /// ); /// ``` pub const fn default_time_units(mut self) -> Self { self.time_units_choice = TimeUnitsChoice::Default; self } /// Configure [`DurationParserBuilder`] to build the [`DurationParser`] with all time units. /// /// Setting the time units with this method overwrites any previous choices with /// /// * [`DurationParserBuilder::default_time_units`] /// * [`DurationParserBuilder::time_units`] /// /// The time units with their identifiers are: /// /// | [`TimeUnit`] | default id /// | --------------- | ----------: /// | Nanosecond | ns /// | Microsecond | Ms /// | Millisecond | ms /// | Second | s /// | Minute | m /// | Hour | h /// | Day | d /// | Week | w /// | Month | M /// | Year | y /// /// # Examples /// /// ```rust /// use fundu::DurationParserBuilder; /// use fundu::TimeUnit::*; /// /// assert_eq!( /// DurationParserBuilder::new() /// .all_time_units() /// .build() /// .get_current_time_units(), /// vec![ /// NanoSecond, /// MicroSecond, /// MilliSecond, /// Second, /// Minute, /// Hour, /// Day, /// Week, /// Month, /// Year /// ] /// ); /// ``` pub const fn all_time_units(mut self) -> Self { self.time_units_choice = TimeUnitsChoice::All; self } /// Configure the [`DurationParserBuilder`] to build the [`DurationParser`] with a custom set /// of time units. /// /// Setting the time units with this method overwrites any previous choices with /// /// * [`DurationParserBuilder::default_time_units`] /// * [`DurationParserBuilder::all_time_units`] /// /// # Examples /// /// ```rust /// use fundu::DurationParserBuilder; /// use fundu::TimeUnit::*; /// /// assert_eq!( /// DurationParserBuilder::new() /// .time_units(&[NanoSecond, Second, Year]) /// .build() /// .get_current_time_units(), /// vec![NanoSecond, Second, Year] /// ); /// ``` pub const fn time_units(mut self, time_units: &'a [TimeUnit]) -> Self { self.time_units_choice = TimeUnitsChoice::Custom(time_units); self } /// Set the default time unit to something different than [`TimeUnit::Second`] /// /// See also [`DurationParser::default_unit`] /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{Duration, DurationParserBuilder}; /// /// assert_eq!( /// DurationParserBuilder::new() /// .all_time_units() /// .default_unit(NanoSecond) /// .build() /// .parse("42") /// .unwrap(), /// Duration::positive(0, 42) /// ); /// ``` pub const fn default_unit(mut self, unit: TimeUnit) -> Self { self.config.default_unit = unit; self } /// Allow one or more delimiters between the number and the [`TimeUnit`]. /// /// See also [`DurationParser::allow_delimiter`]. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParserBuilder}; /// /// let parser = DurationParserBuilder::new() /// .default_time_units() /// .allow_delimiter(|byte| byte.is_ascii_whitespace()) /// .build(); /// /// assert_eq!( /// parser.parse("123 \t\n\x0C\rns"), /// Ok(Duration::positive(0, 123)) /// ); /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123 ns"), Ok(Duration::positive(0, 123))); /// ``` pub const fn allow_delimiter(mut self, delimiter: Delimiter) -> Self { self.config.allow_delimiter = Some(delimiter); self } /// If set, parsing negative durations is possible /// /// See also [`DurationParser::allow_negative`] /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParserBuilder, ParseError}; /// /// let parser = DurationParserBuilder::new().allow_negative().build(); /// /// assert_eq!(parser.parse("-123"), Ok(Duration::negative(123, 0))); /// assert_eq!(parser.parse("-1.23e-7"), Ok(Duration::negative(0, 123))); /// ``` pub const fn allow_negative(mut self) -> Self { self.config.allow_negative = true; self } /// Disable parsing an exponent. /// /// See also [`DurationParser::disable_exponent`]. /// /// # Examples /// /// ```rust /// use fundu::{DurationParserBuilder, ParseError}; /// /// assert_eq!( /// DurationParserBuilder::new() /// .default_time_units() /// .disable_exponent() /// .build() /// .parse("123e+1"), /// Err(ParseError::Syntax(3, "No exponent allowed".to_string())) /// ); /// ``` pub const fn disable_exponent(mut self) -> Self { self.config.disable_exponent = true; self } /// Disable parsing a fraction in the source string. /// /// See also [`DurationParser::disable_fraction`]. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParserBuilder, ParseError}; /// /// let parser = DurationParserBuilder::new() /// .default_time_units() /// .disable_fraction() /// .build(); /// /// assert_eq!( /// parser.parse("123.456"), /// Err(ParseError::Syntax(3, "No fraction allowed".to_string())) /// ); /// /// assert_eq!( /// parser.parse("123e-2"), /// Ok(Duration::positive(1, 230_000_000)) /// ); /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// ``` pub const fn disable_fraction(mut self) -> Self { self.config.disable_fraction = true; self } /// Disable parsing infinity values /// /// See also [`DurationParser::disable_infinity`]. /// /// # Examples /// /// ```rust /// use fundu::{DurationParserBuilder, ParseError}; /// /// let parser = DurationParserBuilder::new().disable_infinity().build(); /// /// assert_eq!( /// parser.parse("inf"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'inf'"))) /// ); /// assert_eq!( /// parser.parse("infinity"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'infinity'"))) /// ); /// assert_eq!( /// parser.parse("+inf"), /// Err(ParseError::Syntax(1, format!("Invalid input: 'inf'"))) /// ); /// ``` pub const fn disable_infinity(mut self) -> Self { self.config.disable_infinity = true; self } /// This setting makes a number in the source string optional. /// /// See also [`DurationParser::number_is_optional`]. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParserBuilder}; /// /// let parser = DurationParserBuilder::new() /// .default_time_units() /// .number_is_optional() /// .build(); /// /// assert_eq!(parser.parse("ns"), Ok(Duration::positive(0, 1))); /// assert_eq!(parser.parse("+ns"), Ok(Duration::positive(0, 1))); /// ``` pub const fn number_is_optional(mut self) -> Self { self.config.number_is_optional = true; self } /// Parse possibly multiple durations and sum them up. /// /// See also [`DurationParser::parse_multiple`]. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParserBuilder}; /// /// let parser = DurationParserBuilder::new() /// .default_time_units() /// .parse_multiple(|byte| matches!(byte, b' ' | b'\t'), Some(&["and"])) /// .build(); /// /// assert_eq!( /// parser.parse("1.5h 2e+2ns"), /// Ok(Duration::positive(5400, 200)) /// ); /// assert_eq!( /// parser.parse("55s500ms"), /// Ok(Duration::positive(55, 500_000_000)) /// ); /// assert_eq!(parser.parse("1m and 1ns"), Ok(Duration::positive(60, 1))); /// assert_eq!( /// parser.parse("1. .1"), /// Ok(Duration::positive(1, 100_000_000)) /// ); /// assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0))); /// assert_eq!( /// parser.parse("300ms20s 5d"), /// Ok(Duration::positive(5 * 60 * 60 * 24 + 20, 300_000_000)) /// ); /// ``` pub const fn parse_multiple( mut self, delimiter: Delimiter, conjunctions: Option<&'a [&'a str]>, ) -> Self { self.config.parse_multiple_delimiter = Some(delimiter); self.config.parse_multiple_conjunctions = conjunctions; self } /// Finally, build the [`DurationParser`] from this builder. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParserBuilder}; /// /// let parser = DurationParserBuilder::new().default_time_units().build(); /// for input in &["1m", "60s"] { /// assert_eq!(parser.parse(input).unwrap(), Duration::positive(60, 0)) /// } /// ``` pub const fn build(self) -> DurationParser<'a> { let parser = Parser::with_config(self.config); match self.time_units_choice { TimeUnitsChoice::Default => DurationParser { time_units: TimeUnits::with_default_time_units(), inner: parser, }, TimeUnitsChoice::All => DurationParser { time_units: TimeUnits::with_all_time_units(), inner: parser, }, TimeUnitsChoice::None => DurationParser { time_units: TimeUnits::new(), inner: parser, }, TimeUnitsChoice::Custom(time_units) => DurationParser { time_units: TimeUnits::with_time_units(time_units), inner: parser, }, } } } #[cfg(test)] mod tests { use rstest::rstest; use super::*; use crate::config::Config; use crate::TimeUnit::*; #[test] fn test_duration_parser_builder_when_new() { let builder = DurationParserBuilder::new(); assert_eq!(builder.config, Config::new()); assert_eq!(builder.time_units_choice, TimeUnitsChoice::None); } #[test] fn test_duration_parser_builder_when_default_time_units() { let builder = DurationParserBuilder::new().default_time_units(); assert_eq!(builder.time_units_choice, TimeUnitsChoice::Default); } #[test] fn test_duration_parser_builder_when_all_time_units() { let builder = DurationParserBuilder::new().all_time_units(); assert_eq!(builder.time_units_choice, TimeUnitsChoice::All); } #[test] fn test_duration_parser_builder_when_custom_time_units() { let builder = DurationParserBuilder::new().time_units(&[MicroSecond, Hour, Week, Year]); assert_eq!( builder.time_units_choice, TimeUnitsChoice::Custom(&[MicroSecond, Hour, Week, Year]) ); } #[test] fn test_duration_parser_builder_when_default_unit() { let mut expected = Config::new(); expected.default_unit = MicroSecond; let builder = DurationParserBuilder::new().default_unit(MicroSecond); assert_eq!(builder.config, expected); } #[test] fn test_duration_parser_builder_when_allow_delimiter() { let builder = DurationParserBuilder::new().allow_delimiter(|b| b == b' '); assert!(builder.config.allow_delimiter.unwrap()(b' ')); } #[test] fn test_duration_parser_builder_when_disable_fraction() { let mut expected = Config::new(); expected.disable_fraction = true; let builder = DurationParserBuilder::new().disable_fraction(); assert_eq!(builder.config, expected); } #[test] fn test_duration_parser_builder_when_disable_exponent() { let mut expected = Config::new(); expected.disable_exponent = true; let builder = DurationParserBuilder::new().disable_exponent(); assert_eq!(builder.config, expected); } #[test] fn test_duration_parser_builder_when_disable_infinity() { let mut expected = Config::new(); expected.disable_infinity = true; let builder = DurationParserBuilder::new().disable_infinity(); assert_eq!(builder.config, expected); } #[test] fn test_duration_parser_builder_when_number_is_optional() { let mut expected = Config::new(); expected.number_is_optional = true; let builder = DurationParserBuilder::new().number_is_optional(); assert_eq!(builder.config, expected); } #[test] fn test_duration_parser_builder_when_parse_multiple() { let builder = DurationParserBuilder::new().parse_multiple(|byte: u8| byte == 0xff, None); assert!(builder.config.parse_multiple_delimiter.unwrap()(0xff)); } #[rstest] #[case::default_time_units(TimeUnitsChoice::Default, DurationParser::new())] #[case::all_time_units(TimeUnitsChoice::All, DurationParser::with_all_time_units())] #[case::no_time_units(TimeUnitsChoice::None, DurationParser::without_time_units())] #[case::custom_time_units( TimeUnitsChoice::Custom(&[NanoSecond, Minute]), DurationParser::with_time_units(&[NanoSecond, Minute]) )] fn test_duration_parser_builder_build( #[case] choice: TimeUnitsChoice, #[case] expected: DurationParser, ) { let mut builder = DurationParserBuilder::new(); builder.time_units_choice = choice; assert_eq!(builder.build(), expected); } } fundu-1.0.0/src/standard/mod.rs000064400000000000000000000003371046102023000144620ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT pub(crate) mod builder; pub(crate) mod parser; pub(crate) mod time_units; fundu-1.0.0/src/standard/parser.rs000064400000000000000000000476351046102023000152130ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use std::time::Duration as StdDuration; use super::time_units::TimeUnits; use crate::config::Delimiter; use crate::parse::Parser; use crate::time::Duration as FunduDuration; use crate::{DurationParserBuilder, ParseError, TimeUnit}; /// A parser with a customizable set of [`TimeUnit`]s with default identifiers. /// /// See also the [module level documentation](crate) for more details and more information about /// the format. /// /// # Examples /// /// A parser with the default set of time units /// /// ```rust /// use fundu::{Duration, DurationParser}; /// /// let parser = DurationParser::new(); /// assert_eq!(parser.parse("42Ms").unwrap(), Duration::positive(0, 42_000)); /// ``` /// /// The parser is reusable and the set of time units is fully customizable /// /// ```rust /// use fundu::{Duration, DurationParser, TimeUnit::*}; // /// let parser = DurationParser::with_time_units(&[NanoSecond, Minute, Hour]); /// for (input, expected) in &[ /// ("9e3ns", Duration::positive(0, 9000)), /// ("10m", Duration::positive(600, 0)), /// ("1.1h", Duration::positive(3960, 0)), /// ("7", Duration::positive(7, 0)), /// ] { /// assert_eq!(parser.parse(input).unwrap(), *expected); /// } /// ``` #[derive(Debug, PartialEq, Eq)] pub struct DurationParser<'a> { pub(super) time_units: TimeUnits, pub(super) inner: Parser<'a>, } impl<'a> DurationParser<'a> { /// Construct the parser with the default set of [`TimeUnit`]s. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{Duration, DurationParser}; /// /// assert_eq!( /// DurationParser::new().parse("1").unwrap(), /// Duration::positive(1, 0) /// ); /// assert_eq!( /// DurationParser::new().parse("1s").unwrap(), /// Duration::positive(1, 0) /// ); /// assert_eq!( /// DurationParser::new().parse("42.0e9ns").unwrap(), /// Duration::positive(42, 0) /// ); /// /// assert_eq!( /// DurationParser::new().get_current_time_units(), /// vec![ /// NanoSecond, /// MicroSecond, /// MilliSecond, /// Second, /// Minute, /// Hour, /// Day, /// Week /// ] /// ); /// ``` pub const fn new() -> Self { Self { time_units: TimeUnits::with_default_time_units(), inner: Parser::new(), } } /// Initialize the parser with a custom set of [`TimeUnit`]s. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{Duration, DurationParser}; /// /// assert_eq!( /// DurationParser::with_time_units(&[NanoSecond, Hour, Week]) /// .parse("1.5w") /// .unwrap(), /// Duration::positive(60 * 60 * 24 * 7 + 60 * 60 * 24 * 7 / 2, 0) /// ); /// ``` pub fn with_time_units(time_units: &[TimeUnit]) -> Self { Self { time_units: TimeUnits::with_time_units(time_units), inner: Parser::new(), } } /// Return a new parser without [`TimeUnit`]s. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser}; /// /// assert_eq!( /// DurationParser::without_time_units().parse("33.33").unwrap(), /// Duration::positive(33, 330_000_000) /// ); /// /// assert_eq!( /// DurationParser::without_time_units().get_current_time_units(), /// vec![] /// ); /// ``` pub const fn without_time_units() -> Self { Self { time_units: TimeUnits::new(), inner: Parser::new(), } } /// Construct a parser with all available [`TimeUnit`]s. /// /// # Examples /// /// ```rust /// use fundu::DurationParser; /// use fundu::TimeUnit::*; /// /// assert_eq!( /// DurationParser::with_all_time_units().get_current_time_units(), /// vec![ /// NanoSecond, /// MicroSecond, /// MilliSecond, /// Second, /// Minute, /// Hour, /// Day, /// Week, /// Month, /// Year /// ] /// ); /// ``` pub const fn with_all_time_units() -> Self { Self { time_units: TimeUnits::with_all_time_units(), inner: Parser::new(), } } /// Use the [`DurationParserBuilder`] to construct a [`DurationParser`]. /// /// The [`DurationParserBuilder`] is more ergonomic in some use cases than using /// [`DurationParser`] directly. Using this method is the same like invoking /// [`DurationParserBuilder::default`]. /// /// See [`DurationParserBuilder`] for more details. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{Duration, DurationParser}; /// /// let parser = DurationParser::builder() /// .all_time_units() /// .default_unit(MicroSecond) /// .allow_delimiter(|byte| byte.is_ascii_whitespace()) /// .build(); /// /// assert_eq!(parser.parse("1 \t\nns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// /// // instead of /// /// let mut parser = DurationParser::with_all_time_units(); /// parser /// .default_unit(MicroSecond) /// .allow_delimiter(Some(|byte| byte == b' ')); /// /// assert_eq!(parser.parse("1 ns").unwrap(), Duration::positive(0, 1)); /// assert_eq!(parser.parse("1").unwrap(), Duration::positive(0, 1_000)); /// ``` pub const fn builder() -> DurationParserBuilder<'a> { DurationParserBuilder::new() } /// Parse the `source` string into a [`crate::Duration`]. /// /// See the [module-level documentation](crate) for more information on the format. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser}; /// /// assert_eq!( /// DurationParser::new().parse("1.2e-1s").unwrap(), /// Duration::positive(0, 120_000_000), /// ); /// ``` #[inline] pub fn parse(&self, source: &str) -> Result { self.inner.parse(source, &self.time_units, None) } /// Set the default [`TimeUnit`] to `unit`. /// /// The default time unit is applied when no time unit was given in the input string. If the /// default time unit is not set with this method the parser defaults to [`TimeUnit::Second`]. /// /// # Examples /// /// ```rust /// use fundu::TimeUnit::*; /// use fundu::{Duration, DurationParser}; /// /// assert_eq!( /// DurationParser::with_all_time_units() /// .default_unit(NanoSecond) /// .parse("42") /// .unwrap(), /// Duration::positive(0, 42) /// ); /// ``` pub fn default_unit(&mut self, unit: TimeUnit) -> &mut Self { self.inner.config.default_unit = unit; self } /// If `Some`, allow one or more [`Delimiter`] between the number and the [`TimeUnit`]. /// /// A [`Delimiter`] is defined as closure taking a byte and returning true if the delimiter /// matched. Per default no delimiter is allowed between the number and the [`TimeUnit`]. As /// usual the default time unit is assumed if no time unit was present. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser, ParseError}; /// /// let mut parser = DurationParser::new(); /// assert_eq!( /// parser.parse("123 ns"), /// Err(ParseError::TimeUnit( /// 3, /// "Invalid time unit: ' ns'".to_string() /// )) /// ); /// /// parser.allow_delimiter(Some(|byte| byte == b' ')); /// assert_eq!(parser.parse("123 ns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123 ns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// /// parser.allow_delimiter(Some(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' '))); /// assert_eq!(parser.parse("123\r\nns"), Ok(Duration::positive(0, 123))); /// assert_eq!(parser.parse("123\t\n\r ns"), Ok(Duration::positive(0, 123))); /// ``` pub fn allow_delimiter(&mut self, delimiter: Option) -> &mut Self { self.inner.config.allow_delimiter = delimiter; self } /// If true, then parsing negative durations is possible /// /// Without setting this option the parser returns [`ParseError::NegativeNumber`] if it /// encounters a negative number. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser, ParseError}; /// /// let mut parser = DurationParser::new(); /// parser.allow_negative(true); /// /// assert_eq!(parser.parse("-123"), Ok(Duration::negative(123, 0))); /// assert_eq!(parser.parse("-1.23e-7"), Ok(Duration::negative(0, 123))); /// ``` pub fn allow_negative(&mut self, value: bool) -> &mut Self { self.inner.config.allow_negative = value; self } /// If true, disable parsing an exponent. /// /// If an exponent is encountered in the input string and this setting is active this results /// in an [`ParseError::Syntax`]. /// /// # Examples /// /// ```rust /// use fundu::{DurationParser, ParseError}; /// /// let mut parser = DurationParser::new(); /// parser.disable_exponent(true); /// assert_eq!( /// parser.parse("123e+1"), /// Err(ParseError::Syntax(3, "No exponent allowed".to_string())) /// ); /// ``` pub fn disable_exponent(&mut self, value: bool) -> &mut Self { self.inner.config.disable_exponent = value; self } /// If true, disable parsing a fraction in the source string. /// /// This setting will disable parsing a fraction and a point delimiter will cause an error /// [`ParseError::Syntax`]. It does not prevent [`crate::Duration`]s from being smaller than /// seconds. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser, ParseError}; /// /// let mut parser = DurationParser::new(); /// parser.disable_fraction(true); /// /// assert_eq!( /// parser.parse("123.456"), /// Err(ParseError::Syntax(3, "No fraction allowed".to_string())) /// ); /// /// assert_eq!( /// parser.parse("123e-2"), /// Ok(Duration::positive(1, 230_000_000)) /// ); /// /// assert_eq!(parser.parse("123ns"), Ok(Duration::positive(0, 123))); /// ``` pub fn disable_fraction(&mut self, value: bool) -> &mut Self { self.inner.config.disable_fraction = value; self } /// If true, disable parsing infinity /// /// This setting will disable parsing infinity values like (`inf` or `infinity`). /// /// # Examples /// /// ```rust /// use fundu::{DurationParser, ParseError}; /// /// let mut parser = DurationParser::new(); /// parser.disable_infinity(true); /// /// assert_eq!( /// parser.parse("inf"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'inf'"))) /// ); /// assert_eq!( /// parser.parse("infinity"), /// Err(ParseError::Syntax(0, format!("Invalid input: 'infinity'"))) /// ); /// assert_eq!( /// parser.parse("+inf"), /// Err(ParseError::Syntax(1, format!("Invalid input: 'inf'"))) /// ); /// ``` pub fn disable_infinity(&mut self, value: bool) -> &mut Self { self.inner.config.disable_infinity = value; self } /// If true, this setting makes a number in the source string optional. /// /// If no number is present, then `1` is assumed. If a number is present then it must still /// consist of either a whole part and/or fraction part, if not disabled with /// [`DurationParser::disable_fraction`]. The exponent is also part of the number and needs a /// mantissa. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser}; /// /// let mut parser = DurationParser::new(); /// parser.number_is_optional(true); /// /// assert_eq!(parser.parse("ns"), Ok(Duration::positive(0, 1))); /// assert_eq!(parser.parse("+ns"), Ok(Duration::positive(0, 1))); /// ``` pub fn number_is_optional(&mut self, value: bool) -> &mut Self { self.inner.config.number_is_optional = value; self } /// If set to some [`Delimiter`], parse possibly multiple durations and sum them up. /// /// If [`Delimiter`] is set to `None`, this functionality is disabled. The [`Delimiter`] may or /// may not occur to separate the durations. If the delimiter does not occur the next duration /// is recognized by a leading digit. It's also possible to use complete words, like `and` as /// conjunctions between durations. Note that the [`Delimiter`] is still needed to be set if /// conjunctions are used in order to separate the conjunctions from durations. /// /// Like a single duration, the summed durations saturate at [`crate::Duration::MAX`]. /// Parsing multiple durations is short-circuiting and parsing stops after the first /// [`ParseError`] was encountered. Note that parsing doesn't stop when reaching /// [`crate::Duration::MAX`], so any [`ParseError`]s later in the input string are still /// reported. /// /// # Usage together with number format customizations /// /// The number format and other aspects can be customized as usual via the methods within this /// struct and have the known effect. However, there is a notable constellation which has an /// effect on how durations are parsed: /// /// If [`DurationParser::allow_delimiter`] is set to some delimiter, the [`Delimiter`] of this /// method and the [`Delimiter`] of the `allow_delimiter` method can be equal either in parts or /// in a whole without having side-effects on each other. But, if simultaneously /// [`DurationParser::number_is_optional`] is set to true, then the resulting /// [`crate::Duration`] will differ: /// /// ```rust /// use fundu::{Duration, DurationParser}; /// /// let delimiter = |byte| matches!(byte, b' ' | b'\t'); /// let mut parser = DurationParser::new(); /// parser /// .parse_multiple(Some(delimiter), None) /// .number_is_optional(true); /// /// // Here, the parser parses `1`, `s`, `1` and then `ns` separately /// assert_eq!(parser.parse("1 s 1 ns"), Ok(Duration::positive(3, 1))); /// /// // Here, the parser parses `1 s` and then `1 ns`. /// parser.allow_delimiter(Some(delimiter)); /// assert_eq!(parser.parse("1 s 1 ns"), Ok(Duration::positive(1, 1))); /// ``` /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser}; /// /// let mut parser = DurationParser::new(); /// parser.parse_multiple(Some(|byte| matches!(byte, b' ' | b'\t')), Some(&["and"])); /// /// assert_eq!( /// parser.parse("1.5h 2e+2ns"), /// Ok(Duration::positive(5400, 200)) /// ); /// assert_eq!( /// parser.parse("55s500ms"), /// Ok(Duration::positive(55, 500_000_000)) /// ); /// assert_eq!(parser.parse("1m and 1ns"), Ok(Duration::positive(60, 1))); /// assert_eq!( /// parser.parse("1. .1"), /// Ok(Duration::positive(1, 100_000_000)) /// ); /// assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0))); /// assert_eq!( /// parser.parse("300ms20s 5d"), /// Ok(Duration::positive(5 * 60 * 60 * 24 + 20, 300_000_000)) /// ); /// ``` pub fn parse_multiple( &mut self, delimiter: Option, conjunctions: Option<&'static [&'static str]>, ) -> &mut Self { self.inner.config.parse_multiple_delimiter = delimiter; self.inner.config.parse_multiple_conjunctions = conjunctions; self } /// Return the currently defined set of [`TimeUnit`]. /// /// # Examples /// /// ```rust /// use fundu::{DurationParser, TimeUnit::*}; /// /// let parser = DurationParser::without_time_units(); /// assert_eq!( /// parser.get_current_time_units(), /// vec![] /// ); /// /// assert_eq!( /// DurationParser::with_time_units(&[NanoSecond]).get_current_time_units(), /// vec![NanoSecond] /// ); pub fn get_current_time_units(&self) -> Vec { self.time_units.get_time_units() } } impl<'a> Default for DurationParser<'a> { fn default() -> Self { Self::new() } } /// Parse a string into a [`std::time::Duration`] by accepting a `string` similar to floating /// point with the default set of time units. /// /// This method is providing a simple onetime parser. In contrast to [`DurationParser::parse`] it /// returns a [`std::time::Duration`]. It is generally a better idea to use the [`DurationParser`] /// builder if multiple inputs with the same set of time units need to be parsed or a customization /// of the time format is needed. /// /// See also the [module level documentation](crate) for more details and more information about /// the format. /// /// # Errors /// /// This function returns a [`ParseError`] when parsing of the input `string` failed. /// /// # Examples /// /// ```rust /// use std::time::Duration; /// /// use fundu::{parse_duration, ParseError}; /// /// let duration = parse_duration("+1.09e1").unwrap(); /// assert_eq!(duration, Duration::new(10, 900_000_000)); /// /// assert_eq!( /// parse_duration("Not a number"), /// Err(ParseError::Syntax( /// 0, /// "Invalid input: 'Not a number'".to_string() /// )) /// ); /// ``` pub fn parse_duration(string: &str) -> Result { DurationParser::new() .parse(string) // unwrap is safe here because negative durations aren't allowed in the default // configuration of the DurationParser .map(|d| d.try_into().unwrap()) } #[cfg(test)] mod tests { use super::*; use crate::config::Config; use crate::TimeUnit::*; #[test] fn test_duration_parser_init_when_default() { let config = Config::new(); let parser = DurationParser::default(); assert_eq!(parser.inner.config, config); assert_eq!( parser.get_current_time_units(), vec![ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week ] ) } #[test] fn test_duration_parser_setting_allow_delimiter() { let mut parser = DurationParser::new(); parser.allow_delimiter(Some(|byte| byte == b' ')); assert!(parser.inner.config.allow_delimiter.unwrap()(b' ')); } #[test] fn test_duration_parser_setting_disable_infinity() { let mut expected = Config::new(); expected.disable_infinity = true; let mut parser = DurationParser::new(); parser.disable_infinity(true); assert_eq!(parser.inner.config, expected); } #[test] fn test_duration_parser_setting_parse_multiple() { let mut parser = DurationParser::new(); parser.parse_multiple(Some(|byte: u8| byte == 0xff), None); assert!(parser.inner.config.parse_multiple_delimiter.unwrap()(0xff)); } #[test] fn test_duration_parser_when_builder() { assert_eq!(DurationParser::builder(), DurationParserBuilder::new()); } #[test] fn test_duration_parser_builder_when_default() { assert_eq!( DurationParserBuilder::default(), DurationParserBuilder::new() ); } } fundu-1.0.0/src/standard/time_units.rs000064400000000000000000000206521046102023000160650ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use crate::time::TimeUnitsLike; use crate::TimeUnit::*; use crate::{ Multiplier, TimeUnit, DEFAULT_ID_DAY, DEFAULT_ID_HOUR, DEFAULT_ID_MICRO_SECOND, DEFAULT_ID_MILLI_SECOND, DEFAULT_ID_MINUTE, DEFAULT_ID_MONTH, DEFAULT_ID_NANO_SECOND, DEFAULT_ID_SECOND, DEFAULT_ID_WEEK, DEFAULT_ID_YEAR, }; const DEFAULT_TIME_UNITS: [&str; 10] = [ DEFAULT_ID_NANO_SECOND, DEFAULT_ID_MICRO_SECOND, DEFAULT_ID_MILLI_SECOND, DEFAULT_ID_SECOND, DEFAULT_ID_MINUTE, DEFAULT_ID_HOUR, DEFAULT_ID_DAY, DEFAULT_ID_WEEK, DEFAULT_ID_MONTH, DEFAULT_ID_YEAR, ]; /// Interface for [`TimeUnit`]s providing common methods to manipulate the available time units. #[derive(Debug, PartialEq, Eq, Clone)] pub(super) struct TimeUnits { data: [Option; 10], } impl Default for TimeUnits { fn default() -> Self { Self::with_default_time_units() } } impl TimeUnitsLike for TimeUnits { /// Return `true` if this set of time units is empty. #[inline] fn is_empty(&self) -> bool { self.data.iter().all(|byte| byte.is_none()) } /// Return the [`TimeUnit`] associated with the provided `identifier`. /// /// Returns `None` if no [`TimeUnit`] with the provided `identifier` is present in the current /// set of time units. #[inline] fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> { match identifier.len() { 1 => self.data.iter().skip(3).filter_map(|t| *t).find_map(|t| { let unit = DEFAULT_TIME_UNITS[t as usize]; if unit == identifier { Some((t, Multiplier::default())) } else { None } }), 2 => self.data.iter().take(3).filter_map(|t| *t).find_map(|t| { let unit = DEFAULT_TIME_UNITS[t as usize]; if unit == identifier { Some((t, Multiplier::default())) } else { None } }), _ => None, } } } impl TimeUnits { /// Create an empty set of [`TimeUnit`]s. pub(super) const fn new() -> Self { Self { data: [None; 10] } } /// Create [`TimeUnits`] with a custom set of [`TimeUnit`]s. pub(super) const fn with_time_units(units: &[TimeUnit]) -> Self { let mut data: [Option; 10] = [None; 10]; let mut counter = 0; while counter < units.len() { let unit = units[counter]; data[unit as usize] = Some(unit); counter += 1; } Self { data } } /// Create [`TimeUnits`] with default [`TimeUnit`]s. pub(super) const fn with_default_time_units() -> Self { Self { data: [ Some(NanoSecond), Some(MicroSecond), Some(MilliSecond), Some(Second), Some(Minute), Some(Hour), Some(Day), Some(Week), None, None, ], } } /// Create [`TimeUnits`] with a all available [`TimeUnit`]s. pub(super) const fn with_all_time_units() -> Self { Self { data: [ Some(NanoSecond), Some(MicroSecond), Some(MilliSecond), Some(Second), Some(Minute), Some(Hour), Some(Day), Some(Week), Some(Month), Some(Year), ], } } /// Return all [`TimeUnit`]s from the set of active time units ordered. pub(super) fn get_time_units(&self) -> Vec { self.data.iter().filter_map(|&p| p).collect() } } #[cfg(test)] mod tests { use rstest::rstest; use super::*; #[test] fn test_time_units_new() { let time_units = TimeUnits::new(); assert!(time_units.data.iter().all(|t| t.is_none())); assert!(time_units.is_empty()); assert_eq!(time_units.get_time_units(), vec![]); } #[test] fn test_time_units_with_default_time_units() { let time_units = TimeUnits::with_default_time_units(); assert!(!time_units.is_empty()); assert_eq!(time_units, TimeUnits::default()); assert_eq!( time_units.get_time_units(), vec![ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week ] ); } #[test] fn test_time_units_with_all_time_units() { let time_units = TimeUnits::with_all_time_units(); assert!(!time_units.is_empty()); assert_eq!( time_units.get_time_units(), vec![ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year ] ); } #[rstest] fn test_time_units_with_time_units_when_single_unit( #[values( NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year )] time_unit: TimeUnit, ) { let mut expected_data: [Option; 10] = [None; 10]; expected_data[time_unit as usize] = Some(time_unit); let time_units = TimeUnits::with_time_units(&[time_unit]); assert!(!time_units.is_empty()); assert_eq!(time_units.data, expected_data); } #[test] fn test_time_units_with_time_units_when_all_time_units() { let expected_data: [Option; 10] = [ Some(NanoSecond), Some(MicroSecond), Some(MilliSecond), Some(Second), Some(Minute), Some(Hour), Some(Day), Some(Week), Some(Month), Some(Year), ]; let time_units = TimeUnits::with_time_units(&[ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year, ]); assert!(!time_units.is_empty()); assert_eq!(time_units.data, expected_data); } #[test] fn test_time_units_when_empty_then_return_true() { assert!(TimeUnits::new().is_empty()) } #[rstest] fn test_time_units_is_empty_when_not_empty_then_return_false( #[values( NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year )] time_unit: TimeUnit, ) { let time_units = TimeUnits::with_time_units(&[time_unit]); assert!(!time_units.is_empty()); } #[rstest] #[case::nano_second("ns", Some((NanoSecond, Multiplier(1,0))))] #[case::micro_second("Ms", Some((MicroSecond, Multiplier(1,0))))] #[case::milli_second("ms", Some((MilliSecond, Multiplier(1,0))))] #[case::second("s", Some((Second, Multiplier(1,0))))] #[case::minute("m", Some((Minute, Multiplier(1,0))))] #[case::hour("h", Some((Hour, Multiplier(1,0))))] #[case::day("d", Some((Day, Multiplier(1,0))))] #[case::week("w", Some((Week, Multiplier(1,0))))] #[case::month("M", Some((Month, Multiplier(1,0))))] #[case::year("y", Some((Year, Multiplier(1,0))))] fn test_time_units_get(#[case] id: &str, #[case] expected: Option<(TimeUnit, Multiplier)>) { assert_eq!(TimeUnits::with_all_time_units().get(id), expected); assert_eq!(TimeUnits::new().get(id), None); } #[test] fn test_time_units_get_time_units() { let time_units = TimeUnits::with_all_time_units(); assert_eq!( time_units.get_time_units(), vec![ NanoSecond, MicroSecond, MilliSecond, Second, Minute, Hour, Day, Week, Month, Year ] ) } } fundu-1.0.0/src/time.rs000064400000000000000000001637311046102023000130510ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use std::cmp::Ordering; use std::fmt::Display; use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use TimeUnit::*; use crate::error::TryFromDurationError; /// The default identifier of [`TimeUnit::NanoSecond`] pub const DEFAULT_ID_NANO_SECOND: &str = "ns"; /// The default identifier of [`TimeUnit::MicroSecond`] pub const DEFAULT_ID_MICRO_SECOND: &str = "Ms"; /// The default identifier of [`TimeUnit::MicroSecond`] pub const DEFAULT_ID_MILLI_SECOND: &str = "ms"; /// The default identifier of [`TimeUnit::Second`] pub const DEFAULT_ID_SECOND: &str = "s"; /// The default identifier of [`TimeUnit::Minute`] pub const DEFAULT_ID_MINUTE: &str = "m"; /// The default identifier of [`TimeUnit::Hour`] pub const DEFAULT_ID_HOUR: &str = "h"; /// The default identifier of [`TimeUnit::Day`] pub const DEFAULT_ID_DAY: &str = "d"; /// The default identifier of [`TimeUnit::Week`] pub const DEFAULT_ID_WEEK: &str = "w"; /// The default identifier of [`TimeUnit::Month`] pub const DEFAULT_ID_MONTH: &str = "M"; /// The default identifier of [`TimeUnit::Year`] pub const DEFAULT_ID_YEAR: &str = "y"; pub(crate) const DEFAULT_TIME_UNIT: TimeUnit = Second; /// The time units the parser can understand and needed to configure the [`DurationParser`]. /// /// # Examples /// /// ```rust /// use fundu::{Duration, DurationParser, TimeUnit}; /// /// assert_eq!( /// DurationParser::with_time_units(&[TimeUnit::NanoSecond]) /// .parse("42ns") /// .unwrap(), /// Duration::positive(0, 42) /// ); /// ``` /// /// [`DurationParser`]: crate::DurationParser #[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TimeUnit { /// Represents the lowest possible time unit. The default id is given by /// [`DEFAULT_ID_NANO_SECOND`] = `ns` NanoSecond, /// The default id is given by [`DEFAULT_ID_MICRO_SECOND`] = `Ms` MicroSecond, /// The default id is given by [`DEFAULT_ID_MILLI_SECOND`] = `ms` MilliSecond, /// The default if no time unit is given. The default id is given by [`DEFAULT_ID_SECOND`] = /// `s` Second, /// The default id is given by [`DEFAULT_ID_MINUTE`] = `m` Minute, /// The default id is given by [`DEFAULT_ID_HOUR`] = `h` Hour, /// The default id is given by [`DEFAULT_ID_DAY`] = `d` Day, /// The default id is given by [`DEFAULT_ID_WEEK`] = `w` Week, /// The default id is given by [`DEFAULT_ID_MONTH`] = `M` Month, /// Represents the hightest possible time unit. The default id is given by [`DEFAULT_ID_YEAR`] /// = `y` Year, } impl Default for TimeUnit { fn default() -> Self { DEFAULT_TIME_UNIT } } impl TimeUnit { /// Return the default identifier pub const fn default_identifier(&self) -> &'static str { match self { NanoSecond => DEFAULT_ID_NANO_SECOND, MicroSecond => DEFAULT_ID_MICRO_SECOND, MilliSecond => DEFAULT_ID_MILLI_SECOND, Second => DEFAULT_ID_SECOND, Minute => DEFAULT_ID_MINUTE, Hour => DEFAULT_ID_HOUR, Day => DEFAULT_ID_DAY, Week => DEFAULT_ID_WEEK, Month => DEFAULT_ID_MONTH, Year => DEFAULT_ID_YEAR, } } /// Return the base [`Multiplier`] of this [`TimeUnit`]. /// /// This multiplier is always seconds based so for example: /// /// ```ignore /// NanoSecond: Multiplier(1, -9) /// Second: Multiplier(1, 0) /// Year: Multiplier(31557600, 0) /// ``` pub const fn multiplier(&self) -> Multiplier { const MULTIPLIERS: [Multiplier; 10] = [ Multiplier(1, -9), Multiplier(1, -6), Multiplier(1, -3), Multiplier(1, 0), Multiplier(60, 0), Multiplier(3600, 0), Multiplier(86400, 0), Multiplier(604800, 0), Multiplier(2629800, 0), // Year / 12 Multiplier(31557600, 0), // 365.25 days ]; MULTIPLIERS[*self as usize] } } pub(crate) trait TimeUnitsLike { fn is_empty(&self) -> bool; fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)>; } /// The multiplier of a [`TimeUnit`]. /// /// The multiplier consists of two numbers `(m, e)` which are applied to another number `x` as /// follows: /// /// `x * m * 10 ^ e` /// /// Examples: /// /// ```ignore /// let nano_second = Multiplier(1, -9); /// let second = Multiplier(1, 0); /// let hour = Multiplier(3600, 0); /// ``` #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Multiplier(pub i64, pub i16); impl Default for Multiplier { fn default() -> Self { Self(1, 0) } } impl Multiplier { /// Return the coefficient component of the `Multiplier` /// /// # Examples /// /// ```rust /// use fundu::Multiplier; /// /// let multiplier = Multiplier(123, 45); /// assert_eq!(multiplier.coefficient(), 123); /// ``` #[inline] pub const fn coefficient(&self) -> i64 { self.0 } /// Return the exponent component of the `Multiplier` /// /// # Examples /// /// ```rust /// use fundu::Multiplier; /// /// let multiplier = Multiplier(123, 45); /// assert_eq!(multiplier.exponent(), 45); /// ``` #[inline] pub const fn exponent(&self) -> i16 { self.1 } /// Return true if the `Multiplier` is negative /// /// # Examples /// /// ```rust /// use fundu::Multiplier; /// /// let multiplier = Multiplier(-123, 45); /// assert!(multiplier.is_negative()); /// ``` #[inline] pub const fn is_negative(&self) -> bool { !self.is_positive() } /// Return true if the `Multiplier` is positive /// /// # Examples /// /// ```rust /// use fundu::Multiplier; /// /// let multiplier = Multiplier(123, 45); /// assert!(multiplier.is_positive()); /// ``` #[inline] pub const fn is_positive(&self) -> bool { self.0 == 0 || self.0.is_positive() } /// Checked `Multiplier` multiplication. Computes `self * other`, returning `None` if an /// overflow occurred. /// /// Let `a, b` be multipliers, with `m` being the coefficient and `e` the exponent. The /// multiplication is performed such that `(x.m, x.e) = (a.m * b.m, a.e + b.e)` /// /// # Examples /// /// ```rust /// use fundu::Multiplier; /// /// assert_eq!( /// Multiplier(1, 2).checked_mul(Multiplier(3, 4)), /// Some(Multiplier(3, 6)) /// ); /// assert_eq!( /// Multiplier(-1, 2).checked_mul(Multiplier(3, -4)), /// Some(Multiplier(-3, -2)) /// ); /// assert_eq!(Multiplier(2, 0).checked_mul(Multiplier(i64::MAX, 1)), None); /// assert_eq!(Multiplier(1, 2).checked_mul(Multiplier(1, i16::MAX)), None); /// ``` #[inline] pub const fn checked_mul(&self, rhs: Self) -> Option { if let Some(coefficient) = self.0.checked_mul(rhs.0) { if let Some(exponent) = self.1.checked_add(rhs.1) { return Some(Multiplier(coefficient, exponent)); } } None } /// Saturating negation. Computes `-self`, returning `i64::MAX` if `self.coefficient() == /// i64::MIN` instead of overflowing. /// /// # Examples /// /// ```rust /// use fundu::Multiplier; /// /// assert_eq!(Multiplier(1, 2).saturating_neg(), Multiplier(-1, 2)); /// assert_eq!(Multiplier(-1, 2).saturating_neg(), Multiplier(1, 2)); /// assert_eq!( /// Multiplier(i64::MIN, 2).saturating_neg(), /// Multiplier(i64::MAX, 2) /// ); /// ``` #[inline] pub const fn saturating_neg(&self) -> Self { Multiplier(self.0.saturating_neg(), self.1) } } impl Mul for Multiplier { type Output = Self; fn mul(self, rhs: Self) -> Self::Output { self.checked_mul(rhs) .expect("Multiplier: Overflow when multiplying") } } /// Conversion which saturates at the maximum or maximum instead of overflowing pub trait SaturatingInto: Sized { /// Performs the saturating conversion fn saturating_into(self) -> T; } /// The duration which is returned by the parser /// /// The `Duration` of this library implements conversions to a [`std::time::Duration`] and if the /// feature is activated into a [`time::Duration`] respectively [`chrono::Duration`]. This crates /// duration is a superset of the aforementioned durations, so converting to fundu's duration with /// `From` or `Into` is lossless. Converting from [`crate::Duration`] to the other durations can /// overflow the other duration's value range, but `TryFrom` is implement for all of these /// durations. Note that fundu's duration also implements [`SaturatingInto`] for the above durations /// which performs the conversion saturating at the maximum or minimum of these durations. /// /// # Examples /// /// Basic conversions from [`Duration`] to [`std::time::Duration`]. /// /// ```rust /// use std::time::Duration as StdDuration; /// /// use fundu::{Duration, SaturatingInto, TryFromDurationError}; /// /// let result: Result = Duration::positive(1, 2).try_into(); /// assert_eq!(result, Ok(StdDuration::new(1, 2))); /// /// let result: Result = Duration::negative(1, 2).try_into(); /// assert_eq!(result, Err(TryFromDurationError::NegativeDuration)); /// /// let duration: StdDuration = Duration::negative(1, 2).saturating_into(); /// assert_eq!(duration, StdDuration::ZERO); /// /// let duration: StdDuration = Duration::MAX.saturating_into(); /// assert_eq!(duration, StdDuration::MAX); /// ``` #[derive(Debug, Eq, Clone, Copy, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Duration { is_negative: bool, inner: std::time::Duration, } impl Duration { /// A duration of zero time /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::ZERO; /// assert!(duration.is_zero()); /// ``` pub const ZERO: Self = Self { is_negative: false, inner: std::time::Duration::ZERO, }; /// The minimum duration /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::MIN; /// assert_eq!(Duration::negative(u64::MAX, 999_999_999), duration); /// ``` pub const MIN: Self = Self { is_negative: true, inner: std::time::Duration::MAX, }; /// The maximum duration /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::MAX; /// assert_eq!(Duration::positive(u64::MAX, 999_999_999), duration); /// ``` pub const MAX: Self = Self { is_negative: false, inner: std::time::Duration::MAX, }; /// Creates a new `Duration` from a [`std::time::Duration`] which can be negative or positive /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::from_std(false, std::time::Duration::new(1, 0)); /// assert_eq!(Duration::positive(1, 0), duration); /// /// let duration = Duration::from_std(true, std::time::Duration::new(1, 0)); /// assert_eq!(Duration::negative(1, 0), duration); /// ``` pub const fn from_std(is_negative: bool, inner: std::time::Duration) -> Self { Self { is_negative, inner } } /// Creates a new positive `Duration` /// /// # Panics /// /// This constructor will panic if creating a [`std::time::Duration`] with the same parameters /// would panic /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::positive(1, 0); /// assert!(duration.is_positive()); /// /// let duration = Duration::positive(0, 0); /// assert!(duration.is_positive()); /// ``` pub const fn positive(secs: u64, nanos: u32) -> Self { Self { is_negative: false, inner: std::time::Duration::new(secs, nanos), } } /// Creates a new negative `Duration` /// /// # Panics /// /// This constructor will panic if creating a [`std::time::Duration`] with the same parameters /// would panic /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::negative(1, 0); /// assert!(duration.is_negative()); /// ``` pub const fn negative(secs: u64, nanos: u32) -> Self { Self { is_negative: true, inner: std::time::Duration::new(secs, nanos), } } /// Returns true if the `Duration` is negative /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::MIN; /// assert!(duration.is_negative()); /// /// let duration = Duration::negative(0, 1); /// assert!(duration.is_negative()); /// ``` #[inline] pub const fn is_negative(&self) -> bool { self.is_negative } /// Returns true if the `Duration` is positive /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::ZERO; /// assert!(duration.is_positive()); /// /// let duration = Duration::positive(0, 1); /// assert!(duration.is_positive()); /// ``` #[inline] pub const fn is_positive(&self) -> bool { !self.is_negative } /// Returns true if the `Duration` is zero /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::ZERO; /// assert!(duration.is_zero()); /// /// let duration = Duration::positive(0, 0); /// assert!(duration.is_zero()); /// /// let duration = Duration::negative(0, 0); /// assert!(duration.is_zero()); /// ``` #[inline] pub const fn is_zero(&self) -> bool { self.inner.is_zero() } /// Returns the absolute value of the duration /// /// This operation is lossless. /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// let duration = Duration::MIN; /// assert_eq!(duration.abs(), Duration::MAX); /// /// let duration = Duration::negative(1, 0); /// assert_eq!(duration.abs(), Duration::positive(1, 0)); /// /// let duration = Duration::positive(1, 0); /// assert_eq!(duration.abs(), Duration::positive(1, 0)); /// ``` #[inline] pub const fn abs(&self) -> Self { Self::from_std(false, self.inner) } /// Sums this duration with the `other` duration, returning None if an overflow occurred /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// assert_eq!( /// Duration::positive(1, 0).checked_add(Duration::positive(1, 0)), /// Some(Duration::positive(2, 0)) /// ); /// assert_eq!( /// Duration::positive(u64::MAX, 0).checked_add(Duration::positive(1, 0)), /// None /// ); /// assert_eq!( /// Duration::negative(u64::MAX, 0).checked_add(Duration::negative(1, 0)), /// None /// ); /// ``` pub fn checked_add(&self, other: Self) -> Option { match ( self.is_negative, other.is_negative, self.inner.cmp(&other.inner), ) { (true, true, _) => self .inner .checked_add(other.inner) .map(|d| Self::from_std(true, d)), (true, false, Ordering::Equal) | (false, true, Ordering::Equal) => Some(Self::ZERO), (true, false, Ordering::Greater) => { Some(Self::from_std(true, self.inner.sub(other.inner))) } (true, false, Ordering::Less) => { Some(Self::from_std(false, other.inner.sub(self.inner))) } (false, true, Ordering::Greater) => { Some(Self::from_std(false, self.inner.sub(other.inner))) } (false, true, Ordering::Less) => { Some(Self::from_std(true, other.inner.sub(self.inner))) } (false, false, _) => self .inner .checked_add(other.inner) .map(|d| Self::from_std(false, d)), } } /// Subtracts this duration with the `other` duration, returning None if an overflow occurred /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// assert_eq!( /// Duration::positive(1, 0).checked_sub(Duration::positive(1, 0)), /// Some(Duration::ZERO) /// ); /// assert_eq!( /// Duration::negative(u64::MAX, 0).checked_sub(Duration::positive(1, 0)), /// None /// ); /// ``` #[inline] pub fn checked_sub(&self, other: Self) -> Option { self.checked_add(other.neg()) } /// Saturating [`Duration`] addition. Computes `self + other`, returning [`Duration::MAX`] or /// [`Duration::MIN`] if an overflow occurred. /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// assert_eq!( /// Duration::positive(1, 0).saturating_add(Duration::positive(0, 1)), /// Duration::positive(1, 1) /// ); /// assert_eq!( /// Duration::positive(u64::MAX, 0).saturating_add(Duration::positive(1, 0)), /// Duration::MAX /// ); /// ``` pub fn saturating_add(&self, other: Self) -> Self { match self.checked_add(other) { Some(d) => d, // checked_add only returns None if both durations are either negative or positive so it // is enough to check one of the durations for negativity None if self.is_negative => Self::MIN, None => Self::MAX, } } /// Saturating [`Duration`] subtraction. Computes `self - other`, returning [`Duration::MAX`] or /// [`Duration::MIN`] if an overflow occurred. /// /// # Examples /// /// ```rust /// use fundu::Duration; /// /// assert_eq!( /// Duration::positive(1, 0).saturating_sub(Duration::positive(1, 0)), /// Duration::ZERO /// ); /// assert_eq!( /// Duration::negative(u64::MAX, 0).saturating_sub(Duration::positive(1, 0)), /// Duration::MIN /// ); /// ``` #[inline] pub fn saturating_sub(&self, other: Self) -> Self { self.saturating_add(other.neg()) } } impl Display for Duration { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { const YEAR: u64 = Year.multiplier().0.unsigned_abs(); const MONTH: u64 = Month.multiplier().0.unsigned_abs(); const WEEK: u64 = Week.multiplier().0.unsigned_abs(); const DAY: u64 = Day.multiplier().0.unsigned_abs(); const HOUR: u64 = Hour.multiplier().0.unsigned_abs(); const MINUTE: u64 = Minute.multiplier().0.unsigned_abs(); const MILLIS_PER_NANO: u32 = 1_000_000; const MICROS_PER_NANO: u32 = 1_000; if self.is_zero() { return f.write_str("0ns"); } let mut result = Vec::with_capacity(10); let mut secs = self.inner.as_secs(); if secs > 0 { if secs >= YEAR { result.push(format!("{}y", secs / YEAR)); secs %= YEAR; } if secs >= MONTH { result.push(format!("{}M", secs / MONTH)); secs %= MONTH; } if secs >= WEEK { result.push(format!("{}w", secs / WEEK)); secs %= WEEK; } if secs >= DAY { result.push(format!("{}d", secs / DAY)); secs %= DAY; } if secs >= HOUR { result.push(format!("{}h", secs / HOUR)); secs %= HOUR; } if secs >= MINUTE { result.push(format!("{}m", secs / MINUTE)); secs %= MINUTE; } if secs >= 1 { result.push(format!("{}s", secs)); } } let mut nanos = self.inner.subsec_nanos(); if nanos > 0 { if nanos >= MILLIS_PER_NANO { result.push(format!("{}ms", nanos / MILLIS_PER_NANO)); nanos %= MILLIS_PER_NANO; } if nanos >= MICROS_PER_NANO { result.push(format!("{}Ms", nanos / MICROS_PER_NANO)); nanos %= MICROS_PER_NANO; } if nanos >= 1 { result.push(format!("{}ns", nanos)); } } if self.is_negative() { f.write_str(&format!("-{}", &result.join(" -"))) } else { f.write_str(&result.join(" ")) } } } impl Add for Duration { type Output = Self; fn add(self, rhs: Self) -> Self::Output { self.checked_add(rhs) .expect("Overflow when adding duration") } } impl AddAssign for Duration { fn add_assign(&mut self, rhs: Self) { *self = *self + rhs } } impl Sub for Duration { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { self.checked_sub(rhs) .expect("Overflow when subtracting duration") } } impl SubAssign for Duration { fn sub_assign(&mut self, rhs: Self) { *self = *self - rhs } } impl Neg for Duration { type Output = Self; fn neg(self) -> Self::Output { Self { is_negative: self.is_negative ^ true, inner: self.inner, } } } impl PartialEq for Duration { fn eq(&self, other: &Self) -> bool { (self.is_zero() && other.is_zero()) || (self.is_negative == other.is_negative && self.inner == other.inner) } } impl Hash for Duration { fn hash(&self, state: &mut H) { if self.is_zero() { false.hash(state); self.inner.hash(state); } else { self.is_negative.hash(state); self.inner.hash(state); } } } impl PartialOrd for Duration { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Duration { fn cmp(&self, other: &Self) -> Ordering { match (self.is_negative, other.is_negative) { (true, true) => other.inner.cmp(&self.inner), (true, false) | (false, true) if self.is_zero() && other.is_zero() => Ordering::Equal, (true, false) => Ordering::Less, (false, true) => Ordering::Greater, (false, false) => self.inner.cmp(&other.inner), } } } /// Convert a [`std::time::Duration`] into a [`Duration`] impl From for Duration { fn from(duration: std::time::Duration) -> Self { Self { is_negative: false, inner: duration, } } } impl SaturatingInto for Duration { fn saturating_into(self) -> std::time::Duration { self.try_into().unwrap_or_else(|error| match error { TryFromDurationError::NegativeDuration => std::time::Duration::ZERO, _ => unreachable!(), // cov:excl-line }) } } /// Convert a [`Duration`] into a [`std::time::Duration`] impl TryFrom for std::time::Duration { type Error = TryFromDurationError; fn try_from(duration: Duration) -> Result { if !duration.is_zero() && duration.is_negative { Err(TryFromDurationError::NegativeDuration) } else { Ok(duration.inner) } } } #[cfg(feature = "time")] /// Convert a [`time::Duration`] into a [`Duration`] /// /// [`time::Duration`]: impl From for Duration { fn from(duration: time::Duration) -> Self { Self { is_negative: duration.is_negative(), inner: std::time::Duration::new( duration.whole_seconds().unsigned_abs(), duration.subsec_nanoseconds().unsigned_abs(), ), } } } #[cfg(feature = "time")] /// Convert a [`Duration`] into a [`time::Duration`] /// /// [`time::Duration`]: impl TryFrom for time::Duration { type Error = TryFromDurationError; fn try_from(duration: Duration) -> Result { (&duration).try_into() } } #[cfg(feature = "time")] /// Convert a [`Duration`] into a [`time::Duration`] /// /// [`time::Duration`]: impl TryFrom<&Duration> for time::Duration { type Error = TryFromDurationError; fn try_from(duration: &Duration) -> Result { match (duration.is_negative, duration.inner.as_secs()) { (true, secs) if secs > i64::MIN.unsigned_abs() => { Err(TryFromDurationError::NegativeOverflow) } (true, secs) => Ok(time::Duration::new( secs.wrapping_neg() as i64, -(duration.inner.subsec_nanos() as i32), )), (false, secs) if secs > i64::MAX as u64 => Err(TryFromDurationError::PositiveOverflow), (false, secs) => Ok(time::Duration::new( secs as i64, duration.inner.subsec_nanos() as i32, )), } } } #[cfg(feature = "time")] impl SaturatingInto for Duration { fn saturating_into(self) -> time::Duration { self.try_into().unwrap_or_else(|error| match error { TryFromDurationError::PositiveOverflow => time::Duration::MAX, TryFromDurationError::NegativeOverflow => time::Duration::MIN, _ => unreachable!(), // cov:excl-line }) } } #[cfg(feature = "chrono")] /// Convert a [`Duration`] into a [`chrono::Duration`] impl TryFrom for chrono::Duration { type Error = TryFromDurationError; fn try_from(duration: Duration) -> Result { (&duration).try_into() } } #[cfg(feature = "chrono")] /// Convert a [`Duration`] into a [`chrono::Duration`] impl TryFrom<&Duration> for chrono::Duration { type Error = TryFromDurationError; fn try_from(duration: &Duration) -> Result { const MAX: chrono::Duration = chrono::Duration::max_value(); const MIN: chrono::Duration = chrono::Duration::min_value(); match (duration.is_negative, duration.inner.as_secs()) { (true, secs) if secs > MIN.num_seconds().unsigned_abs() => { Err(TryFromDurationError::NegativeOverflow) } (true, secs) => { let nanos = chrono::Duration::nanoseconds((duration.inner.subsec_nanos() as i64).neg()); match chrono::Duration::seconds((secs as i64).neg()).checked_add(&nanos) { Some(d) => Ok(d), None => Err(TryFromDurationError::NegativeOverflow), } } (false, secs) if secs > MAX.num_seconds() as u64 => { Err(TryFromDurationError::PositiveOverflow) } (false, secs) => { let nanos = chrono::Duration::nanoseconds(duration.inner.subsec_nanos() as i64); match chrono::Duration::seconds(secs as i64).checked_add(&nanos) { Some(d) => Ok(d), None => Err(TryFromDurationError::PositiveOverflow), } } } } } #[cfg(feature = "chrono")] impl SaturatingInto for Duration { fn saturating_into(self) -> chrono::Duration { self.try_into().unwrap_or_else(|error| match error { TryFromDurationError::PositiveOverflow => chrono::Duration::max_value(), TryFromDurationError::NegativeOverflow => chrono::Duration::min_value(), _ => unreachable!(), // cov:excl-line }) } } #[cfg(feature = "chrono")] /// Convert a [`chrono::Duration`] into a [`Duration`] impl From for Duration { fn from(duration: chrono::Duration) -> Self { match duration.to_std() { Ok(inner) => Self { is_negative: false, inner, }, Err(_) => Self { is_negative: true, inner: duration.abs().to_std().unwrap(), }, } } } #[cfg(test)] mod tests { use std::collections::hash_map::DefaultHasher; use std::time::Duration as StdDuration; #[cfg(feature = "chrono")] use chrono::Duration as ChronoDuration; use rstest::rstest; use rstest_reuse::{apply, template}; #[cfg(feature = "serde")] use serde_test::{assert_tokens, Token}; use super::*; #[cfg(feature = "chrono")] const CHRONO_MIN_DURATION: Duration = Duration::negative(i64::MIN.unsigned_abs() / 1000, 808_000_000); #[cfg(feature = "chrono")] const CHRONO_MAX_DURATION: Duration = Duration::positive(i64::MAX as u64 / 1000, 807_000_000); const YEAR_AS_SECS: u64 = 60 * 60 * 24 * 365 + 60 * 60 * 24 / 4; // 365 days + day/4 const MONTH_AS_SECS: u64 = YEAR_AS_SECS / 12; #[cfg(feature = "serde")] #[test] fn test_time_unit_serde() { let time_unit = TimeUnit::Day; assert_tokens( &time_unit, &[ Token::Enum { name: "TimeUnit" }, Token::Str("Day"), Token::Unit, ], ) } #[cfg(feature = "serde")] #[test] fn test_serde_multiplier() { let multiplier = Multiplier(1, 2); assert_tokens( &multiplier, &[ Token::TupleStruct { name: "Multiplier", len: 2, }, Token::I64(1), Token::I16(2), Token::TupleStructEnd, ], ) } #[cfg(feature = "serde")] #[test] fn test_serde_duration() { let duration = Duration::positive(1, 2); assert_tokens( &duration, &[ Token::Struct { name: "Duration", len: 2, }, Token::Str("is_negative"), Token::Bool(false), Token::Str("inner"), Token::Struct { name: "Duration", len: 2, }, Token::Str("secs"), Token::U64(1), Token::Str("nanos"), Token::U32(2), Token::StructEnd, Token::StructEnd, ], ) } #[rstest] #[case::nano_second(NanoSecond, "ns")] #[case::micro_second(MicroSecond, "Ms")] #[case::milli_second(MilliSecond, "ms")] #[case::second(Second, "s")] #[case::minute(Minute, "m")] #[case::hour(Hour, "h")] #[case::day(Day, "d")] #[case::week(Week, "w")] #[case::month(Month, "M")] #[case::year(Year, "y")] fn test_time_unit_default_identifier(#[case] time_unit: TimeUnit, #[case] expected: &str) { assert_eq!(time_unit.default_identifier(), expected); } #[rstest] #[case::nano_second(NanoSecond, Multiplier(1, -9))] #[case::micro_second(MicroSecond, Multiplier(1, -6))] #[case::milli_second(MilliSecond, Multiplier(1, -3))] #[case::second(Second, Multiplier(1, 0))] #[case::minute(Minute, Multiplier(60, 0))] #[case::hour(Hour, Multiplier(60 * 60, 0))] #[case::day(Day, Multiplier(60 * 60 * 24, 0))] #[case::week(Week, Multiplier(60 * 60 * 24 * 7, 0))] #[case::month(Month, Multiplier((60 * 60 * 24 * 365 + 60 * 60 * 24 / 4) / 12, 0))] // (365 days + day/4) / 12 #[case::year(Year, Multiplier(60 * 60 * 24 * 365 + 60 * 60 * 24 / 4, 0))] // 365 days + day/4 fn test_time_unit_multiplier(#[case] time_unit: TimeUnit, #[case] expected: Multiplier) { assert_eq!(time_unit.multiplier(), expected); } #[test] fn test_multiplier_get_coefficient() { let multi = Multiplier(1234, 0); assert_eq!(multi.coefficient(), 1234); } #[test] fn test_multiplier_get_exponent() { let multi = Multiplier(0, 1234); assert_eq!(multi.exponent(), 1234); } #[rstest] #[case::zero(Multiplier(0, 0), false)] #[case::negative(Multiplier(-1, 0), true)] #[case::positive(Multiplier(1, 0), false)] #[case::negative_exponent(Multiplier(1, -1), false)] #[case::positive_exponent(Multiplier(-1, 1), true)] fn test_multiplier_is_negative_and_is_positive( #[case] multi: Multiplier, #[case] expected: bool, ) { assert_eq!(multi.is_negative(), expected); assert_eq!(multi.is_positive(), !expected); } #[rstest] #[case::nano_second(NanoSecond, Multiplier(i64::MAX, i16::MIN + 9))] #[case::micro_second(MicroSecond, Multiplier(i64::MAX, i16::MIN + 6))] #[case::milli_second(MilliSecond, Multiplier(i64::MAX, i16::MIN + 3))] #[case::second(Second, Multiplier(i64::MAX, i16::MIN))] #[case::minute(Minute, Multiplier(i64::MAX / 60, i16::MIN))] #[case::hour(Hour, Multiplier(i64::MAX / (60 * 60), i16::MIN))] #[case::day(Day, Multiplier(i64::MAX / (60 * 60 * 24), i16::MIN))] #[case::week(Week, Multiplier(i64::MAX / (60 * 60 * 24 * 7), i16::MIN))] #[case::month(Month, Multiplier(3_507_252_276_543, i16::MIN))] #[case::year(Year, Multiplier(292_271_023_045, i16::MIN))] fn test_multiplier_multiplication_barely_no_panic( #[case] time_unit: TimeUnit, #[case] multiplier: Multiplier, ) { let _ = time_unit.multiplier() * multiplier; } #[rstest] #[case::nano_second(NanoSecond, Multiplier(i64::MAX, i16::MIN + 8))] #[case::micro_second(MicroSecond, Multiplier(i64::MAX, i16::MIN + 4))] #[case::milli_second(MilliSecond, Multiplier(i64::MAX, i16::MIN + 2))] #[case::minute(Minute, Multiplier(i64::MAX / 60 + 1, i16::MIN))] #[case::hour(Hour, Multiplier(i64::MAX / (60 * 60) + 1, i16::MIN))] #[case::day(Day, Multiplier(i64::MAX / (60 * 60 * 24) + 1, i16::MIN))] #[case::week(Week, Multiplier(i64::MAX / (60 * 60 * 24 * 7) + 1, i16::MIN))] #[case::month(Month, Multiplier(3_507_252_276_543 + 1, i16::MIN))] #[case::year(Year, Multiplier(292_271_023_045 + 1, i16::MIN))] #[should_panic] fn test_multiplier_multiplication_then_panic( #[case] time_unit: TimeUnit, #[case] multiplier: Multiplier, ) { let _ = time_unit.multiplier() * multiplier; } #[rstest] #[case::positive_zero(Duration::positive(0, 0), true)] #[case::negative_zero(Duration::negative(0, 0), false)] // FIXME: This should return true #[case::positive_one(Duration::positive(1, 0), true)] #[case::negative_one(Duration::negative(1, 0), false)] fn test_fundu_duration_is_positive_and_is_negative( #[case] duration: Duration, #[case] expected: bool, ) { assert_eq!(duration.is_positive(), expected); assert_eq!(duration.is_negative(), !expected); } #[rstest] #[case::positive_zero(Duration::positive(0, 0), Duration::positive(0, 0))] #[case::negative_zero(Duration::negative(0, 0), Duration::positive(0, 0))] #[case::positive_one(Duration::positive(1, 0), Duration::positive(1, 0))] #[case::positive_one_nano(Duration::positive(0, 1), Duration::positive(0, 1))] #[case::negative_one(Duration::negative(1, 0), Duration::positive(1, 0))] #[case::negative_one_nano(Duration::negative(0, 1), Duration::positive(0, 1))] #[case::negative_one_one(Duration::negative(1, 1), Duration::positive(1, 1))] fn test_fundu_duration_abs(#[case] duration: Duration, #[case] expected: Duration) { assert_eq!(duration.abs(), expected); } #[rstest] #[case::zero(Duration::ZERO, "0ns")] #[case::nano_second(Duration::positive(0, 1), "1ns")] #[case::micro_second(Duration::positive(0, 1_000), "1Ms")] #[case::milli_second(Duration::positive(0, 1_000_000), "1ms")] #[case::second(Duration::positive(1, 0), "1s")] #[case::minute(Duration::positive(60, 0), "1m")] #[case::hour(Duration::positive(60 * 60, 0), "1h")] #[case::day(Duration::positive(60 * 60 * 24, 0), "1d")] #[case::week(Duration::positive(60 * 60 * 24 * 7, 0), "1w")] #[case::month(Duration::positive(MONTH_AS_SECS, 0), "1M")] #[case::year(Duration::positive(YEAR_AS_SECS, 0), "1y")] #[case::all_one( Duration::positive( YEAR_AS_SECS + MONTH_AS_SECS + 60 * 60 * 24 * 8 + 60 * 60 + 60 + 1, 1_001_001 ), "1y 1M 1w 1d 1h 1m 1s 1ms 1Ms 1ns" )] #[case::max(Duration::MAX, "584542046090y 7M 2w 1d 17h 30m 15s 999ms 999Ms 999ns")] fn test_fundu_display(#[case] duration: Duration, #[case] expected: &str) { assert_eq!(duration.to_string(), expected.to_string()); if duration.is_zero() { assert_eq!(duration.neg().to_string(), expected.to_string()); } else { assert_eq!( duration.neg().to_string(), format!("-{}", expected.replace(' ', " -")) ); } } #[template] #[rstest] #[case::both_standard_zero(Duration::ZERO, Duration::ZERO, Duration::ZERO)] #[case::both_positive_zero( Duration::positive(0, 0), Duration::positive(0, 0), Duration::positive(0, 0) )] #[case::both_negative_zero(Duration::negative(0, 0), Duration::negative(0, 0), Duration::ZERO)] #[case::one_add_zero(Duration::positive(1, 0), Duration::ZERO, Duration::positive(1, 0))] #[case::minus_one_add_zero(Duration::negative(1, 0), Duration::ZERO, Duration::negative(1, 0))] #[case::minus_one_add_plus_one( Duration::negative(1, 0), Duration::positive(1, 0), Duration::ZERO )] #[case::minus_one_add_plus_two_then_carry( Duration::negative(1, 0), Duration::positive(2, 0), Duration::positive(1, 0) )] #[case::minus_one_nano_add_one_then_carry( Duration::negative(0, 1), Duration::positive(1, 0), Duration::positive(0, 999_999_999) )] #[case::plus_one_nano_add_minus_one_then_carry( Duration::positive(0, 1), Duration::negative(1, 0), Duration::negative(0, 999_999_999) )] #[case::plus_one_add_minus_two_then_carry( Duration::positive(1, 0), Duration::negative(2, 0), Duration::negative(1, 0) )] #[case::one_sec_below_min_add_max( Duration::negative(u64::MAX - 1, 999_999_999), Duration::MAX, Duration::positive(1, 0), )] #[case::one_nano_below_min_add_max( Duration::negative(u64::MAX, 999_999_998), Duration::MAX, Duration::positive(0, 1) )] #[case::one_sec_below_max_add_min( Duration::positive(u64::MAX - 1, 999_999_999), Duration::MIN, Duration::negative(1, 0) )] #[case::one_nano_below_max_add_min( Duration::positive(u64::MAX, 999_999_998), Duration::MIN, Duration::negative(0, 1) )] #[case::min_and_max(Duration::MIN, Duration::MAX, Duration::ZERO)] fn test_fundu_duration_add_no_overflow_template( #[case] lhs: Duration, #[case] rhs: Duration, #[case] expected: Duration, ) { } #[apply(test_fundu_duration_add_no_overflow_template)] fn test_fundu_duration_add(lhs: Duration, rhs: Duration, expected: Duration) { assert_eq!(lhs + rhs, expected); assert_eq!(rhs + lhs, expected); } #[apply(test_fundu_duration_add_no_overflow_template)] fn test_fundu_duration_add_assign(lhs: Duration, rhs: Duration, expected: Duration) { let mut res = lhs; res += rhs; assert_eq!(res, expected); let mut res = rhs; res += lhs; assert_eq!(res, expected); } #[apply(test_fundu_duration_add_no_overflow_template)] fn test_fundu_duration_checked_add(lhs: Duration, rhs: Duration, expected: Duration) { assert_eq!(lhs.checked_add(rhs), Some(expected)); assert_eq!(rhs.checked_add(lhs), Some(expected)); } #[apply(test_fundu_duration_add_no_overflow_template)] fn test_fundu_duration_sub(lhs: Duration, rhs: Duration, expected: Duration) { assert_eq!(lhs - rhs.neg(), expected); assert_eq!(rhs - lhs.neg(), expected); } #[apply(test_fundu_duration_add_no_overflow_template)] fn test_fundu_duration_sub_assign(lhs: Duration, rhs: Duration, expected: Duration) { let mut res = lhs; res -= rhs.neg(); assert_eq!(res, expected); let mut res = rhs; res -= lhs.neg(); assert_eq!(res, expected); } #[apply(test_fundu_duration_add_no_overflow_template)] fn test_fundu_duration_checked_sub(lhs: Duration, rhs: Duration, expected: Duration) { assert_eq!(lhs.checked_sub(rhs.neg()), Some(expected)); assert_eq!(rhs.checked_sub(lhs.neg()), Some(expected)); } #[template] #[rstest] #[case::min(Duration::MIN, Duration::MIN)] #[case::min_minus_one(Duration::MIN, Duration::negative(1, 0))] #[case::min_minus_one_nano(Duration::MIN, Duration::negative(0, 1))] #[case::max(Duration::MAX, Duration::MAX)] #[case::max_plus_one(Duration::MAX, Duration::positive(1, 0))] #[case::max_plus_one_nano(Duration::MAX, Duration::positive(0, 1))] #[case::positive_middle_nano_overflow( Duration::positive(u64::MAX / 2 + 1, 999_999_999 / 2 + 1), Duration::positive(u64::MAX / 2, 999_999_999 / 2 + 1) )] #[case::positive_middle_secs_overflow( Duration::positive(u64::MAX / 2 + 1, 999_999_999 / 2 + 1), Duration::positive(u64::MAX / 2 + 1, 999_999_999 / 2) )] #[case::negative_middle_nano_overflow( Duration::negative(u64::MAX / 2 + 1, 999_999_999 / 2 + 1), Duration::negative(u64::MAX / 2, 999_999_999 / 2 + 1) )] #[case::negative_middle_secs_overflow( Duration::negative(u64::MAX / 2 + 1, 999_999_999 / 2 + 1), Duration::negative(u64::MAX / 2 + 1, 999_999_999 / 2) )] fn test_fundu_duration_add_overflow_template(#[case] lhs: Duration, #[case] rhs: Duration) {} #[apply(test_fundu_duration_add_overflow_template)] #[should_panic = "Overflow when adding duration"] fn test_fundu_duration_add_and_add_assign_then_overflow(mut lhs: Duration, rhs: Duration) { lhs += rhs; } #[apply(test_fundu_duration_add_overflow_template)] #[should_panic = "Overflow when subtracting duration"] #[allow(clippy::no_effect)] fn test_fundu_duration_sub_and_sub_assign_then_overflow(mut lhs: Duration, rhs: Duration) { lhs -= rhs.neg(); } #[apply(test_fundu_duration_add_overflow_template)] fn test_fundu_duration_checked_add_then_overflow(lhs: Duration, rhs: Duration) { assert_eq!(lhs.checked_add(rhs), None); assert_eq!(rhs.checked_add(lhs), None); } #[apply(test_fundu_duration_add_overflow_template)] fn test_fundu_duration_checked_sub_then_overflow(lhs: Duration, rhs: Duration) { assert_eq!(lhs.checked_sub(rhs.neg()), None); } #[apply(test_fundu_duration_add_overflow_template)] fn test_fundu_duration_saturating_add_then_saturate(lhs: Duration, rhs: Duration) { let expected = if lhs.checked_add(rhs).is_none() && lhs.is_positive() && rhs.is_positive() { Duration::MAX } else { Duration::MIN }; assert_eq!(lhs.saturating_add(rhs), expected); assert_eq!(rhs.saturating_add(lhs), expected); } #[apply(test_fundu_duration_add_overflow_template)] fn test_fundu_duration_saturating_sub_then_saturate(lhs: Duration, rhs: Duration) { let expected = if lhs.checked_sub(rhs.neg()).is_none() && lhs.is_negative() && rhs.neg().is_positive() { Duration::MIN } else { Duration::MAX }; assert_eq!(lhs.saturating_sub(rhs.neg()), expected); } #[rstest] #[case::positive_zero(Duration::ZERO, Duration::ZERO, Ordering::Equal)] #[case::negative_zero(Duration::negative(0, 0), Duration::negative(0, 0), Ordering::Equal)] #[case::negative_zero_and_positive_zero( Duration::negative(0, 0), Duration::ZERO, Ordering::Equal )] #[case::both_positive_one_sec( Duration::positive(1, 0), Duration::positive(1, 0), Ordering::Equal )] #[case::both_negative_one_sec( Duration::negative(1, 0), Duration::negative(1, 0), Ordering::Equal )] #[case::negative_and_positive_one_sec( Duration::negative(1, 0), Duration::positive(1, 0), Ordering::Less )] #[case::one_nano_second_difference_positive( Duration::ZERO, Duration::positive(0, 1), Ordering::Less )] #[case::one_nano_second_difference_negative( Duration::ZERO, Duration::negative(0, 1), Ordering::Greater )] #[case::max(Duration::MAX, Duration::MAX, Ordering::Equal)] #[case::one_nano_below_max( Duration::MAX, Duration::positive(u64::MAX, 999_999_998), Ordering::Greater )] #[case::min(Duration::MIN, Duration::MIN, Ordering::Equal)] #[case::one_nano_above_min( Duration::MIN, Duration::negative(u64::MAX, 999_999_998), Ordering::Less )] #[case::min_max(Duration::MIN, Duration::MAX, Ordering::Less)] #[case::mixed_positive( Duration::positive(123, 987), Duration::positive(987, 123), Ordering::Less )] #[case::mixed_negative( Duration::negative(123, 987), Duration::negative(987, 123), Ordering::Greater )] #[case::mixed_positive_and_negative( Duration::positive(123, 987), Duration::negative(987, 123), Ordering::Greater )] fn test_duration_partial_and_total_ordering( #[case] lhs: Duration, #[case] rhs: Duration, #[case] expected: Ordering, ) { assert_eq!(lhs.partial_cmp(&rhs), Some(expected)); assert_eq!(rhs.partial_cmp(&lhs), Some(expected.reverse())); } #[rstest] #[case::positive_zero(Duration::ZERO, Duration::ZERO)] #[case::negative_zero(Duration::negative(0, 0), Duration::negative(0, 0))] #[case::negative_zero_and_positive_zero(Duration::negative(0, 0), Duration::ZERO)] #[case::positive_one_sec(Duration::positive(1, 0), Duration::positive(1, 0))] #[case::negative_one_sec(Duration::negative(1, 0), Duration::negative(1, 0))] #[case::max(Duration::MAX, Duration::MAX)] #[case::min(Duration::MIN, Duration::MIN)] fn test_duration_hash_and_eq_property_when_equal( #[case] duration: Duration, #[case] other: Duration, ) { assert_eq!(duration, other); assert_eq!(other, duration); let mut hasher = DefaultHasher::new(); duration.hash(&mut hasher); let mut other_hasher = DefaultHasher::new(); other.hash(&mut other_hasher); assert_eq!(hasher.finish(), other_hasher.finish()); } #[rstest] #[case::zero_and_positive_one(Duration::ZERO, Duration::positive(1, 0))] #[case::zero_and_negative_one(Duration::ZERO, Duration::negative(1, 0))] #[case::positive_sec(Duration::positive(1, 0), Duration::negative(2, 0))] #[case::positive_and_negative_one_sec(Duration::positive(1, 0), Duration::negative(1, 0))] #[case::min_and_max(Duration::MIN, Duration::MAX)] fn test_duration_hash_and_eq_property_when_not_equal( #[case] duration: Duration, #[case] other: Duration, ) { assert_ne!(duration, other); assert_ne!(other, duration); let mut hasher = DefaultHasher::new(); duration.hash(&mut hasher); let mut other_hasher = DefaultHasher::new(); other.hash(&mut other_hasher); assert_ne!(hasher.finish(), other_hasher.finish()); } #[rstest] #[case::positive_zero(Duration::ZERO, StdDuration::ZERO)] #[case::negative_zero(Duration::negative(0, 0), StdDuration::ZERO)] #[case::positive_one(Duration::positive(1, 0), StdDuration::new(1, 0))] #[case::positive_one_nano(Duration::positive(0, 1), StdDuration::new(0, 1))] #[case::negative_one(Duration::negative(1, 0), StdDuration::ZERO)] #[case::negative_one_nano(Duration::negative(0, 1), StdDuration::ZERO)] #[case::max(Duration::MAX, StdDuration::MAX)] fn test_fundu_duration_saturating_into_std_duration( #[case] duration: Duration, #[case] expected: StdDuration, ) { assert_eq!( SaturatingInto::::saturating_into(duration), expected ); } #[cfg(feature = "time")] #[rstest] #[case::positive_zero(Duration::ZERO, time::Duration::ZERO)] #[case::negative_zero(Duration::negative(0, 0), time::Duration::ZERO)] #[case::positive_one(Duration::positive(1, 0), time::Duration::new(1, 0))] #[case::negative_one(Duration::negative(1, 0), time::Duration::new(-1, 0))] #[case::negative_barely_no_overflow( Duration::negative(i64::MIN.unsigned_abs(), 999_999_999), time::Duration::MIN )] #[case::negative_barely_overflow( Duration::negative(i64::MIN.unsigned_abs() + 1, 0), time::Duration::MIN )] #[case::negative_max_overflow(Duration::negative(u64::MAX, 999_999_999), time::Duration::MIN)] #[case::positive_barely_no_overflow( Duration::positive(i64::MAX as u64, 999_999_999), time::Duration::MAX )] #[case::positive_barely_overflow( Duration::positive(i64::MAX as u64 + 1, 999_999_999), time::Duration::MAX )] #[case::positive_max_overflow(Duration::positive(u64::MAX, 999_999_999), time::Duration::MAX)] fn test_fundu_duration_saturating_into_time_duration( #[case] duration: Duration, #[case] expected: time::Duration, ) { assert_eq!( SaturatingInto::::saturating_into(duration), expected ); } #[cfg(feature = "chrono")] #[rstest] #[case::positive_zero(Duration::ZERO, chrono::Duration::zero())] #[case::negative_zero(Duration::negative(0, 0), chrono::Duration::zero())] #[case::positive_one(Duration::positive(1, 0), chrono::Duration::seconds(1))] #[case::negative_one(Duration::negative(1, 0), chrono::Duration::seconds(-1))] #[case::negative_barely_no_overflow( Duration::negative(i64::MIN.unsigned_abs() / 1000, 808_000_000), chrono::Duration::min_value() )] #[case::negative_barely_overflow( Duration::negative(i64::MIN.unsigned_abs() / 1000 + 1, 0), chrono::Duration::min_value() )] #[case::negative_max_overflow( Duration::negative(u64::MAX, 999_999_999), chrono::Duration::min_value() )] #[case::positive_barely_no_overflow( Duration::positive(i64::MAX as u64 / 1000, 807_000_000), chrono::Duration::max_value() )] #[case::positive_barely_overflow( Duration::positive(i64::MAX as u64 / 1000 + 1, 807_000_000), chrono::Duration::max_value() )] #[case::positive_max_overflow( Duration::positive(u64::MAX, 999_999_999), chrono::Duration::max_value() )] fn test_fundu_duration_saturating_into_chrono_duration( #[case] duration: Duration, #[case] expected: chrono::Duration, ) { assert_eq!( SaturatingInto::::saturating_into(duration), expected ); } #[rstest] #[case::negative_one(Duration::negative(1, 0))] #[case::negative_one_nano(Duration::negative(0, 1))] #[case::one_one(Duration::negative(1, 1))] #[case::min(Duration::MIN)] fn test_std_duration_try_from_for_fundu_duration_then_error(#[case] duration: Duration) { assert_eq!( TryInto::::try_into(duration).unwrap_err(), TryFromDurationError::NegativeDuration ); } #[cfg(feature = "chrono")] #[rstest] #[case::zero(Duration::ZERO, ChronoDuration::zero())] #[case::negative_zero(Duration::negative(0, 0), ChronoDuration::zero())] #[case::positive_one_sec(Duration::positive(1, 0), ChronoDuration::seconds(1))] #[case::positive_one_sec_and_nano( Duration::positive(1, 1), ChronoDuration::nanoseconds(1_000_000_001) )] #[case::negative_one_sec(Duration::negative(1, 0), ChronoDuration::seconds(-1))] #[case::negative_one_sec_and_nano( Duration::negative(1, 1), ChronoDuration::nanoseconds(-1_000_000_001) )] #[case::max_nanos( Duration::positive(0, 999_999_999), ChronoDuration::nanoseconds(999_999_999) )] #[case::min_nanos( Duration::negative(0, 999_999_999), ChronoDuration::nanoseconds(-999_999_999) )] #[case::max_secs( Duration::positive(i64::MAX as u64 / 1000, 0), ChronoDuration::seconds(i64::MAX / 1000)) ] #[case::max_secs_and_nanos( Duration::positive(i64::MAX as u64 / 1000, 807_000_000), ChronoDuration::max_value()) ] #[case::secs_and_nanos_one_below_max( Duration::positive(i64::MAX as u64 / 1000, 807_000_000 - 1), ChronoDuration::max_value().checked_sub(&ChronoDuration::nanoseconds(1)).unwrap()) ] #[case::min_secs( Duration::negative(i64::MIN.unsigned_abs() / 1000, 0), ChronoDuration::seconds(i64::MIN / 1000)) ] #[case::min_secs_and_nanos( Duration::negative(i64::MIN.unsigned_abs() / 1000, 808_000_000), ChronoDuration::min_value()) ] #[case::secs_and_nanos_one_above_min( Duration::negative(i64::MIN.unsigned_abs() / 1000, 808_000_000 - 1), ChronoDuration::min_value().checked_add(&ChronoDuration::nanoseconds(1)).unwrap()) ] fn test_chrono_duration_try_from_fundu_duration( #[case] duration: Duration, #[case] expected: ChronoDuration, ) { let chrono_duration: ChronoDuration = duration.try_into().unwrap(); assert_eq!(chrono_duration, expected) } #[cfg(feature = "chrono")] #[rstest] #[case::positive_overflow_secs( Duration::positive(i64::MAX as u64 / 1000 + 1, 0), TryFromDurationError::PositiveOverflow) ] #[case::positive_overflow_secs_and_nanos( Duration::positive(i64::MAX as u64 / 1000, 807_000_000 + 1), TryFromDurationError::PositiveOverflow) ] #[case::positive_overflow_max_fundu_duration( Duration::MAX, TryFromDurationError::PositiveOverflow )] #[case::negative_overflow_secs( Duration::negative(i64::MIN.unsigned_abs() / 1000 + 1, 0), TryFromDurationError::NegativeOverflow) ] #[case::negative_overflow_secs_and_nanos( Duration::negative(i64::MIN.unsigned_abs() / 1000, 808_000_001), TryFromDurationError::NegativeOverflow) ] #[case::negative_overflow_min_fundu_duration( Duration::MIN, TryFromDurationError::NegativeOverflow )] fn test_chrono_duration_try_from_fundu_duration_then_error( #[case] duration: Duration, #[case] expected: TryFromDurationError, ) { let result: Result = duration.try_into(); assert_eq!(result.unwrap_err(), expected) } #[cfg(feature = "time")] #[test] fn test_time_duration_try_from_fundu_duration() { let duration = Duration::from_std(false, std::time::Duration::new(1, 0)); let time_duration: time::Duration = duration.try_into().unwrap(); assert_eq!(time_duration, time::Duration::new(1, 0)); } #[rstest] #[case::zero( std::time::Duration::ZERO, Duration::from_std(false, std::time::Duration::ZERO) )] #[case::one( std::time::Duration::new(1, 0), Duration::from_std(false, std::time::Duration::new(1, 0)) )] #[case::with_nano_seconds( std::time::Duration::new(1, 123_456_789), Duration::from_std(false, std::time::Duration::new(1, 123_456_789)) )] #[case::max( std::time::Duration::MAX, Duration::from_std(false, std::time::Duration::MAX) )] fn test_fundu_duration_from_std_time_duration( #[case] std_duration: std::time::Duration, #[case] expected: Duration, ) { assert_eq!(Duration::from(std_duration), expected); } #[cfg(feature = "time")] #[rstest] #[case::zero(time::Duration::ZERO, Duration::ZERO)] #[case::positive_one(time::Duration::new(1, 0), Duration::positive(1, 0))] #[case::positive_one_nano(time::Duration::new(0, 1), Duration::positive(0, 1))] #[case::negative_one(time::Duration::new(-1, 0), Duration::negative(1, 0))] #[case::negative_one_nano(time::Duration::new(0, -1), Duration::negative(0, 1))] #[case::positive_one_negative_one_nano( time::Duration::new(1, -1), Duration::positive(0, 999_999_999) )] #[case::negative_one_positive_one_nano( time::Duration::new(-1, 1), Duration::negative(0, 999_999_999) )] #[case::min(time::Duration::MIN, Duration::negative(i64::MIN.unsigned_abs(), 999_999_999))] #[case::max(time::Duration::MAX, Duration::positive(i64::MAX as u64, 999_999_999))] fn test_fundu_duration_from_time_duration( #[case] time_duration: time::Duration, #[case] expected: Duration, ) { assert_eq!(Duration::from(time_duration), expected); } #[cfg(feature = "chrono")] #[rstest] #[case::zero(chrono::Duration::zero(), Duration::ZERO)] #[case::positive_one(chrono::Duration::seconds(1), Duration::positive(1, 0))] #[case::positive_one_nano(chrono::Duration::nanoseconds(1), Duration::positive(0, 1))] #[case::negative_one(chrono::Duration::seconds(-1), Duration::negative(1, 0))] #[case::negative_one_nano(chrono::Duration::nanoseconds(-1), Duration::negative(0, 1))] #[case::min(chrono::Duration::min_value(), CHRONO_MIN_DURATION)] #[case::max(chrono::Duration::max_value(), CHRONO_MAX_DURATION)] fn test_fundu_duration_from_chrono_duration( #[case] chrono_duration: chrono::Duration, #[case] expected: Duration, ) { assert_eq!(Duration::from(chrono_duration), expected) } } fundu-1.0.0/src/util.rs000064400000000000000000000056161046102023000130650ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT pub(crate) const POW10: [u64; 20] = [ 1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000, 100_000_000_000, 1_000_000_000_000, 10_000_000_000_000, 100_000_000_000_000, 1_000_000_000_000_000, 10_000_000_000_000_000, 100_000_000_000_000_000, 1_000_000_000_000_000_000, 10_000_000_000_000_000_000, ]; pub(crate) trait FloorLog10 { fn floor_log10(&self) -> u32; } pub(crate) trait FloorLog2 { fn floor_log2(&self) -> u32; } impl FloorLog10 for u64 { // This solution is based on a post https://stackoverflow.com/a/25934909 by Stephen Canon #[inline] fn floor_log10(&self) -> u32 { const GUESS_TABLE: [u32; 64] = [ 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 18, ]; let estimate = GUESS_TABLE[self.floor_log2() as usize]; let over_estimate = estimate + 1; if *self >= POW10[over_estimate as usize] { over_estimate } else { estimate } } } impl FloorLog2 for u64 { #[inline] fn floor_log2(&self) -> u32 { 63u32 .checked_sub(self.leading_zeros()) .expect("Logarithm of zero is undefined") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_floor_log2_for_u64_at_critical_margins() { for i in 0..63 { let pow = 1u64 << i; assert_eq!(pow.floor_log2(), i); if i > 0 { assert_eq!(pow.saturating_sub(1).floor_log2(), i.saturating_sub(1)); assert_eq!(pow.saturating_add(1).floor_log2(), i); } } } #[test] fn test_floor_log2_for_u64_when_u64_max() { assert_eq!(u64::MAX.floor_log2(), 63) } #[test] #[should_panic = "Logarithm of zero is undefined"] fn test_floor_log2_for_u64_when_zero() { 0u64.floor_log2(); } #[test] fn test_floor_log10_for_u64_at_critical_margins() { for i in 0..19u32 { let pow = POW10[i as usize]; assert_eq!(pow.floor_log10(), i); if i != 0 { assert_eq!(pow.saturating_sub(1).floor_log10(), i.saturating_sub(1)); } assert_eq!(pow.saturating_add(1).floor_log10(), i); } } #[test] fn test_floor_log10_for_u64_when_u64_max() { assert_eq!(u64::MAX.floor_log10(), 19); } #[test] #[should_panic = "Logarithm of zero is undefined"] fn test_floor_log10_for_u64_when_zero() { 0u64.floor_log10(); } } fundu-1.0.0/tests/parser_test.rs000064400000000000000000001307541046102023000150200ustar 00000000000000// Copyright (c) 2023 Joining7943 // // This software is released under the MIT License. // https://opensource.org/licenses/MIT use std::time::Duration as StdDuration; use fundu::TimeUnit::*; use fundu::{ parse_duration, CustomDurationParser, CustomDurationParserBuilder, CustomTimeUnit, Duration, DurationParser, Multiplier, ParseError, TimeKeyword, TimeUnit, SYSTEMD_TIME_UNITS, }; use rstest::rstest; const YEAR: u64 = 60 * 60 * 24 * 365 + 60 * 60 * 24 / 4; // 365 days + day/4 const MONTH: u64 = YEAR / 12; #[rstest] #[case::empty_string("")] #[case::leading_whitespace(" 1")] #[case::trailing_whitespace("1 ")] #[case::trailing_invalid_character("1.1e1msNOPE")] #[case::only_whitespace(" \t\n")] #[case::only_point(".")] #[case::only_sign("+")] #[case::only_exponent("e-10")] #[case::sign_with_exponent("-e1")] #[case::sign_with_point_and_exponent("-.e1")] #[case::negative_seconds("-1")] #[case::negative_seconds_with_fraction("-1.0")] #[case::negative_nano_seconds("-0.000000001")] #[case::exponent_with_only_plus("2E+")] #[case::negative_number_high_exponent("-3E75")] #[case::negative_number_barely_not_zero("-1.e-18")] #[case::negative_number_1("-.00000000000019999999e-4")] #[case::negative_number_2("-.00000003e-2")] #[case::negative_large_input_with_high_exponent("-088888888888888888888888818288833333333333333333333333333333333333333333333388888888888888888880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001616564957e-1027")] fn test_parse_duration_with_illegal_argument_then_error(#[case] source: &str) { let parser = DurationParser::builder().default_time_units().build(); assert!(parser.parse(source).is_err()); let parser = DurationParser::builder() .default_time_units() .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) .build(); assert!(parser.parse(source).is_err()); } #[rstest] #[case::simple_number("1", Duration::positive(1, 0))] #[case::trailing_zeros("10.010000000", Duration::positive(10, 10_000_000))] #[case::simple_zero("0", Duration::ZERO)] #[case::many_zeros(&"0".repeat(2000), Duration::ZERO)] #[case::many_leading_zeros(&format!("{}1", "0".repeat(2000)), Duration::positive(1, 0))] #[case::zero_point_zero("0.0", Duration::ZERO)] #[case::point_zero(".0", Duration::ZERO)] #[case::zero_point("0.", Duration::ZERO)] #[case::one_with_fraction_number("1.1", Duration::positive(1, 100_000_000))] #[case::leading_zero_max_nanos("0.999999999", Duration::positive(0, 999_999_999))] #[case::leading_number_max_nanos("1.999999999", Duration::positive(1, 999_999_999))] #[case::simple_number("1234.123456789", Duration::positive(1234, 123_456_789))] #[case::max_seconds(&u64::MAX.to_string(), Duration::positive(u64::MAX, 0))] #[case::leading_zeros("000000100", Duration::positive(100, 0))] #[case::leading_zeros_with_fraction("00000010.0", Duration::positive(10, 0))] #[case::negative_number_negative_exponent_below_attos("-9.99999999999E-19", Duration::ZERO)] #[case::negative_large_input_with_high_exponent_is_zero("-.000000000000000000000000000000000000011888888888888888888888888888880000000000003333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333888888888888888888888888888888888800003333333333333333333333333333333333333333333333333333333333333333333333333338888888888888888833333333333333333333333333333333333333333333333338888888888888833333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333338888888888888888888888888888888888888888888888885888888000000000000333333333333333333333333333338883333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333888888888888888888888888888888888888888888888888888888800000000000033333333333333333000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011414703819657838596", Duration::ZERO)] #[case::f64_max(&format!("{}", f64::MAX), Duration::MAX)] fn test_parse_duration_when_simple_arguments_are_valid( #[case] source: &str, #[case] expected: Duration, ) { let parser = DurationParser::builder().default_time_units().build(); assert_eq!(parser.parse(source), Ok(expected)); let parser = DurationParser::builder() .default_time_units() .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) // cov:excl-line .build(); assert_eq!(parser.parse(source), Ok(expected)); } #[rstest] #[case::minimum_exponent_with_minimum_time_unit(&format!("1{}.0e{}ns", "0".repeat((i16::MAX as usize) + 1), i16::MIN), Duration::positive(0, 1))] #[case::seconds_overflow_when_negative_exponent(&format!("{}e-1", u64::MAX as u128 * 100), Duration::MAX)] #[case::seconds_overflow_when_positive_exponent(&format!("{}.11e1", u64::MAX), Duration::MAX)] #[case::minus_sign_whole_to_fract("1.00000001e-1", Duration::positive(0, 100_000_001))] #[case::zero("1.1e0", Duration::positive(1, 100_000_000))] #[case::point_and_then_exponent("1.e0", Duration::positive(1, 0))] #[case::negative_zero("1.1e-0", Duration::positive(1, 100_000_000))] #[case::simple("1.09e1", Duration::positive(10, 900_000_000))] #[case::simple_big_e("1.09E1", Duration::positive(10, 900_000_000))] #[case::two_e4("2e4", Duration::positive(20000, 0))] #[case::lower_than_nanos_min("0.0000000001e1", Duration::positive(0, 1))] #[case::higher_than_seconds_max(&format!("{}9.999999999e-1", u64::MAX), Duration::MAX)] #[case::plus_sign("0.1000000001e+1", Duration::positive(1, 1))] #[case::minus_sign_zero_to_fract("10.00000001e-1", Duration::positive(1, 1))] #[case::exponent_then_nineteen_zeros_in_fraction("1.0e-20", Duration::ZERO)] #[case::no_overflow_error_low("1.0e-32768", Duration::ZERO)] #[case::no_overflow_error_high("1.0e+32767", Duration::MAX)] #[case::maximum_exponent(&format!("0.{}9e+{}", "0".repeat(i16::MAX as usize), i16::MAX), Duration::positive(0, 900_000_000))] #[case::maximum_exponent_barely_not_zero(&format!(".{}1e{}", "0".repeat((i16::MAX as usize) + 8), i16::MAX), Duration::positive(0, 1))] #[case::maximum_exponent_barely_not_zero_with_time_unit(&format!(".{}1e{}y", "0".repeat((i16::MAX as usize) + 15), i16::MAX), Duration::positive(0, 3))] #[case::maximum_exponent_with_maximum_time_unit(&format!("0.{}9e+{}y", "0".repeat(i16::MAX as usize), i16::MAX), Duration::positive(28401840, 0))] #[case::minimum_exponent(&format!("1{}.0e{}", "0".repeat(i16::MIN.unsigned_abs() as usize), i16::MIN), Duration::positive(1, 0))] #[case::minimum_exponent_barely_not_max_duration(&format!("1{}.0e{}", "0".repeat((i16::MIN.unsigned_abs() as usize) + 19), i16::MIN), Duration::positive(10_000_000_000_000_000_000, 0))] #[case::minimum_exponent_barely_not_max_duration_with_time_unit(&format!("1{}.0e{}ns", "0".repeat((i16::MIN.unsigned_abs() as usize) + 28), i16::MIN), Duration::positive(10_000_000_000_000_000_000, 0))] fn test_parse_duration_when_arguments_contain_exponent( #[case] source: &str, #[case] expected: Duration, ) { let duration = DurationParser::with_all_time_units().parse(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::exponent_without_mantissa("e1")] #[case::no_number("1e")] #[case::no_number_but_time_unit("6eMs")] #[case::invalid_number("1e+F")] #[case::exponent_overflow_error_high("1e32768")] #[case::exponent_overflow_error_low("1e-32769")] #[case::exponent_parse_i16_overflow_error(&format!("1e{}", i16::MIN as i32 - 1))] fn test_parse_duration_when_arguments_with_illegal_exponent_then_error(#[case] source: &str) { let parser = DurationParser::builder().all_time_units().build(); let result = parser.parse(source); assert!(result.is_err()); } #[rstest] #[case::no_rounding("1.99999999999999999", StdDuration::new(1, 999_999_999))] #[case::high_value_no_swallow_fract(&format!("{}.1", u64::MAX), StdDuration::new(u64::MAX, 100_000_000) )] fn test_parse_duration_when_precision_of_float_would_be_insufficient_then_still_parse_exact( #[case] source: &str, #[case] expected: StdDuration, ) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::lower_than_min_nanos("1.0000000001", StdDuration::new(1, 0))] #[case::max_digits_of_nanos("1.99999999999", StdDuration::new(1, 999_999_999))] #[case::higher_than_max_seconds(&format!("{}", u64::MAX as u128 + 1), StdDuration::MAX)] #[case::higher_than_max_seconds_with_fraction(&format!("{}.0", u64::MAX as u128 + 1), StdDuration::MAX)] fn test_parse_duration_when_arguments_are_capped_then_max_duration_or_min_nanos( #[case] source: &str, #[case] expected: StdDuration, ) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::plus_zero("+0", StdDuration::ZERO)] #[case::plus_zero_with_fraction("+0.0", StdDuration::ZERO)] #[case::minus_zero("-0", StdDuration::ZERO)] #[case::minus_zero_with_fraction("-0.0", StdDuration::ZERO)] #[case::plus_one_with_fraction("+1.0", StdDuration::new(1, 0))] fn test_parse_duration_when_arguments_have_a_sign( #[case] source: &str, #[case] expected: StdDuration, ) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::infinity_short("inf")] #[case::infinity_short_with_sign("+inf")] #[case::infinity_short_case_insensitive("iNf")] #[case::infinity_long("infinity")] #[case::infinity_long_with_sign("+infinity")] #[case::infinity_long_case_insensitive("InfiNitY")] fn test_parse_duration_when_arguments_are_infinity_values(#[case] source: &str) { let duration = parse_duration(source).unwrap(); assert_eq!(duration, StdDuration::MAX); } #[rstest] #[case::negative_infinity_short("-inf", ParseError::NegativeNumber)] #[case::negative_infinity_long("-infinity", ParseError::NegativeNumber)] #[case::infinity_long_trailing_invalid("infinityINVALID", ParseError::Syntax(8, "Expected end of input but found: 'I'".to_string()))] #[case::incomplete_infinity("infin", ParseError::Syntax(5, "Error parsing infinity: Premature end of input".to_string()))] #[case::infinity_with_number("inf1.0", ParseError::Syntax(3, "Error parsing infinity: Invalid character '1'".to_string()))] fn test_parse_duration_when_arguments_are_illegal_infinity_values_then_error( #[case] source: &str, #[case] expected: ParseError, ) { assert_eq!(parse_duration(source), Err(expected)); } #[rstest] #[case::max_attos_no_u64_overflow(&format!("0.{}y", "9".repeat(100)), Duration::positive(31557599, 999_999_999))] #[case::seconds("1s", Duration::positive(1, 0))] #[case::milli_seconds("1ms", Duration::positive(0, 1_000_000))] #[case::milli_seconds("1000ms", Duration::positive(1, 0))] #[case::micro_seconds("1Ms", Duration::positive(0, 1_000))] #[case::micro_seconds("1e-3Ms", Duration::positive(0, 1))] #[case::nano_seconds_time_unit("1ns", Duration::positive(0, 1))] #[case::minutes_fraction("0.1m", Duration::positive(6, 0))] #[case::minutes_negative_exponent("100.0e-3m", Duration::positive(6, 0))] #[case::minutes_underflow("0.0000000001m", Duration::positive(0, 6))] #[case::hours_underflow("0.000000000001h", Duration::positive(0, 3))] #[case::years_underflow("0.0000000000000001y", Duration::positive(0, 3))] #[case::minutes_overflow(&format!("{}m", u64::MAX), Duration::MAX)] fn test_parse_duration_when_time_units_are_given(#[case] source: &str, #[case] expected: Duration) { let duration = DurationParser::with_all_time_units().parse(source).unwrap(); assert_eq!(duration, expected); } #[rstest] #[case::seconds("1s", vec![TimeUnit::Second])] #[case::hour("1h", vec![TimeUnit::Hour])] fn test_parser_when_time_units(#[case] source: &str, #[case] time_units: Vec) { DurationParser::with_time_units(&time_units) .parse(source) .unwrap(); } #[rstest] #[case::minute_short("1s", vec![TimeUnit::Minute])] fn test_parser_when_time_units_are_not_present_then_error( #[case] source: &str, #[case] time_units: Vec, ) { assert!( DurationParser::with_time_units(time_units.as_slice()) .parse(source) .is_err() ); } #[rstest] #[case::empty("", ParseError::Empty)] #[case::only_space(" ", ParseError::Syntax(0, "Invalid input: ' '".to_string()))] #[case::space_before_number(" 123", ParseError::Syntax(0, "Invalid input: ' 123'".to_string()))] #[case::space_at_end_of_input("123 ns ", ParseError::TimeUnit(4, "Invalid time unit: 'ns '".to_string()))] #[case::other_whitespace("123\tns", ParseError::TimeUnit(3, "Invalid time unit: '\tns'".to_string()))] fn test_parser_when_allow_delimiter_then_error(#[case] input: &str, #[case] expected: ParseError) { assert_eq!( DurationParser::with_all_time_units() .allow_delimiter(Some(|b| b == b' ')) .parse(input) .unwrap_err(), expected ); } #[rstest] #[case::without_delimiter("123ns", |b : u8| b.is_ascii_whitespace(), Duration::positive(0, 123))] #[case::all_rust_whitespace("123 \t\n\x0C\rns", |b : u8| b.is_ascii_whitespace(), Duration::positive(0, 123))] fn test_parser_when_allow_delimiter( #[case] input: &str, #[case] delimiter: fundu::Delimiter, #[case] expected: Duration, ) { assert_eq!( DurationParser::with_all_time_units() .allow_delimiter(Some(delimiter)) .parse(input) .unwrap(), expected ); } #[rstest] #[case::without_spaces("123ns", Duration::positive(0, 123))] #[case::single_space("123 ns", Duration::positive(0, 123))] #[case::multiple_spaces("123 ns", Duration::positive(0, 123))] fn test_parser_when_allow_delimiter_then_ok(#[case] input: &str, #[case] expected: Duration) { assert_eq!( DurationParser::with_all_time_units() .allow_delimiter(Some(|b| b == b' ')) .parse(input), Ok(expected) ); } #[rstest] #[case::nano_seconds("ns", Ok(Duration::positive(0, 1)))] #[case::exponent_without_mantissa("e1", Err(ParseError::Syntax(0, "Exponent must have a mantissa".to_string())))] #[case::just_point(".", Err(ParseError::Syntax(0, "Either the whole number part or the fraction must be present".to_string())))] fn test_parser_when_number_is_optional( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .number_is_optional(true) .parse(input), expected ); } #[rstest] #[case::starts_with_delimiter("\rd", ParseError::Syntax(0, "Input may not start with a delimiter".to_string()))] fn test_parser_when_number_is_optional_and_allow_delimiter_then_error( #[case] input: &str, #[case] expected: ParseError, ) { assert_eq!( DurationParser::with_all_time_units() .number_is_optional(true) .allow_delimiter(Some(|byte| byte.is_ascii_whitespace())) .parse(input), Err(expected) ); } #[rstest] #[case::whole_with_just_point("1.", Err(ParseError::Syntax(1, "No fraction allowed".to_string())))] #[case::fract_with_just_point(".1", Err(ParseError::Syntax(0, "No fraction allowed".to_string())))] #[case::just_point(".", Err(ParseError::Syntax(0, "No fraction allowed".to_string())))] fn test_parser_when_disable_fraction( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .disable_fraction(true) .parse(input), expected ); } #[rstest] #[case::whole_with_exponent("1e0", Err(ParseError::Syntax(1, "No exponent allowed".to_string())))] #[case::fract_with_exponent("0.1e0", Err(ParseError::Syntax(3, "No exponent allowed".to_string())))] #[case::exponent_without_number("1e", Err(ParseError::Syntax(1, "No exponent allowed".to_string())))] fn test_parser_when_disable_exponent( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .disable_exponent(true) .parse(input), expected ); } #[rstest] #[case::whole_with_exponent("i", Err(ParseError::Syntax(0, "Invalid input: 'i'".to_string())))] #[case::whole_with_exponent("in", Err(ParseError::Syntax(0, "Invalid input: 'in'".to_string())))] #[case::whole_with_exponent("inf", Err(ParseError::Syntax(0, "Invalid input: 'inf'".to_string())))] #[case::whole_with_exponent("+inf", Err(ParseError::Syntax(1, "Invalid input: 'inf'".to_string())))] #[case::whole_with_exponent("-inf", Err(ParseError::Syntax(1, "Invalid input: 'inf'".to_string())))] #[case::whole_with_exponent("infi", Err(ParseError::Syntax(0, "Invalid input: 'infi'".to_string())))] #[case::whole_with_exponent("infinity", Err(ParseError::Syntax(0, "Invalid input: 'infinity'".to_string())))] fn test_parser_when_disable_infinity( #[case] input: &str, #[case] expected: Result, ) { assert_eq!( DurationParser::with_all_time_units() .disable_infinity(true) .parse(input), expected ); } #[rstest] #[case::minute_short("1s", TimeUnit::Minute)] fn test_parser_when_custom_time_unit_then_error(#[case] source: &str, #[case] time_unit: TimeUnit) { assert!( DurationParser::with_time_units(&[time_unit]) .parse(source) .is_err() ); } #[rstest] #[case::time_unit_error_when_no_time_units( DurationParser::without_time_units(), "1y", ParseError::TimeUnit(1, "No time units allowed but found: 'y'".to_string()), "Time unit error: No time units allowed but found: 'y' at column 1" )] #[case::time_unit_error_when_all_time_units( DurationParser::with_all_time_units(), "1years", ParseError::TimeUnit(1, "Invalid time unit: 'years'".to_string()), "Time unit error: Invalid time unit: 'years' at column 1" )] #[case::negative_exponent_overflow_error( DurationParser::new(), "1e-32769", ParseError::NegativeExponentOverflow, "Negative exponent overflow: Minimum is -32768" )] #[case::positive_exponent_overflow_error( DurationParser::new(), "1e+32768", ParseError::PositiveExponentOverflow, "Positive exponent overflow: Maximum is +32767" )] #[case::negative_number_error( DurationParser::new(), "-1", ParseError::NegativeNumber, "Number was negative" )] #[case::negative_number_when_infinity_error( DurationParser::new(), "-inf", ParseError::NegativeNumber, "Number was negative" )] fn test_parse_error_messages( #[case] parser: DurationParser, #[case] input: &str, #[case] expected_error: ParseError, #[case] expected_string: &str, ) { assert_eq!(parser.parse(input), Err(expected_error)); assert_eq!( parser.parse(input).unwrap_err().to_string(), expected_string ); } #[rstest] #[case::nano_second(NanoSecond, Duration::positive(0, 1))] #[case::micro_second(MicroSecond, Duration::positive(0, 1_000))] #[case::milli_second(MilliSecond, Duration::positive(0, 1_000_000))] #[case::second(Second, Duration::positive(1, 0))] #[case::minute(Minute, Duration::positive(60, 0))] #[case::hour(Hour, Duration::positive(60 * 60, 0))] #[case::day(Day, Duration::positive(60 * 60 * 24, 0))] #[case::week(Week, Duration::positive(60 * 60 * 24 * 7, 0))] #[case::month(Month, Duration::positive(MONTH, 0))] #[case::year(Year, Duration::positive(YEAR, 0))] fn test_parser_setting_default_time_unit(#[case] time_unit: TimeUnit, #[case] expected: Duration) { assert_eq!( DurationParser::without_time_units() .default_unit(time_unit) .parse("1") .unwrap(), expected ); } #[rstest] #[case::zero_zero("0 0", Duration::ZERO)] #[case::zero_conjunction_zero("0 and 0", Duration::ZERO)] #[case::many_whitespace("0 \t\n\r 0", Duration::ZERO)] #[case::zero_one("0 1", Duration::positive(1, 0))] #[case::zero_one_conjunction("0 and 1", Duration::positive(1, 0))] #[case::one_zero("1 0", Duration::positive(1, 0))] #[case::one_zero_conjunction("1 and 0", Duration::positive(1, 0))] #[case::one_one("1 1", Duration::positive(2, 0))] #[case::one_one_conjunction("1 and 1", Duration::positive(2, 0))] #[case::one_one("1 1", Duration::positive(2, 0))] #[case::two_with_time_units("1ns 1ns", Duration::positive(0, 2))] #[case::two_with_time_units_conjunction("1ns and 1ns", Duration::positive(0, 2))] #[case::two_with_time_units_without_delimiter("1ns1ns", Duration::positive(0, 2))] #[case::two_with_fraction_exponent_time_units( "1.123e9ns 1.987e9ns", Duration::positive(3, 110_000_000) )] #[case::two_with_fraction_exponent_time_units_conjunction( "1.123e9ns and 1.987e9ns", Duration::positive(3, 110_000_000) )] #[case::two_when_saturing(&format!("{0}s {0}s", u64::MAX), Duration::MAX)] #[case::multiple_mixed("1ns 1.001Ms1e1ms 9 .9 3m6h", Duration::positive(21789, 910_001_002))] #[case::multiple_mixed_with_conjunction( "1ns and 1.001Ms and1e1ms and 9 .9 and 3m6h", Duration::positive(21789, 910_001_002) )] #[case::single_infinity_short("inf", Duration::MAX)] #[case::single_infinity_long("infinity", Duration::MAX)] #[case::multiple_infinity_short("inf inf", Duration::MAX)] #[case::multiple_infinity_long("infinity infinity", Duration::MAX)] #[case::multiple_infinity_mixed_short_then_long("inf infinity", Duration::MAX)] #[case::multiple_infinity_mixed_long_then_short("infinity inf", Duration::MAX)] #[case::multiple_infinity_short_conjunction("inf and inf", Duration::MAX)] #[case::multiple_infinity_long_conjunction("infinity and infinity", Duration::MAX)] fn test_parser_when_setting_parse_multiple(#[case] input: &str, #[case] expected: Duration) { let parser = DurationParser::builder() .all_time_units() .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) .build(); assert_eq!(parser.parse(input), Ok(expected)) } #[rstest] #[case::empty("", ParseError::Empty)] #[case::only_conjunction("and", ParseError::Syntax(0, "Invalid input: 'and'".to_string()))] #[case::only_whitespace(" \t\n", ParseError::Syntax(0, "Invalid input: ' \t\n'".to_string()))] #[case::just_point(".", ParseError::Syntax(0, "Either the whole number part or the fraction must be present".to_string()))] #[case::two_points("1..1", ParseError::TimeUnit(2, "Invalid time unit: '.'".to_string()))] #[case::just_time_unit("ns", ParseError::Syntax(0, "Invalid input: 'ns'".to_string()))] #[case::valid_then_invalid("1 a", ParseError::Syntax(2, "Invalid input: 'a'".to_string()))] #[case::end_with_space("1 1 ", ParseError::Syntax(3, "Input may not end with a delimiter".to_string()))] #[case::end_with_conjunction("1 and", ParseError::Syntax(2, "Input may not end with a conjunction but found: 'and'".to_string()))] #[case::end_with_wrong_conjunction("1 anda", ParseError::Syntax(5, "A conjunction must be separated by a delimiter or digit but found: 'a'".to_string()))] #[case::end_with_conjunction_and_delimiter("1 and ", ParseError::Syntax(5, "Input may not end with a delimiter".to_string()))] #[case::valid_time_unit_end_with_delimiter("1s ", ParseError::Syntax(2, "Input may not end with a delimiter".to_string()))] #[case::two_end_with_conjunction("1 1 and", ParseError::Syntax(4, "Input may not end with a conjunction but found: 'and'".to_string()))] #[case::invalid_then_valid("a 1", ParseError::Syntax(0, "Invalid input: 'a 1'".to_string()))] #[case::multiple_invalid("a a", ParseError::Syntax(0, "Invalid input: 'a a'".to_string()))] #[case::infinity_then_space("inf ", ParseError::Syntax(3, "Input may not end with a delimiter".to_string()))] #[case::infinity_then_conjunction("inf and", ParseError::Syntax(4, "Input may not end with a conjunction but found: 'and'".to_string()))] #[case::infinity_short_then_number("inf1", ParseError::Syntax(3, "Error parsing infinity: Invalid character '1'".to_string()))] #[case::infinity_long_then_number("infinity1", ParseError::Syntax(8, "Error parsing infinity: Expected a delimiter but found '1'".to_string()))] #[case::premature_end_parsing_infininity("infi", ParseError::Syntax(0, "Error parsing infinity: 'infi' is an invalid identifier for infinity".to_string()))] fn test_parser_when_setting_parse_multiple_then_error( #[case] input: &str, #[case] expected: ParseError, ) { let parser = DurationParser::builder() .all_time_units() .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) .build(); assert_eq!(parser.parse(input), Err(expected)) } #[test] fn test_parser_when_parse_multiple_number_is_optional_allow_delimiter() { let delimiter = |byte: u8| byte == b' '; let parser = DurationParser::builder() .all_time_units() .parse_multiple(delimiter, Some(&["and"])) .number_is_optional() .allow_delimiter(delimiter) .build(); assert_eq!(parser.parse("1 ns 1 s"), Ok(Duration::positive(1, 1))) } #[test] fn test_parser_when_parse_multiple_number_is_optional_not_allow_delimiter() { let delimiter = |byte: u8| byte == b' '; let parser = DurationParser::builder() .all_time_units() .parse_multiple(delimiter, Some(&["and"])) .number_is_optional() .build(); assert_eq!(parser.parse("1 ns 1 s"), Ok(Duration::positive(3, 1))) } #[test] fn test_parser_when_parse_multiple_with_invalid_delimiter() { let delimiter = |byte: u8| byte == 0xb5; let parser = CustomDurationParser::builder() .time_unit(CustomTimeUnit::with_default(MicroSecond, &["µ"])) .parse_multiple(delimiter, None) .build(); // The delimiter will split the multibyte µ and produces invalid utf-8 // µ = 0xc2 0xb5 assert_eq!( parser.parse("1µ"), Err(ParseError::TimeUnit( 1, "Invalid utf-8 when applying the delimiter".to_string() )) ) } #[test] fn test_parser_when_allow_ago_with_invalid_delimiter() { let delimiter = |byte: u8| byte == 0xb5; let parser = CustomDurationParser::builder() .time_unit(CustomTimeUnit::with_default(MicroSecond, &["µ"])) .allow_ago(delimiter) .build(); // The delimiter will split the multibyte µ and produces invalid utf-8 // µ = 0xc2 0xb5 assert_eq!( parser.parse("1µ ago"), Err(ParseError::TimeUnit( 1, "Invalid utf-8 when applying the delimiter".to_string() )) ) } #[test] fn test_parser_parse_multiple_and_keywords_with_invalid_delimiter() { let delimiter = |byte: u8| byte == 0xb5; let parser = CustomDurationParser::builder() .time_unit(CustomTimeUnit::with_default(MicroSecond, &["µ"])) .keyword(TimeKeyword::new(MicroSecond, &["manµ"], None)) .parse_multiple(delimiter, None) .build(); // The delimiter will split the multibyte µ and produces invalid utf-8 // µ = 0xc2 0xb5 assert_eq!( parser.parse("someµ"), Err(ParseError::Syntax( 4, "Invalid utf-8 when applying the delimiter".to_string() )) ) } #[rstest] #[case::only_numbers("1 1", Ok(Duration::positive(2, 0)))] #[case::with_time_units("1ns 1ns", Err(ParseError::Syntax(1, "Invalid input: 'ns 1ns'".to_string())))] #[case::number_then_with_time_unit("1 1ns", Err(ParseError::Syntax(3, "Invalid input: 'ns'".to_string())))] #[case::ago_without_time_unit("1 ago", Err(ParseError::Syntax(2, "Invalid input: 'ago'".to_string())))] // FIXME: Improve error message in such a case fn test_parser_when_parse_multiple_without_time_units( #[case] input: &str, #[case] expected: Result, ) { let delimiter = |byte: u8| byte == b' '; let parser = CustomDurationParser::builder() .parse_multiple(delimiter, Some(&["and"])) .allow_ago(delimiter) .build(); assert_eq!(parser.parse(input), expected); } #[test] fn test_parser_when_parse_multiple_without_time_units_default_unit_is_not_seconds() { let delimiter = |byte: u8| byte == b' '; let parser = DurationParser::builder() .parse_multiple(delimiter, Some(&["and"])) .default_unit(NanoSecond) .build(); assert_eq!(parser.parse("1 1"), Ok(Duration::positive(0, 2))); } #[rstest] #[case::nano_second(&["ns", "nsec"], Duration::positive(0, 1))] #[case::micro_second(&["us", "µs", "usec"], Duration::positive(0, 1000))] #[case::milli_second(&["ms", "msec"], Duration::positive(0, 1_000_000))] #[case::second(&["s", "sec", "second", "seconds"], Duration::positive(1, 0))] #[case::minute(&["m", "min", "minute", "minutes"], Duration::positive(60, 0))] #[case::hour(&["h", "hr", "hour", "hours"], Duration::positive(60 * 60, 0))] #[case::day(&["d", "day", "days"], Duration::positive(60 * 60 * 24, 0))] #[case::week(&["w", "week", "weeks"], Duration::positive(60 * 60 * 24 * 7, 0))] #[case::month(&["M", "month", "months"], Duration::positive(MONTH, 0))] #[case::year(&["y", "year", "years"], Duration::positive(YEAR, 0))] fn test_custom_duration_parser_parse_when_systemd_time_units( #[case] inputs: &[&str], #[case] expected: Duration, ) { let parser = CustomDurationParser::with_time_units(&SYSTEMD_TIME_UNITS); for input in inputs { assert_eq!(parser.parse(&format!("1{input}")), Ok(expected)); } } #[rstest] #[case::negative_zero("-0", Duration::ZERO)] #[case::negative_barely_not_zero("-0.000000001", Duration::negative(0, 1))] #[case::negative_barely_zero("-0.0000000001", Duration::negative(0, 0))] // FIXME: should be Duration::ZERO #[case::positive_zero("+0", Duration::ZERO)] #[case::positive_barely_not_zero("0.000000001", Duration::positive(0, 1))] #[case::positive_barely_zero("0.0000000001", Duration::ZERO)] #[case::zero_without_sign("0", Duration::ZERO)] #[case::negative_one("-1", Duration::negative(1, 0))] #[case::negative_one_with_fraction("-1.2", Duration::negative(1, 200_000_000))] #[case::negative_one_with_exponent("-1e-9", Duration::negative(0, 1))] #[case::negative_min_seconds(&format!("-{}", u64::MAX), Duration::negative(u64::MAX, 0))] #[case::negative_min_seconds_low_nanos(&format!("-{}.000000001", u64::MAX), Duration::negative(u64::MAX, 1))] #[case::negative_min_seconds_and_nanos(&format!("-{}.999999999", u64::MAX), Duration::MIN)] #[case::negative_min_nanos_10_digits(&format!("-{}.9999999999", u64::MAX), Duration::MIN)] #[case::negative_seconds_barely_saturate(&format!("-{}", u64::MAX as u128 + 1), Duration::MIN)] #[case::negative_some_mixed_number( "-1122334455.123456789e-4", Duration::negative(112233, 445512345) )] #[case::negative_years("-1.000000001y", Duration::negative(YEAR, YEAR as u32))] #[case::negative_high_value_saturate(&format!("-{}.{}e1000", "1".repeat(1000), "1".repeat(1000)), Duration::MIN)] #[case::positive_one("1", Duration::positive(1, 0))] #[case::positive_max_seconds(&format!("{}", u64::MAX), Duration::positive(u64::MAX, 0))] #[case::positive_max_seconds_low_nanos(&format!("{}.000000001", u64::MAX), Duration::positive(u64::MAX, 1))] #[case::positive_max_seconds_and_nanos(&format!("{}.999999999", u64::MAX), Duration::MAX)] #[case::positive_seconds_barely_saturate(&format!("{}", u64::MAX as u128 + 1), Duration::MAX)] #[case::positive_seconds_and_nanos_barely_saturate(&format!("{}.000000001", u64::MAX as u128 + 1), Duration::MAX)] #[case::positive_some_mixed_number( "1122334455.123456789e-4", Duration::positive(112233, 445512345) )] #[case::positive_years("1.000000001y", Duration::positive(YEAR, YEAR as u32))] #[case::positive_high_value_saturate(&format!("{}.{}e1000", "1".repeat(1000), "1".repeat(1000)), Duration::MAX)] #[case::time_unit_causes_saturate_negative(&format!("-{}y", u64::MAX), Duration::MIN)] fn test_parse_negative(#[case] source: &str, #[case] expected: Duration) { let actual = DurationParser::with_all_time_units() .allow_negative(true) .parse(source) .unwrap(); assert_eq!(actual, expected); } #[rstest] #[case::simple_zero("0", Duration::ZERO)] #[case::two_zero("0 0", Duration::ZERO)] #[case::two_one("1 1", Duration::positive(2, 0))] #[case::negative_and_positive_then_zero("-1 1", Duration::ZERO)] #[case::two_negative("-1 -1", Duration::negative(2, 0))] #[case::two_negative_conjunction("-1 and -1", Duration::negative(2, 0))] #[case::two_negative_with_time_units("-1ns -1s", Duration::negative(1, 1))] #[case::two_negative_with_time_units_conjunction("-1ns and -1s", Duration::negative(1, 1))] #[case::negative_and_positive_with_time_units("1ns -1s", Duration::negative(0, 999_999_999))] #[case::two_negative_mixed("-1.1ms -1e-9s", Duration::negative(0, 1_100_001))] #[case::two_negative_saturate_negative(&format!("-{}s -1s", u64::MAX), Duration::MIN)] #[case::two_positive_saturate_positive(&format!("{}s 1s", u64::MAX), Duration::MAX)] #[case::negative_infinity_short("-inf", Duration::MIN)] #[case::two_negative_infinity_short_then_saturate("-inf -inf", Duration::MIN)] #[case::negative_infinity_long("-infinity", Duration::MIN)] #[case::two_negative_infinity_long_then_saturate("-infinity -infinity", Duration::MIN)] #[case::two_negative_infinity_long_conjunction_then_saturate( "-infinity and -infinity", Duration::MIN )] #[case::two_positive_infinity_long_then_saturate_max("infinity infinity", Duration::MAX)] #[case::negative_infinity_and_positive_infinity_no_error("-inf +inf", Duration::ZERO)] #[case::negative_infinity_and_positive_infinity_conjunction("-inf and +inf", Duration::ZERO)] fn test_parse_negative_when_multiple(#[case] input: &str, #[case] expected: Duration) { assert_eq!( DurationParser::builder() .all_time_units() .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) .allow_negative() .build() .parse(input) .unwrap(), expected ); } #[rstest] #[case::i_without_number("i", "i")] #[case::big_i_without_number("I", "I")] #[case::simple_plus_i("i", "+i")] #[case::simple_i_one("i", "1i")] #[case::simple_i_with_fraction("i", "1.0i")] #[case::simple_i_with_exponent("i", "1.0e0i")] #[case::simple_inf("inf", "inf")] #[case::infi("infi", "infi")] #[case::infinity("infinity", "infinity")] #[case::one_infinity("infinity", "1infinity")] fn test_custom_parser_when_disable_infinity_then_no_problems_with_infinity_like_ids( #[case] id: &str, #[case] input: &str, ) { let ids = &[id]; let parser = CustomDurationParserBuilder::new() .disable_infinity() .number_is_optional() .time_units(&[CustomTimeUnit::with_default(NanoSecond, ids)]) .build(); assert_eq!(parser.parse(input), Ok(Duration::positive(0, 1))); } #[rstest] #[case::positive_zero("0s", Duration::ZERO)] #[case::negative_zero("-0s", Duration::ZERO)] #[case::positive_second("1s", Duration::negative(1, 0))] #[case::negative_second("-1s", Duration::positive(1, 0))] #[case::positive_two_seconds("2s", Duration::negative(2, 0))] #[case::negative_two_seconds("-2s", Duration::positive(2, 0))] #[case::positive_fraction("0.1s", Duration::negative(0, 100_000_000))] #[case::negative_fraction("-0.1s", Duration::positive(0, 100_000_000))] #[case::positive_overflow(&format!("{}s", u128::MAX), Duration::MIN)] #[case::negative_overflow(&format!("-{}s", u128::MAX), Duration::MAX)] fn test_custom_parser_when_negative_multipier(#[case] input: &str, #[case] expected: Duration) { let parser = CustomDurationParserBuilder::new() .time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(-1, 0)))) .allow_negative() .build(); let actual = parser.parse(input).unwrap(); assert_eq!(actual, expected); } #[rstest] #[case::yesterday("yesterday", Duration::negative(60 * 60 * 24, 0))] #[case::negative_yesterday("-yesterday", Duration::positive(60 * 60 * 24, 0))] #[case::positive_yesterday("+yesterday", Duration::negative(60 * 60 * 24, 0))] #[case::tomorrow("tomorrow", Duration::positive(60 * 60 * 24, 0))] #[case::negative_tomorrow("-tomorrow", Duration::negative(60 * 60 * 24, 0))] #[case::today("today", Duration::ZERO)] #[case::negative_today("-today", Duration::ZERO)] fn test_custom_parser_with_keywords(#[case] input: &str, #[case] expected: Duration) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new( Day, &["yesterday"], Some(Multiplier(-1, 0)), )) .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .keyword(TimeKeyword::new(Day, &["today"], Some(Multiplier(0, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .allow_negative() .build(); let actual = parser.parse(input).unwrap(); assert_eq!(actual, expected); } #[rstest] #[case::leading_space(" tomorrow", ParseError::Syntax(0, "Invalid input: ' tomorrow'".to_string()))] #[case::trailing_space("tomorrow ", ParseError::Syntax(0, "Invalid input: 'tomorrow '".to_string()))] #[case::incomplete_keyword("tomorro", ParseError::Syntax(0, "Invalid input: 'tomorro'".to_string()))] #[case::number("1tomorrow", ParseError::TimeUnit(1, "Invalid time unit: 'tomorrow'".to_string()))] #[case::number_with_space("1 tomorrow", ParseError::TimeUnit(1, "Invalid time unit: ' tomorrow'".to_string()))] fn test_custom_parser_with_keywords_then_error(#[case] input: &str, #[case] expected: ParseError) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .build(); assert_eq!(parser.parse(input), Err(expected)); } #[rstest] #[case::single_tomorrow("tomorrow", Duration::positive(60 * 60 * 24, 0))] #[case::tomorrow_then_number("tomorrow1.1", Duration::positive(60 * 60 * 24 + 1, 100_000_000))] #[case::two_tomorrow("tomorrow tomorrow", Duration::positive(60 * 60 * 24 * 2, 0))] #[case::yesterday_tomorrow("yesterday tomorrow", Duration::ZERO)] #[case::number_tomorrow("1 tomorrow", Duration::positive(60 * 60 * 24 + 1, 0))] #[case::fraction_tomorrow(".1 tomorrow", Duration::positive(60 * 60 * 24, 100_000_000))] #[case::number_conjunction_keyword(".1 and tomorrow", Duration::positive(60 * 60 * 24, 100_000_000))] #[case::keyword_conjunction_number("tomorrow and .1", Duration::positive(60 * 60 * 24, 100_000_000))] #[case::keyword_conjunction_no_delimiter_number("tomorrow and1.1", Duration::positive(60 * 60 * 24 + 1, 100_000_000))] fn test_custom_parser_with_keywords_when_parse_multiple( #[case] input: &str, #[case] expected: Duration, ) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new( Day, &["yesterday"], Some(Multiplier(-1, 0)), )) .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) .allow_negative() .build(); assert_eq!(parser.parse(input), Ok(expected)); } #[rstest] #[case::nano_second("ns", Duration::positive(0, 1))] #[case::two_times_nano_second("ns ns", Duration::positive(0, 2))] #[case::two_times_nano_second_with_conjunction("ns and ns", Duration::positive(0, 2))] #[case::tomorrow_nano_second("tomorrow ns", Duration::positive(60 * 60 * 24, 1))] #[case::tomorrow_conjunction_nano_second("tomorrow and ns", Duration::positive(60 * 60 * 24, 1))] #[case::nano_second_tomorrow("ns tomorrow", Duration::positive(60 * 60 * 24, 1))] #[case::nano_second_conjunction_tomorrow("ns and tomorrow", Duration::positive(60 * 60 * 24, 1))] #[case::one_tomorrow_nano_second("1 tomorrow ns", Duration::positive(60 * 60 * 24 + 1, 1))] #[case::one_tomorrow_nano_second_with_conjunctions("1 and tomorrow and ns", Duration::positive(60 * 60 * 24 + 1, 1))] fn test_custom_parser_with_keywords_when_parse_multiple_and_number_is_optional( #[case] input: &str, #[case] expected: Duration, ) { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new(Day, &["tomorrow"], Some(Multiplier(1, 0)))) .time_units(&[ CustomTimeUnit::with_default(NanoSecond, &["ns"]), CustomTimeUnit::with_default(Second, &["s", "second"]), ]) .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) .number_is_optional() .build(); assert_eq!(parser.parse(input), Ok(expected)); } #[test] fn test_custom_parser_with_negative_keyword_when_not_allow_negative_then_error() { let parser = CustomDurationParserBuilder::new() .keyword(TimeKeyword::new( Day, &["yesterday"], Some(Multiplier(-1, 0)), )) .build(); assert_eq!(parser.parse("yesterday"), Err(ParseError::NegativeNumber)); } #[rstest] #[case::one("1", Duration::positive(1, 0))] #[case::second("1s", Duration::positive(1, 0))] #[case::seconds_ago("1s ago", Duration::negative(1, 0))] #[case::seconds_ago_big("1s AGO", Duration::negative(1, 0))] #[case::seconds_ago_mixed("1s aGO", Duration::negative(1, 0))] #[case::negative_seconds_ago("-1s ago", Duration::positive(1, 0))] #[case::day_ago("1day ago", Duration::negative(60 * 60 * 24, 0))] #[case::with_delimiter_between_number_and_time_unit("1 s ago", Duration::negative(1, 0))] fn test_custom_parser_with_allow_ago(#[case] input: &str, #[case] expected: Duration) { let parser = CustomDurationParserBuilder::new() .time_units(&SYSTEMD_TIME_UNITS) .allow_delimiter(|byte| byte.is_ascii_whitespace()) .allow_ago(|byte| byte.is_ascii_whitespace()) .allow_negative() .build(); assert_eq!(parser.parse(input), Ok(expected)); } #[rstest] #[case::ago_without_time_unit("1 :ago", ParseError::Syntax(2, "Expected end of input but found: ':'".to_string()))] // TODO: Improve the error message #[case::ago_as_time_unit("1 ago", ParseError::TimeUnit(2, "Invalid time unit: 'ago'".to_string()))] #[case::just_ago("ago", ParseError::Syntax(0, "Invalid input: 'ago'".to_string()))] #[case::incomplete_ago("1s:ag", ParseError::TimeUnit(3, "Found unexpected keyword: 'ag'".to_string()))] #[case::one_second_twice("1s:1s", ParseError::TimeUnit(3, "Found unexpected keyword: '1s'".to_string()))] fn test_custom_parser_with_allow_ago_then_error(#[case] input: &str, #[case] expected: ParseError) { let parser = CustomDurationParserBuilder::new() .time_units(&SYSTEMD_TIME_UNITS) .allow_delimiter(|byte| byte.is_ascii_whitespace()) .allow_ago(|byte| matches!(byte, b':')) .allow_negative() .build(); assert_eq!(parser.parse(input), Err(expected)); } #[rstest] #[case::seconds_without_ago("1s", Duration::positive(1, 0))] #[case::seconds("1s ago", Duration::negative(1, 0))] #[case::twice_seconds("1s ago 1s ago", Duration::negative(2, 0))] #[case::seconds_and_day("1s ago 1day ago", Duration::negative(60 * 60 * 24 + 1, 0))] #[case::two_without_delimiter_between_multiple("1s ago1s ago", Duration::negative(2, 0))] #[case::two_with_delimiter_between_number_and_time_unit( "1 s ago 1 day ago", Duration::negative(60 * 60 * 24 + 1, 0) )] #[case::two_with_conjunction_between_number_and_time_unit( "1 s ago and 1 day ago", Duration::negative(60 * 60 * 24 + 1, 0) )] #[case::without_ago_conjunction_and_with_ago( "1 s and 1 day ago", Duration::negative(60 * 60 * 24 - 1, 0) )] #[case::without_ago_and_with_ago( "1 s 1 day ago", Duration::negative(60 * 60 * 24 - 1, 0) )] #[case::with_ago_and_without_ago( "1s ago 1 day", Duration::positive(60 * 60 * 24 - 1, 0) )] #[case::with_ago_conjuntion_without_ago( "1s ago and 1 day", Duration::positive(60 * 60 * 24 - 1, 0) )] fn test_custom_parser_with_allow_ago_when_parse_multiple( #[case] input: &str, #[case] expected: Duration, ) { let parser = CustomDurationParserBuilder::new() .time_units(&SYSTEMD_TIME_UNITS) .allow_delimiter(|byte| byte.is_ascii_whitespace()) .allow_ago(|byte| byte.is_ascii_whitespace()) .allow_negative() .parse_multiple(|byte| byte.is_ascii_whitespace(), Some(&["and"])) .build(); assert_eq!(parser.parse(input), Ok(expected)); }