Configurable, precise and fast string parser to a rust std::time::Duration

Released API Docs | Changelog


GitHub branch checks state Crates.io docs.rs MSRV

Table of Contents

Overview

fundu provides a parser to convert strings into a [std::time::Duration]. It tries to improve on the standard methods [Duration::from_secs_f64] and [Duration::try_from_secs_f64] (which is stable since 1.66.0) with intermediate parsing to a float via [f64::from_str] by

This library aims for low runtime costs (See Benchmarks) and being a lightweight crate. fundu is purely built on top of the rust stdlib, and there are no additional dependencies required. The accepted string format is almost the same like the scientific floating point format and compatible to the [f64::from_str] format. In other words, if the accepted input string could previously converted to an f64 with f64::from_str, no change is needed to accept the same format with fundu. For a direct comparison of fundu vs the rust native methods Duration::(try_)from_secs_f64 see Comparison. For further details see the Documentation!

Installation

Add this to Cargo.toml for fundu with the standard feature.

toml [dependencies] fundu = "0.4.1"

fundu is split into two 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. 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 = "0.4.1", default-features = false, features = ["custom"] }

Examples

If only the default parser is required once, then the parse_duration method can be used.

```rust use fundu::parse_duration; use std::time::Duration;

let input = "1.0e2s"; asserteq!(parseduration(input).unwrap(), Duration::new(100, 0)); ```

When a customization of the accepted TimeUnits is required, then the builder DurationParser can be used.

```rust use fundu::DurationParser; use std::time::Duration;

let input = "3m"; asserteq!( DurationParser::withalltimeunits().parse(input).unwrap(), Duration::new(180, 0) ); ```

When no time units are configured, seconds is assumed.

```rust use fundu::DurationParser; use std::time::Duration;

let input = "1.0e2"; asserteq!( DurationParser::withouttime_units().parse(input).unwrap(), Duration::new(100, 0) ); ```

However, setting the default time unit to something different than seconds can be achieved with

```rust use fundu::{DurationParser, TimeUnit::*}; use std::time::Duration;

asserteq!( DurationParser::withouttimeunits().defaultunit(MilliSecond).parse("1000").unwrap(), Duration::new(1, 0) ); ```

Note the following will return an error because y (Years) is not in the default set of TimeUnits.

```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::{DurationParser, TimeUnit::*}; use std::time::Duration;

let parser = DurationParser::withtimeunits(&[NanoSecond, Minute, Hour]); for (input, expected) in &[ ("9e3ns", Duration::new(0, 9000)), ("10m", Duration::new(600, 0)), ("1.1h", Duration::new(3960, 0)), ("7", Duration::new(7, 0)), ] { assert_eq!(parser.parse(input).unwrap(), *expected); } ```

The identifiers for time units can be fully customized with any number of valid utf-8 sequences if the custom feature is activated:

```rust use fundu::{CustomDurationParser, TimeUnit::*}; use std::time::Duration;

let parser = CustomDurationParser::withtimeunits( &[ (MilliSecond, &["χιλιοστό του δευτερολέπτου"]), (Second, &["s", "secs", "..."]), (Hour, &["⏳"]) ] ); for (input, expected) in &[ (".3χιλιοστό του δευτερολέπτου", Duration::new(0, 300000)), ("1e3...", Duration::new(1000, 0)), ("1.1⏳", Duration::new(3960, 0)), ] { asserteq!(parser.parse(input).unwrap(), *expected); } ```

Also, fundu tries to give informative error messages

```rust use fundu::DurationParser; use std::time::Duration;

asserteq!( DurationParser::withouttimeunits() .parse("1y") .unwraperr() .to_string(), "Time unit error: No time units allowed but found: 'y' at column 1" ); ```

See also the examples folder for common recipes. Run an example with

shell cargo run --example $FILE_NAME_WITHOUT_FILETYPE_SUFFIX

Time units

Time units are used to calculate the final Duration. Second is the default time unit (if not specified otherwise) and if no time unit was specified 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.

Name | Time unit | Calculation | DurationParser::new \| parse_duration | DurationParser:: with_all_time_units | DurationParser:: without_time_units --- | --- | --- | --- | --- | --- Nanoseconds | ns | 1e-9s | ☑ | ☑ | ☐ Microseconds | Ms | 1e-6s | ☑ | ☑ | ☐ Milliseconds | ms | 1e-3s |☑ | ☑ | ☐ Seconds | s | SI definition | ☑ | ☑ | ☐ Minutes | m | 60s | ☑ | ☑ | ☐ Hours | h | 60m | ☑ | ☑ | ☐ Days | d | 24h | ☑ | ☑ | ☐ Weeks | w | 7d | ☑ | ☑ | ☐ Months | M | Year / 12 | ☐ | ☑ | ☐ Years | 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 based calculation is used. (See table above)

With the CustomDurationParser in the custom feature, the identifiers for time units can be fully customized.

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

Benchmarks can be 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 two inputs on a comparatively slow machine (Quad core 3000Mhz, 8GB DDR3, Linux)

Input | parser with time units | avg parsing time | ~ samples / s --- | --- | --- | --- 1 | no | 48.716 ns | 20_527_136.874 1 | yes | 52.548 ns | 19_030_219.989 format!("{}.{}e-1022", "1".repeat(1022), "1".repeat(1022)) | no | 3.7219 µs | 268_679.975 format!("{}.{}e-1022", "1".repeat(1022), "1".repeat(1022)) | yes | 3.7132 µs | 269_309.490

For comparison, fundu's precision and additional features only add a very low performance overhead (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 format!("{}.{}e-1022", "1".repeat(1022), "1".repeat(1022)) | 1.7457 µs | 572_836.111

Comparison fundu vs Duration::(try_)from_secs_f64

Here's a short incomplete overview of differences and advantages of fundu over using Duration::(try_)from_secs_f64(input.parse().unwrap())

Input | Result fundu | Result 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 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

Nevertheless, fundu has a small impact on performance, so if you need to parse a massive amount of inputs and get along without the full precision or any of its features, you may be better off using the native methods from the rust stdlib.

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

TODO

See also Changelog

License

MIT license (LICENSE or http://opensource.org/licenses/MIT)