18 个版本
0.2.18 | 2023年12月8日 |
---|---|
0.2.17 | 2022年12月21日 |
0.1.9 | 2022年12月10日 |
#171 in Rust 模式
145KB
2.5K SLoC
aoc-parse
为 Advent of Code 设计的解析库。
此库主要提供一个宏 parser!
,允许您在几秒钟内为您的 AoC 谜题输入编写自定义解析器。
例如,我 2015 年 12 月 2 日的谜题输入看起来像这样
4x23x21
22x29x19
11x4x11
8x10x5
24x18x16
...
此格式的解析器是一行代码:parser!(lines(u64 "x" u64 "x" u64))
.
如何使用 aoc-parse
非常简单。
use aoc_parse::{parser, prelude::*};
let p = parser!(lines(u64 "x" u64 "x" u64));
assert_eq!(
p.parse("4x23x21\n22x29x19\n").unwrap(),
vec![(4, 23, 21), (22, 29, 19)]
);
如果您使用 aoc-runner,它可能看起来像这样
use aoc_runner_derive::*;
use aoc_parse::{parser, prelude::*};
#[aoc_generator(day2)]
fn parse_input(text: &str) -> Vec<(u64, u64, u64)> {
let p = parser!(lines(u64 "x" u64 "x" u64));
p.parse(text).unwrap()
}
模式
传递给 parser!
宏的参数是一个 模式;aoc-parse 所做的只是 匹配 字符串与您选择的模式,并将它们转换为 Rust 值。
以下是一些模式的示例
lines(i32) // matches a list of integers, one per line
// converts them to a Vec<i32>
line(lower+) // matches a single line of one or more lowercase letters
// converts them to a Vec<char>
lines({ // matches lines made up of the characters < = >
"<" => -1, // converts them to a Vec<Vec<i32>> filled with -1, 0, and 1
"=" => 0,
">" => 1
}+)
以下是您可以在模式中使用的部分
基本模式
i8
、i16
、i32
、i64
、i128
、isize
、big_int
- 这些匹配以十进制数字写出的整数,可选地在开头带有 +
或 -
符号,例如 0
或 -11474
。
如果字符串包含一个太大而无法适应您选择的类型的数字,则会发生错误。例如,parser!(i8).parse("1000")
是一个错误。(它匹配字符串,但在“转换”阶段失败。)
big_int
解析一个 num_bigint::BigInt
。
u8
、u16
、u32
、u64
、u128
、usize
、big_uint
- 与上述相同,但没有符号。
i8_bin
、i16_bin
、i32_bin
、i64_bin
、i128_bin
、isize_bin
、big_int_bin
、u8_bin
、u16_bin
、u32_bin
、u64_bin
、u128_bin
、usize_bin
、big_uint_bin
、i8_hex
、i16_hex
、i32_hex
、i64_hex
、i128_hex
、isize_hex
、big_int_hex
、u8_hex
、u16_hex
、u32_hex
、u64_hex
、u128_hex
、usize_hex
、big_uint_hex
- 匹配二进制或十六进制表示的整数。带有 _hex
的解析器允许大写和小写数字 A
-F
。
f32
、f64
- 这些匹配使用十进制数字表示的浮点数,格式如下 (此格式)。( Advent of Code 的任何谜题都不依赖于浮点数,但做好准备并无害处。)
bool
- 匹配 true
或 false
并将其转换为相应的 bool
值。
'x'
或 "hello"
- 一个用引号括起来的 Rust 字符或字符串是只匹配该确切文本的模式。
精确模式不产生值。
pattern1 pattern2 pattern3...
- 模式可以连接起来形成更大的模式。这是 parser!(u64 "x" u64 "x" u64)
匹配字符串 4x23x21
的方法。它只是按顺序匹配每个子模式。如果有两个或更多产生值的子模式,它将匹配转换为元组。
parser_var
- 您可以使用之前定义并存储在局部变量中的解析器。
例如,下面的 amount
解析器使用了上一行定义的 fraction
解析器。
let fraction = parser!(i64 "/" u64);
let amount = parser!(fraction " tsp");
assert_eq!(amount.parse("1/4 tsp").unwrap(), (1, 4));
标识符还可以引用字符串或字符常量。
重复模式
pattern*
- 后跟星号的任何模式可以匹配零次或多次该模式。它将结果转换为Rust的 Vec
。例如,parser!("A"*)
匹配字符串 A
、AA
、AAAAAAAAAAAAAA
等,以及空字符串。
pattern+
- 匹配一次或多次模式,生成一个 Vec
。 parser!("A"+)
匹配 A
、AA
等,但不匹配空字符串。
pattern?
- 可选模式,生成Rust的 Option
。例如,parser!("x=" i32?)
匹配 x=123
,生成 Some(123)
;它也匹配 x=
,生成值 None
。
这些行为与正则表达式中的特殊字符 *
、+
和 ?
相同。
repeat_sep(pattern, separator)
- 匹配给定的 pattern 任意次,由 separator 分隔。它仅将匹配 pattern 的部分转换为Rust值,生成一个 Vec
。由 separator 匹配的字符串部分不会转换。
匹配单个字符
alpha
、alnum
、upper
、lower
- 匹配各种类别的单个字符。(尽管Advent of Code历史上坚持使用ASCII,但这些使用Unicode类别。)
digit
、digit_bin
、digit_hex
- 分别匹配十进制、二进制和十六进制的单个ASCII数字字符。数字被转换为它的数值,作为一个 usize
。
any_char
- 匹配下一个字符,无论是什么(如正则表达式中的 .
,但 any_char
也匹配换行符)。
char_of(str)
- 如果下一个字符在 str 中,则匹配该字符。例如,char_of(">^<v")
匹配恰好一个字符,即 >
、^
、<
或 v
。返回字符在选项列表中的索引(在这种情况下,0
、1
、2
或 3
)。
匹配多个字符
string(pattern)
- 匹配给定的 pattern,但不是将其转换为某个值,而是简单地返回匹配的字符作为 String
。
默认情况下,alpha+
返回一个 Vec<char>
,这在 AoC 中有时很有用,但通常最好返回一个 String
。
自定义转换
... name1:pattern1 ... => expr
- 在成功匹配到 =>
左侧的模式后,计算 Rust 表达式 expr 将结果转换为单个 Rust 值。
使用此功能将输入转换为结构体。例如,假设你的谜题输入包含每个精灵的名字和身高
Holly=33
Ivy=7
DouglasFir=1093
并且你希望将其转换为 struct Elf
值的向量。你需要编写的代码是
struct Elf {
name: String,
height: u32,
}
let p = parser!(lines(
elf:string(alpha+) '=' ht:u32 => Elf { name: elf, height: ht }
));
名字 elf
适用于模式 string(alpha+)
,名字 ht
适用于模式 i32
。=>
后面是普通的 Rust 代码。
名字 仅在相同匹配括号或花括号集中的 expr 中有效。
备选方案
{pattern1, pattern2, ...}
- 匹配任何一种 模式。首先尝试匹配 pattern1;如果匹配,停止。如果不匹配,则尝试 pattern2,依此类推。所有模式必须产生相同类型的 Rust 值。
这类似于 Rust 的 match
表达式。
例如,parser!({"<" => -1, ">" => 1})
要么匹配 <
,返回值 -1
,要么匹配 >
,返回 1
。
备选方案在你想将输入转换为枚举时很有用。例如,我的 2015 年 12 月 23 日的谜题输入是一系列看起来像这样的指令列表
jie a, +4
tpl a
inc a
jmp +2
hlf a
jmp -7
这可以轻松解析为美观的枚举向量,如下所示
enum Reg {
A,
B,
}
enum Insn {
Hlf(Reg),
Tpl(Reg),
Inc(Reg),
Jmp(isize),
Jie(Reg, isize),
Jio(Reg, isize),
}
use Reg::*;
use Insn::*;
let reg = parser!({"a" => A, "b" => B});
let p = parser!(lines({
"hlf " r:reg => Hlf(r),
"tpl " r:reg => Tpl(r),
"inc " r:reg => Inc(r),
"jmp " offset:isize => Jmp(offset),
"jie " r:reg ", " offset:isize => Jie(r, offset),
"jio " r:reg ", " offset:isize => Jio(r, offset),
}));
规则集
rule name1: type1 = pattern1;
- 引入一个“规则”,一个有名称的子解析器。
这支持解析带有嵌套括号或方括号的文本。
enum Formation {
Elf(char),
Stack(Vec<Formation>),
}
let p = parser!(
// First rule: A "formation" has return type Formation and is either
// a letter or a stack.
rule formation: Formation = {
s:alpha => Formation::Elf(s),
v:stack => Formation::Stack(v),
};
// Second rule: A "stack" is one or more formations, wrapped in
// matching parentheses.
rule stack: Vec<Formation> = '(' v:formation+ ')' => v;
// After all rules, the pattern that .parse() will actually match.
lines(formation+)
);
assert!(p.parse("px(fo(i)(RR(c)))j(Q)zww\n").is_ok());
assert!(p.parse("x(fo))\n").is_err()); // parens not balanced
通常 let
就足以用于其他解析器使用的解析器;但 rule
对于像上面的 formation
和 stack
那样需要自我引用或相互引用的解析器是必需的。Rust 的 let
不支持这一点。
注意:左递归语法通常不适用于 PEG 解析器。
行和部分
line(pattern)
- 匹配匹配 pattern 的单行文本和行尾的换行符。
这类似于正则表达式中的 ^pattern\n
,有两个小差异
-
line(pattern)
将仅匹配一行文本,即使 pattern 可以匹配更多换行符。 -
如果您的输入不以换行符结尾,
line(<var<pattern)
仍然可以匹配末尾的非换行符结束的"line"。
line(string(any_char+))
匹配一行文本,移除换行符,并返回剩余的部分作为一个String
。 line("")
匹配一个空白行。
lines(pattern)
- 匹配任何数量的与pattern匹配的文本行。等同于line(pattern)*
。
let p = parser!(lines(repeat_sep(digit, " ")));
assert_eq!(
p.parse("1 2 3\n4 5 6\n").unwrap(),
vec![vec![1, 2, 3], vec![4, 5, 6]],
);
section(pattern)
- 匹配零个或多个非空白行,随后是一个空白行或输入结束。非空白行必须与pattern匹配。例如,section(lines(u64))
匹配一个包含数字的列表部分,每行一个。
AoC谜题的输入通常包含几行数据,然后是一个空白行,然后是不同类型的数据。您可以使用section(p1) section(p2)
来解析它。
sections(pattern)
- 匹配任何数量的与pattern匹配的部分。等同于section(pattern)*
。
集合
hash_set(pattern)
,hash_map(pattern)
,btree_set(pattern)
,btree_map(pattern)
,vec_deque(pattern)
- 这些使用pattern匹配一些文本,然后将结果值放入HashSet
或其他集合中。
pattern必须生成一个可迭代类型。这些函数通过在pattern生成的任何内容上调用.into_iter()
来实现,然后使用.collect()
来生成新的集合。
pattern本身需要一个*
或+
,或者其他使其匹配多个值的东西。
let p = parser!(hash_set(digit+)); // <-- note the `+`
assert_eq!(p.parse("3127").unwrap(), HashSet::from([1, 2, 3, 7]));
一个映射是由一对序列构建的
let p = parser!(hash_map(
lines(string(alpha+) ": " any_char) // <-- this produces a vector of (String, char) pairs
));
assert_eq!(
p.parse("Midge: @\nToyler: #\nKnitley: &\n").unwrap(),
HashMap::from([
("Midge".to_string(), '@'),
("Toyler".to_string(), '#'),
("Knitley".to_string(), '&'),
]),
);
将所有这些结合起来解析一个复杂示例
let example = "\
Wiring Diagram #1:
a->q->E->z->J
D->f->D
Wiring Diagram #2:
g->r->f
g->B
";
let p = parser!(sections(
line("Wiring Diagram #" usize ":")
lines(repeat_sep(alpha, "->"))
));
assert_eq!(
p.parse(example).unwrap(),
vec![
(1, vec![vec!['a', 'q', 'E', 'z', 'J'], vec!['D', 'f', 'D']]),
(2, vec![vec!['g', 'r', 'f'], vec!['g', 'B']]),
],
);
许可证:MIT
依赖关系
~2.8–4.5MB
~81K SLoC