#parser #lexical

无需 std lexical-parse-float

高效解析字符串中的浮点数

4 个版本

0.8.5 2022 年 5 月 18 日
0.8.3 2022 年 3 月 10 日
0.8.2 2021 年 10 月 4 日
0.8.0 2021 年 9 月 3 日

#83解析器实现

Download history 286975/week @ 2024-03-17 260221/week @ 2024-03-24 252579/week @ 2024-03-31 265873/week @ 2024-04-07 285342/week @ 2024-04-14 301154/week @ 2024-04-21 262700/week @ 2024-04-28 259477/week @ 2024-05-05 289208/week @ 2024-05-12 274009/week @ 2024-05-19 304844/week @ 2024-05-26 390285/week @ 2024-06-02 383143/week @ 2024-06-09 373314/week @ 2024-06-16 393214/week @ 2024-06-23 187054/week @ 2024-06-30

1,356,753 每月下载量
627 crate(直接使用 4 个)中使用

MIT/Apache

1MB
19K SLoC

lexical

no_std 环境中使用的、高性能的数值转换例程。此例程不依赖于任何标准库功能,也不依赖于系统分配器。

类似项目

如果您需要 lexical 的浮点数解析算法的精简、稳定和编译时友好的版本,请参阅 minimal-lexical。如果您需要一个精简、高效的浮点数解析器,Rust 标准库的最近版本应该足够使用了。

目录

入门

将 lexical 添加到您的 Cargo.toml

[dependencies]
lexical = "^6.0"

并开始使用 lexical

// Number to string
use lexical_core::BUFFER_SIZE;
let mut buffer = [b'0'; BUFFER_SIZE];
lexical_core::write(3.0, &mut buffer);   // "3.0", always has a fraction suffix,
lexical_core::write(3, &mut buffer);     // "3"

// String to number.
let i: i32 = lexical_core::parse("3")?;      // Ok(3), auto-type deduction.
let f: f32 = lexical_core::parse("3.5")?;    // Ok(3.5)
let d: f64 = lexical_core::parse("3.5")?;    // Ok(3.5), error checking parse.
let d: f64 = lexical_core::parse("3a")?;     // Err(Error(_)), failed to parse.

为了在泛型代码中使用 lexical,提供了 FromLexical(用于 parse)和 ToLexical(用于 to_string)的特制界限。

/// Multiply a value in a string by multiplier, and serialize to string.
fn mul_2<T>(value: &str, multiplier: T)
    -> Result<String, lexical_core::Error>
where 
    T: lexical_core::ToLexical + lexical_core::FromLexical,
{
    let value: T = lexical_core::parse(value.as_bytes())?;
    let mut buffer = [b'0'; lexical_core::BUFFER_SIZE];
    let bytes = lexical_core::write(value * multiplier, &mut buffer);
    Ok(std::str::from_utf8(bytes).unwrap())
}

部分/完整解析器

Lexical 具有部分和完整解析器:完整解析器确保在解析时使用整个缓冲区,而不会忽略尾部字符,部分解析器尽可能多地解析字符,并返回解析值和解析数字的数量。在遇到错误时,lexical 将返回一个错误,指示错误类型和错误发生在缓冲区内的索引。

完整解析器

// This will return Err(Error::InvalidDigit(3)), indicating
// the first invalid character occurred at the index 3 in the input
// string (the space character).
let x: i32 = lexical_core::parse(b"123 456")?;

部分解析器

// This will return Ok((123, 3)), indicating that 3 digits were successfully
// parsed, and that the returned value is `123`.
let (x, count): (i32, usize) = lexical_core::parse_partial(b"123 456")?;

no_std

lexical-core 不依赖于标准库,也不依赖于系统分配器。要在 no_std 环境中使用 lexical-core,请将以下内容添加到 Cargo.toml

[dependencies.lexical-core]
version = "0.8.5"
default-features = false
# Can select only desired parsing/writing features.
features = ["write-integers", "write-floats", "parse-integers", "parse-floats"]

