#解析器 #解析 # Advent of Code #aoc #单行

aoc-parse

一个用于解析你的 Advent of Code 谜题输入的小型库

18 个版本

0.2.18 2023年12月8日
0.2.17 2022年12月21日
0.1.9 2022年12月10日

#171 in Rust 模式

MIT 许可证

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
}+)

以下是您可以在模式中使用的部分

基本模式

i8i16i32i64i128isizebig_int - 这些匹配以十进制数字写出的整数,可选地在开头带有 +- 符号,例如 0-11474

如果字符串包含一个太大而无法适应您选择的类型的数字,则会发生错误。例如,parser!(i8).parse("1000") 是一个错误。(它匹配字符串,但在“转换”阶段失败。)

big_int 解析一个 num_bigint::BigInt

u8u16u32u64u128usizebig_uint - 与上述相同,但没有符号。

i8_bini16_bini32_bini64_bini128_binisize_binbig_int_binu8_binu16_binu32_binu64_binu128_binusize_binbig_uint_bini8_hexi16_hexi32_hexi64_hexi128_hexisize_hexbig_int_hexu8_hexu16_hexu32_hexu64_hexu128_hexusize_hexbig_uint_hex - 匹配二进制或十六进制表示的整数。带有 _hex 的解析器允许大写和小写数字 A-F

f32f64 - 这些匹配使用十进制数字表示的浮点数,格式如下 (此格式)。( Advent of Code 的任何谜题都不依赖于浮点数,但做好准备并无害处。)

bool - 匹配 truefalse 并将其转换为相应的 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"*) 匹配字符串 AAAAAAAAAAAAAAAAA 等,以及空字符串。

pattern+ - 匹配一次或多次模式,生成一个 Vecparser!("A"+) 匹配 AAA 等,但不匹配空字符串。

pattern? - 可选模式,生成Rust的 Option。例如,parser!("x=" i32?) 匹配 x=123,生成 Some(123);它也匹配 x=,生成值 None

这些行为与正则表达式中的特殊字符 *+? 相同。

repeat_sep(pattern, separator) - 匹配给定的 pattern 任意次,由 separator 分隔。它仅将匹配 pattern 的部分转换为Rust值,生成一个 Vec。由 separator 匹配的字符串部分不会转换。

匹配单个字符

alphaalnumupperlower - 匹配各种类别的单个字符。(尽管Advent of Code历史上坚持使用ASCII,但这些使用Unicode类别。)

digitdigit_bindigit_hex - 分别匹配十进制、二进制和十六进制的单个ASCII数字字符。数字被转换为它的数值,作为一个 usize

any_char - 匹配下一个字符,无论是什么(如正则表达式中的 .,但 any_char 也匹配换行符)。

char_of(str) - 如果下一个字符在 str 中,则匹配该字符。例如,char_of(">^<v") 匹配恰好一个字符,即 >^<v。返回字符在选项列表中的索引(在这种情况下,0123)。

匹配多个字符

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 对于像上面的 formationstack 那样需要自我引用或相互引用的解析器是必需的。Rust 的 let 不支持这一点。

注意:左递归语法通常不适用于 PEG 解析器。

行和部分

line(pattern) - 匹配匹配 pattern 的单行文本和行尾的换行符。

这类似于正则表达式中的 ^pattern\n,有两个小差异

  • line(pattern) 将仅匹配一行文本,即使 pattern 可以匹配更多换行符。

  • 如果您的输入不以换行符结尾,line(<var<pattern)仍然可以匹配末尾的非换行符结束的"line"。

line(string(any_char+))匹配一行文本,移除换行符,并返回剩余的部分作为一个Stringline("")匹配一个空白行。

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