1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
// Copyright (c) 2023 Joining7943 <joining@posteo.de>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//! # Overview
//!
//! Parse a rust string into a [`std::time::Duration`] or for negative numbers into a
//! [`time::Duration`].
//!
//! `fundu` is a configurable, precise and blazingly fast duration parser
//!
//! * with the flexibility to customize [`TimeUnit`]s, the number format and other aspects
//! * 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 the option to parse negative numbers to negative durations if the `negative` feature is
//! enabled
//! * and with informative error messages
//!
//! # 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`].
//!
//! ## `negative`
//!
//! Enable parsing negative numbers with [`DurationParser::parse_negative`] and
//! [`CustomDurationParser::parse_negative`] into negative durations represented by
//! [`time::Duration`]. If not activated, negative numbers produce a
//! [`ParseError::NegativeNumber`].
//!
//! # Configuration and Format
//!
//! This 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) are 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 setting [`DurationParser::allow_delimiter`].
//!
//! # Format specification
//!
//! The time units are case-sensitive, all other alphabetic characters are case-insensitive
//!
//! ```text
//! Duration ::= Sign? ( 'inf' | 'infinity' | ( Number TimeUnit?))
//! TimeUnit ::= ns | Ms | ms | s | m | h | d | w | M | y
//! Number ::= ( Digit+ |
//! Digit+ '.' Digit* |
//! Digit* '.' Digit+ )
//! Exp?
//! Exp ::= 'e' Sign? Digit+
//! Sign ::= [+-]
//! Digit ::= [0-9]
//! ```
//!
//! Special cases which are not displayed in the specification:
//!
//! * The `TimeUnit` rule is based on the default identifiers as defined in the table above. They
//! can also be completely customized with the [`CustomDurationParser`].
//! * Negative values, including negative infinity are not allowed as long as the `negative` feature
//! is not activated. 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`]
//! * The exponent must be in the range `-32768 <= Exp <= 32767`
//! * If `allow_delimiter` is set then any defined delimiter is allowed between the Number and
//! TimeUnit. This setting also allows the input string to end with this delimiter but only if no
//! time unit was present. The parser then assumes the default time unit.
//!
//! # 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 the builder
//! [`DurationParser`] can be used.
//!
//! ```rust
//! use std::time::Duration;
//!
//! use fundu::DurationParser;
//!
//! let input = "3m";
//! assert_eq!(
//! DurationParser::with_all_time_units().parse(input).unwrap(),
//! Duration::new(180, 0)
//! );
//! ```
//!
//! When no time units are configured, seconds is assumed.
//!
//! ```rust
//! use std::time::Duration;
//!
//! use fundu::DurationParser;
//!
//! let input = "1.0e2";
//! assert_eq!(
//! DurationParser::without_time_units().parse(input).unwrap(),
//! Duration::new(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 std::time::Duration;
//!
//! use fundu::DurationParser;
//! use fundu::TimeUnit::*;
//!
//! let parser = DurationParser::with_time_units(&[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);
//! }
//! ```
//!
//! 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 std::time::Duration;
//!
//! use fundu::DurationParser;
//! use fundu::TimeUnit::*;
//!
//! assert_eq!(
//! DurationParser::without_time_units()
//! .default_unit(MilliSecond)
//! .parse("1000")
//! .unwrap(),
//! Duration::new(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 std::time::Duration;
//!
//! use fundu::CustomDurationParser;
//! use fundu::TimeUnit::*;
//!
//! let parser = CustomDurationParser::with_time_units(&[
//! (MilliSecond, &["χιλιοστό του δευτερολέπτου"]),
//! (Second, &["s", "secs"]),
//! (Hour, &["⏳"]),
//! ]);
//! for (input, expected) in &[
//! (".3χιλιοστό του δευτερολέπτου", Duration::new(0, 300_000)),
//! ("1e3secs", Duration::new(1000, 0)),
//! ("1.1⏳", Duration::new(3960, 0)),
//! ] {
//! assert_eq!(parser.parse(input).unwrap(), *expected);
//! }
//! ```
//!
//! 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 std::time::Duration;
//!
//! use fundu::TimeUnit::*;
//! use fundu::{DurationParser, ParseError};
//!
//! let parser = DurationParser::builder()
//! .custom_time_units(&[NanoSecond])
//! .allow_delimiter(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' '))
//! .number_is_optional()
//! .disable_fraction()
//! .disable_exponent()
//! .build();
//!
//! for (input, expected) in &[
//! ("ns", Duration::new(0, 1)),
//! ("1000\t\n\r ns", Duration::new(0, 1000)),
//! ] {
//! assert_eq!(parser.parse(input).unwrap(), *expected);
//! }
//!
//! for (input, expected) in &[
//! (
//! "1.0ns",
//! ParseError::Syntax(1, "No fraction allowed".to_string()),
//! ),
//! (
//! "1e9ns",
//! ParseError::Syntax(1, "No exponent allowed".to_string()),
//! ),
//! ] {
//! assert_eq!(parser.parse(input).unwrap_err(), *expected);
//! }
//! ```
//!
//! [`time::Duration`]: <https://docs.rs/time/latest/time/struct.Duration.html>
//! [`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;
pub use config::Delimiter;
#[cfg(feature = "custom")]
pub use custom::{
builder::CustomDurationParserBuilder,
parser::CustomDurationParser,
time_units::{
CustomTimeUnit, Identifiers, DEFAULT_ALL_TIME_UNITS, DEFAULT_TIME_UNITS, SYSTEMD_TIME_UNITS,
},
};
pub use error::ParseError;
#[cfg(feature = "standard")]
pub use standard::{
builder::DurationParserBuilder, parser::parse_duration, parser::DurationParser,
};
pub use crate::time::{
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,
};