并开始使用 lexical

// A constant for the maximum number of bytes a formatter will write.
use lexical_core::BUFFER_SIZE;
let mut buffer = [b'0'; BUFFER_SIZE];

// Number to string. The underlying buffer must be a slice of bytes.
let count = lexical_core::write(3.0, &mut buffer);
assert_eq!(buffer[..count], b"3.0");
let count = lexical_core::write(3i32, &mut buffer);
assert_eq!(buffer[..count], b"3");

// String to number. The input must be a slice of bytes.
let i: i32 = lexical_core::parse(b"3")?;      // Ok(3), auto-type deduction.
let f: f32 = lexical_core::parse(b"3.5")?;    // Ok(3.5)
let d: f64 = lexical_core::parse(b"3.5")?;    // Ok(3.5), error checking parse.
let d: f64 = lexical_core::parse(b"3a")?;     // Err(Error(_)), failed to parse.

功能

Lexical 为每个数值转换例程提供功能门,如果使用某些数值转换,则编译时间更快。这些功能可以针对 lexical-core(不要求系统分配器)和 lexical 启用/禁用。默认情况下,所有转换都启用。

  • parse-floats:启用字符串到浮点数的转换。
  • parse-integers:启用字符串到整数的转换。
  • write-floats:启用浮点数到字符串的转换。
  • write-integers:启用整数到字符串的转换。

Lexical 可以高度定制,并且包含许多其他可选功能。

  • std: 启用使用 Rust 标准库(默认启用)。
  • power-of-two: 启用非十进制字符串的转换。
    启用 power_of_two 后,以下进制数有效:{2, 4, 8, 10, 16,32},否则只有 10 是有效的。这允许常见的十进制整数/浮点数的转换,无需为其他进制创建大型的预计算表。
  • radix: 允许非十进制字符串的转换。
    启用 radix 后,2 到 36(包括两端)的任何进制都有效,否则只有 10 是有效的。
  • format: 定制数字解析和写入可接受的数量格式。
    启用 format 后,数字格式通过位标志和掩码打包到 u128 中来指定。这些定义了解析和写入数字的有效语法,包括启用数字分隔符、要求整数或小数位数,以及切换大小写敏感的指数字符。
  • compact: 以性能为代价优化二进制大小。
    这最小化了预计算表的使用,产生了显著更小的二进制文件。
  • safe: 要求所有数组索引都进行边界检查。
    对于数字解析器来说,这实际上是一个无操作,因为它们在索引无边界检查可以明显证明是正确的地方使用安全索引。数字写入器经常使用不安全的索引,因为我们很容易高估输出中的位数,因为输入长度是固定的。
  • f16: 添加对 16 位浮点数的转换支持。
    添加 f16,半精度 IEEE-754 浮点类型,以及 bf16,Brain Float 16 类型,并提供这些浮点数的数字转换。请注意,由于这些是存储格式,因此没有本地算术运算,所有转换都使用中间的 f32

为确保禁用边界检查时的安全性,我们对所有数字转换例程进行了广泛的模糊测试。有关更多信息,请参阅下面的 安全性 部分。

Lexical 还高度重视代码膨胀:通过既优化性能又优化大小的算法。默认情况下,这侧重于性能,但是,通过使用 compact 功能,您还可以以性能为代价选择减少代码大小。紧凑算法以性能为代价最小化了预计算表和其他优化的使用。

自定义

警告:如果更改写入的有效数字位数、禁用指数表示法或更改指数表示法阈值,BUFFER_SIZE 可能不足以容纳结果输出。使用 WriteOptions::buffer_size 将提供写入字节数的正确上限。如果提供不足的长度缓冲区,lexical-core 将引发恐慌。

每种语言都有针对有效数值输入的竞争性规范,这意味着 Rust 的数字解析器可能对不同编程或数据语言的输入进行错误地接受或拒绝。

// Valid in Rust strings.
// Not valid in JSON.
let f: f64 = lexical_core::parse(b"3.e7")?;  // 3e7

