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