#duration-parser #time-parser #duration #time #parse-duration #string #parser

fundu

可配置、精确且快速的 Rust 字符串解析器,用于将字符串解析为持续时间

15 个版本 (4 个稳定版)

2.0.0 2023年8月7日
1.2.0 2023年7月9日
1.1.0 2023年6月26日
0.5.1 2023年5月1日
0.5.0 2023年3月29日

#32日期和时间

Download history 5609/week @ 2024-04-07 5556/week @ 2024-04-14 6522/week @ 2024-04-21 9365/week @ 2024-04-28 5695/week @ 2024-05-05 5623/week @ 2024-05-12 6247/week @ 2024-05-19 4162/week @ 2024-05-26 5215/week @ 2024-06-02 4085/week @ 2024-06-09 4712/week @ 2024-06-16 6242/week @ 2024-06-23 7030/week @ 2024-06-30 5839/week @ 2024-07-07 5139/week @ 2024-07-14 5535/week @ 2024-07-21

每月下载 24,237
用于 4 个 Crates (3 个直接使用)

MIT 许可证

415KB
6K SLoC

可配置、精确且快速的 Rust 字符串解析器,用于将字符串解析为持续时间


目录

概述

fundu 提供了一个灵活且快速的解析器,用于将 Rust 字符串转换为 Durationfundu 解析成它自己的 Duration,但提供了将数据转换为 std::time::Durationchrono::Durationtime::Duration 的方法。除非另有说明,否则此 README 描述的是主要的 fundu 软件包。以下是一些使用 standard 功能的有效的输入字符串示例

  • "1.41"
  • "42"
  • "2e-8""2e+8"(或类似地 "2.0e8"
  • ".5" 或类似地 "0.5"
  • "3." 或类似地 "3.0"
  • "inf""+inf""infinity""+infinity"
  • "1w"(1周)或类似地"7d""168h""10080m""604800s"、……

以及自定义(或基本)功能,假设定义了一些自定义时间单位ssecsminutesdaydaysyearyearscentury以及时间关键词yesterday

  • "1.41minutes"或类似地,如果设置了allow_delimiter则可以写成"1.41 minutes"
  • "years"或类似地,如果设置了number_is_optional则可以写成"1 years""1years"
  • "42 secs ago"或类似地,如果设置了allow_agoallow_negative则可以写成"-42 secs"
  • "9e-3s""9e3s"(或类似地"9.0e+3s"
  • "yesterday"或类似地,如果设置了allow_negative则可以写成"-1day""-1days"
  • "9 century"或类似地"900 years"

有关自定义功能的更多示例,请参阅自定义部分。本crate提供功能的摘要

  • 精度:没有浮点计算,输入被精确解析,所以您输入的值就是您得到的值,在Duration的范围内。
  • 性能:解析器速度极快(基准测试
  • 自定义TimeUnits,数字格式和其他方面都易于配置(参见自定义
  • 音量限制:如果输入数字大于该最大值或输入字符串为正 infinity,则持续时间会饱和于 Duration::MAX
  • 负持续时间:解析器可以配置为解析负持续时间。Fundu的Duration可以表示负持续时间,并且实现了TryFrom以支持chrono::Durationtime::Duration,前提是激活了相应的功能。
  • 错误处理:错误信息力求信息丰富,但也可以轻松调整(参见示例

fundu旨在实现良好的性能和轻量级。它完全基于rust的stdlib构建,标准配置中无需额外的依赖。默认接受的数字格式为科学浮点格式,与f64::from_str兼容。然而,数字格式和其他方面可以根据需要自定义,例如与systemd时间跨度GNU相对时间兼容的格式。有两个专门的、易于使用的fundu副项目

  • fundu-systemd,用于完全兼容的systemd时间跨度解析器
  • fundu-gnu,用于完全兼容的GNU相对时间解析器。

请参阅示例部分示例文件夹。

有关详细信息,请参阅文档

安装

要将fundu添加到Cargo.toml,请使用standard功能。

[dependencies]
fundu = "2.0.0"

fundu分为三个主要功能,standard(提供DurationParserparse_duration)和custom(提供CustomDurationParser)以及base,用于更基本的核心解析方法。第一个在这里详细描述,custom功能为时间单位添加了完全可自定义的标识符。通常只需要一个解析器。例如,要仅包含CustomDurationParser,请在Cargo.toml中添加以下内容

[dependencies]
fundu = { version = "2.0.0", default-features = false, features = ["custom"] }

启用 chronotime 功能,为 chrono::Durationtime::Duration 提供 TryFromSaturatingInto 实现。在无需额外功能的情况下,支持将 std::time::Duration 转换为/从转换。

启用 serde 功能允许某些结构和枚举使用 serde 进行序列化或反序列化。

示例

如果只需一次使用默认配置,可以使用 parse_duration 方法。注意,与其它解析器的 parse 方法不同,parse_duration 返回的是 std::time::Duration,其它解析器的 parse 方法返回的是 fundu::Duration

use std::time::Duration;

use fundu::parse_duration;

let input = "1.0e2s";
assert_eq!(parse_duration(input).unwrap(), Duration::new(100, 0));

当需要自定义接受的时间单位(TimeUnit)时,可以使用 DurationParser::with_time_units

use fundu::{Duration, DurationParser};

let input = "3m";
assert_eq!(
    DurationParser::with_all_time_units().parse(input).unwrap(),
    Duration::positive(180, 0)
);

如果没有配置时间单位,则默认为秒。

use fundu::{Duration, DurationParser};

let input = "1.0e2";
assert_eq!(
    DurationParser::without_time_units().parse(input).unwrap(),
    Duration::positive(100, 0)
);

但是,以下将返回错误,因为 y(年)不是默认时间单位。

use fundu::DurationParser;

let input = "3y";
assert!(DurationParser::new().parse(input).is_err());

解析器是可重用的,时间单位集可以完全自定义。

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));

如果将默认时间单位(如果没有在输入字符串中给出时间单位)设置为不同于秒的值,这也是容易实现的。

use fundu::TimeUnit::*;
use fundu::{Duration, DurationParser};

assert_eq!(
    DurationParser::without_time_units()
        .default_unit(MilliSecond)
        .parse("1000")
        .unwrap(),
    Duration::positive(1, 0)
);

如果激活了 custom 功能,可以完全自定义时间单位的标识符,可以使用任意数量的有效 utf-8 序列。

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)));