// Let's only accept JSON floats.
const JSON: u128 = lexical_core::format::JSON;
let options = ParseFloatOptions::new();
let f: f64 = lexical_core::parse_with_options::<JSON>(b"3.0e7", &options)?; // 3e7
let f: f64 = lexical_core::parse_with_options::<JSON>(b"3.e7", &options)?;  // Errors!

由于不同编程和数据语言中数字语法的极高可变性,我们提供了 2 个不同的 API 来简化不同语法要求下的数字转换。

  • 数字格式 API(通过 formatpower-of-two 功能启用)。
    这是一个打包的结构,包含标志以指定编译时语法规则,用于数字解析或写入。这包括如下功能:数字字符串的基数、数字分隔符、大小写敏感的指数字符、可选的基数前缀/后缀等。
  • 选项API。
    这包含了解析和写入数字的运行时规则。这包括指数断点、舍入模式、指数和十进制点的字符,以及NaN和Infinity的字符串表示。

以下示例中记录了功能的一个有限子集,但完整的规范可以在API参考文档中找到。

数字格式 API

数字格式类提供了许多标志,用于指定解析或写入时的数字语法。当启用power-of-two功能时,还会添加额外的标志

  • 有效数字的基数(默认值为10)。
  • 指数基数的基数(默认值为10)。
  • 指数数字的基数(默认值为10)。

当启用format功能时,还会启用许多其他语法和数字分隔符标志,包括

  • 一个数字分隔符字符,用于分组数字以提高可读性。
  • 是否允许前导、尾部、内部和连续的数字分隔符。
  • 切换所需的浮点组件,如小数点前的数字。
  • 切换是否允许特殊浮点数或它们是否大小写敏感。

因此存在许多预定义的常量,以简化常见的用例,包括

  • JSON、XML、TOML、YAML、SQLite等。
  • Rust、Python、C#、FORTRAN、COBOL字面量和字符串等。

以下是一个构建自定义数字格式的示例

const FORMAT: u128 = lexical_core::NumberFormatBuilder::new()
    // Disable exponent notation.
    .no_exponent_notation(true)
    // Disable all special numbers, such as Nan and Inf.
    .no_special(true)
    .build();

// Due to use in a `const fn`, we can't panic or expect users to unwrap invalid
// formats, so it's up to the caller to verify the format. If an invalid format
// is provided to a parser or writer, the function will error or panic, respectively.
debug_assert!(lexical_core::format_is_valid::<FORMAT>());

选项 API

选项API允许在运行时自定义数字解析和写入,例如指定最大有效数字位数、指数字符等。

以下是一个构建自定义选项结构的示例

use std::num;

let options = lexical_core::WriteFloatOptions::builder()
    // Only write up to 5 significant digits, IE, `1.23456` becomes `1.2345`.
    .max_significant_digits(num::NonZeroUsize::new(5))
    // Never write less than 5 significant digits, `1.1` becomes `1.1000`.
    .min_significant_digits(num::NonZeroUsize::new(5))
    // Trim the trailing `.0` from integral float strings.
    .trim_floats(true)
    // Use a European-style decimal point.
    .decimal_point(b',')
    // Panic if we try to write NaN as a string.
    .nan_string(None)
    // Write infinity as "Infinity".
    .inf_string(Some(b"Infinity"))
    .build()
    .unwrap();

文档

Lexical的API参考可以在docs.rs上找到,同样lexical-core的。算法的详细描述可以在此找到

此外,还记录了Lexical如何处理数字分隔符以及实现大整数算术

验证

浮点数解析

浮点数解析很难做正确,从libstdc++的strtodPython的实现中发现了重大错误。为了验证lexical的准确性,我们采用了以下外部测试

  1. Hrvoje Abraham的strtod测试用例。
  2. Rust的test-float-parse单元测试。
  3. Testbase的将十进制转换为二进制的压力测试
  4. Nigel Tao从Freetype、Google的双转换库、IBM的IEEE-754R合规性测试以及其他许多精心挑选的示例中提取的测试
  5. 在博客上报道的各种 困难 情况