可以使用 custom 功能来自定义更多内容。有关更多信息,请参阅 custom 功能的导出项的文档(例如 CustomTimeUnitTimeKeyword)。

此外,fundu 尝试提供信息丰富的错误消息。

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"
);

数字格式可以轻松调整以满足您的需求。例如,允许数字是可选的,允许数字和时间单位之间有某些 ascii 空白,并将数字格式限制为整数,没有小数部分和指数(还请注意,DurationParserBuilder 可以在 const 上下文中编译时构建 DurationParser)。

use fundu::TimeUnit::*;
use fundu::{Duration, DurationParser, ParseError};

const PARSER: DurationParser = DurationParser::builder()
    .time_units(&[NanoSecond])
    .allow_time_unit_delimiter()
    .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())
);

还可以使用 parse_multiple 一次解析多个持续时间。不同的持续时间可以用空白和可选的连接词(此处为 and)分隔。如果没有遇到分隔符,数字或符号字符也可以表示新的持续时间。

use fundu::{Duration, DurationParser};

let parser = DurationParser::builder()
    .default_time_units()
    .parse_multiple(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))
);

有关常见配方和与其他 crate 集成的信息,请参阅示例文件夹。使用以下命令运行示例:

cargo run --example $FILE_NAME_WITHOUT_FILETYPE_SUFFIX

例如,systemd 时间范围解析器示例

# 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

时间单位

Second 是默认的时间单位(如果没有指定其他单位,例如使用 DurationParser::default_unit),当输入字符串中没有遇到时间单位时应用。下表概述了构造函数方法和哪些时间单位可用。如果需要自定义一组时间单位,可以使用 DurationParser::with_time_units

时间单位 默认标识符 计算 默认时间单位
纳秒 ns 1e-9s
微秒 Ms 1e-6s
毫秒 ms 1e-3s
s SI定义
分钟 m 60s
小时 h 60m
d 24h
w 7d
M / 12
y 365.25d

请注意,MonthsYears 不包含在默认时间单位集中。当前实现使用对 MonthsYears 的近似计算(以秒为单位),如果它们包含在最终配置中,则使用基于 儒略年 的计算。(见上表)

使用 CustomDurationParser 特性中的 CustomDurationParser,可以完全自定义时间单位的标识符。

自定义

与其他crate不同,fundu 不试图建立时间单位及其标识符或特定数字格式的标准。许多这些方面可以在初始化或构建解析器时进行调整。以下是不完整的示例,说明可能的数字格式自定义

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_time_unit_delimiter()
    // 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())
);

以下是一个使用 CustomDurationParser 特性中的 CustomDurationParser 的完全可定制时间单位的示例

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)
);

基准测试

要在您的机器上运行基准测试,请克隆存储库

git clone https://github.com/fundu-rs/fundu.git
cd fundu

然后运行所有基准测试

cargo bench --all-features

iai-callgrind(特性 = with-iai)和 flamegraph(特性 = with-flamegraph)基准测试只能在 Unix 上运行。使用 cargo 的 --features 选项运行特定特性的基准测试

cargo bench --features standard,custom

上面的命令不会运行 flamegraphiai-callgrind 基准测试。

可以使用以下方式进一步筛选基准测试,例如

cargo bench --bench benchmarks_standard
cargo bench --bench benchmarks_standard -- 'parsing speed'
cargo bench --features custom --no-default-features --bench benchmarks_custom

有关更多信息,请使用帮助

cargo bench --help # The cargo help for bench
cargo bench --bench benchmarks_standard -- --help # The criterion help

为了大致了解解析时间,以下是某些输入的平均解析速度(四核 3000MHz,8GB DDR3,Linux)

输入 平均解析时间
1 38.705ns
123456789.123456789 57.974ns
格式!("{0}.{0}e-1022", "1".重复(1022)) 421.56ns
1s 55.755ns
1ns 59.842ns
1y 57.760ns

贡献

贡献总是受欢迎!可以从现有的问题开始,或者打开一个新问题,以便我们可以讨论一切,这样就不会浪费任何努力。不要犹豫,提出问题!

使用 fundu 的项目

许可证

MIT 许可证(LICENSEhttp://opensource.org/licenses/MIT

依赖关系

~0–610KB
~11K SLoC