尽管词法分析器可能包含导致舍入误差的错误,但它已经经过了一套全面的随机数据和近似中间表示的测试,对于大多数使用场景应该是快速且正确的。

指标

这里展示了各种基准、二进制大小和编译时间。

构建时间

启用所有数值转换时的编译时间。对于更详细的分解,请参阅构建时间

Build Timings

二进制大小

在优化级别"2"编译的剥离二进制文件的大小。对于更详细的分解,请参阅二进制大小

Parse Stripped - Optimization Level "2" Write Stripped - Optimization Level "2"

基准测试 -- 解析整数

针对在整个范围内均匀分布的随机生成的整数的基准测试。对于更详细的分解,请参阅基准测试

Uniform Random Data

基准测试 -- 解析浮点数

针对从各种现实世界数据集中解析浮点数的基准测试。对于更详细的分解,请参阅基准测试

Real Data

基准测试 -- 写入整数

针对在整个范围内均匀分布的随机整数的写入基准测试。对于更详细的分解,请参阅基准测试

Uniform Random Data

基准测试 -- 写入浮点数

针对通过随机数生成器生成的浮点数和从JSON文档中解析的浮点数的写入基准测试。对于更详细的分解,请参阅基准测试

Random Data

安全性

由于整数和浮点数写入器中使用了内存不安全代码,我们对浮点数写入器和解析器进行了广泛的模糊测试。模糊测试工具可以在模糊测试下找到,并且是持续运行的。到目前为止,我们已经解析和写入超过720亿的浮点数。

由于整数写入器的简单逻辑和整数解析器中缺乏内存安全性,我们对两者进行了最小程度的模糊测试,并使用边缘情况进行了测试,迄今为止尚未发现内存安全问题。

平台支持

lexical-core已在各种平台上进行了测试,包括大端和小端系统,以确保代码的可移植性。支持的架构包括

  • x86_64 Linux、Windows、macOS、Android、iOS、FreeBSD和NetBSD。
  • x86 Linux、macOS、Android、iOS和FreeBSD。
  • aarch64 (ARM8v8-A) Linux、Android和iOS。
  • armv7 (ARMv7-A) Linux、Android和iOS。
  • arm (ARMv6) Linux和Android。
  • mips (MIPS) Linux。
  • mipsel (MIPS LE) Linux。
  • mips64 (MIPS64 BE) Linux。
  • mips64el (MIPS64 LE) Linux。
  • powerpc (PowerPC) Linux。
  • powerpc64 (PPC64) Linux。
  • powerpc64le (PPC64LE) Linux。
  • s390x (IBM Z) Linux。

lexical-core也应能在广泛的其它架构和指令集架构(ISA)上运行。如果您在某个架构上编译lexical-core遇到任何问题,请提交错误报告。

版本和版本支持

版本支持

当前支持的版本包括

  • v0.8.x
  • v0.7.x(维护状态)
  • v0.6.x(维护状态)

Rustc 兼容性

  • v0.8.x支持1.51+,包括稳定版、测试版和nightly版本。
  • v0.7.x支持1.37+,包括稳定版、测试版和nightly版本。
  • v0.6.x支持Rustc 1.24+,包括稳定版、测试版和nightly版本。

请报告在兼容的Rustc版本上编译支持的lexical-core版本时出现的任何错误。

版本控制

lexical使用语义版本控制。移除对最新稳定Debian或Ubuntu版本中Rustc版本的支持被视为不兼容的API更改,需要增加主要版本号。

变更日志

所有更改均在变更日志中记录。

许可证

Lexical采用Apache 2.0许可证以及MIT许可证双授权。有关完整的许可详情,请参阅LICENSE.md文件。

贡献

除非您明确表示,否则您根据Apache-2.0许可证提交的任何有意贡献,均将按照上述方式双授权,没有任何附加条款或条件。向仓库贡献意味着遵守行为准则

有关如何向lexical贡献的流程,请参阅开发快速入门指南

依赖项