diff --git a/Cargo.toml b/Cargo.toml index cbeeee5..cf6e37d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0" -const_format = "0.2.31" itertools = "0.11" num-traits = "0.2" -once_cell = "1.18.0" -regex = "1.7" thiserror = "1.0" +nom = "7" diff --git a/src/common/mod.rs b/src/common/mod.rs index 3c42571..3a2379d 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -3,5 +3,6 @@ pub mod direction; pub mod file; pub mod helper; pub mod math; +pub mod parser; pub mod pos; pub mod turn; diff --git a/src/common/parser.rs b/src/common/parser.rs new file mode 100644 index 0000000..fa6b022 --- /dev/null +++ b/src/common/parser.rs @@ -0,0 +1,70 @@ +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{line_ending, space0, space1}, + combinator::{eof, opt, value}, + error::ParseError, + multi::many0, + sequence::{delimited, preceded, terminated, tuple}, + Err, IResult, Parser, +}; + +pub fn extract_result>( + mut parser: F, +) -> impl FnMut(I) -> Result> +where + F: FnMut(I) -> IResult, +{ + move |input: I| parser(input).map(|(_, value)| value) +} + +pub fn ignore>(mut parser: F) -> impl FnMut(I) -> Result> +where + F: FnMut(I) -> IResult, +{ + move |input: I| parser(input).map(|(i, _)| i) +} + +pub fn eol_terminated<'a, F, O, E: ParseError<&'a str>>( + line: F, +) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> +where + F: Parser<&'a str, O, E>, +{ + terminated(line, alt((line_ending, eof))) +} + +pub fn true_false<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, bool, E> { + alt((value(true, tag("true")), value(false, tag("false"))))(input) +} + +pub fn empty_lines<'a, E: ParseError<&'a str>>(input: &'a str) -> Result<&'a str, Err> { + ignore(tuple((many0(line_ending), opt(eof))))(input) +} + +pub fn trim_left1<'a, F, O, E: ParseError<&'a str>>( + inner: F, +) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> +where + F: Parser<&'a str, O, E>, +{ + preceded(space1, inner) +} + +pub fn trim0<'a, F, O, E: ParseError<&'a str>>( + inner: F, +) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> +where + F: Parser<&'a str, O, E>, +{ + delimited(space0, inner, space0) +} + +pub fn trim1<'a, F, O, E: ParseError<&'a str>>( + inner: F, +) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> +where + F: Parser<&'a str, O, E>, +{ + delimited(space1, inner, space1) +} diff --git a/src/days/day08/mod.rs b/src/days/day08/mod.rs index aa671ea..c031b2a 100644 --- a/src/days/day08/mod.rs +++ b/src/days/day08/mod.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::common::file::split_lines; use super::template::{DayTrait, ResultType}; @@ -14,13 +16,13 @@ impl DayTrait for Day { } fn part1(&self, lines: &str) -> anyhow::Result { - let forest = Forest::try_from(lines)?; + let forest: Forest = lines.parse()?; let result = forest.count_visible(); Ok(ResultType::Integer(result)) } fn part2(&self, lines: &str) -> anyhow::Result { - let forest = Forest::try_from(lines)?; + let forest: Forest = lines.parse()?; let result = forest.best_score(); Ok(ResultType::Integer(result)) } @@ -181,10 +183,10 @@ impl Forest { } } -impl TryFrom<&str> for Forest { - type Error = ForestError; +impl FromStr for Forest { + type Err = ForestError; - fn try_from(lines: &str) -> Result { + fn from_str(lines: &str) -> Result { split_lines(lines) .map(|line| { line.chars() @@ -206,7 +208,7 @@ mod test { fn test_parse() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; - let forest = Forest::try_from(lines.as_str())?; + let forest: Forest = lines.parse()?; assert_eq!(forest.width, 5); assert_eq!(forest.height, 5); diff --git a/src/days/day11/mod.rs b/src/days/day11/mod.rs index 058cb57..ddb6f62 100644 --- a/src/days/day11/mod.rs +++ b/src/days/day11/mod.rs @@ -1,9 +1,19 @@ -use crate::common::file::split_lines; +use crate::common::parser::{ + empty_lines, eol_terminated, extract_result, ignore, trim0, trim_left1, true_false, +}; use super::template::{DayTrait, ResultType}; -use once_cell::sync::Lazy; -use regex::Regex; -use std::{iter::zip, num::ParseIntError}; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{char, i64, u32}, + combinator::{map, value, verify}, + error::Error, + multi::{many1, separated_list1}, + sequence::{preceded, tuple}, + Err, IResult, Parser, +}; +use std::{iter::zip, str::FromStr}; use thiserror::Error; const DAY_NUMBER: usize = 11; @@ -16,50 +26,50 @@ impl DayTrait for Day { } fn part1(&self, lines: &str) -> anyhow::Result { - let troop = Troop::try_from(lines)?; + let troop: Troop = lines.parse()?; Ok(ResultType::Integer(troop.play(20))) } fn part2(&self, lines: &str) -> anyhow::Result { - let troop = Troop::try_from(lines)?; + let troop: Troop = lines.parse()?; Ok(ResultType::Integer(troop.play_again(10_000))) } } #[derive(Debug, Error)] enum MonkeyError { - #[error("Not an Integer")] - NotAnInteger(#[from] ParseIntError), - - #[error("Can't parse line: {0}")] - UnknownLine(String), - - #[error("Did not expect end of Input")] - PrematureEndOfInput, + #[error("Error while parsing: {0}")] + ParsingError(String), } -#[derive(Debug, PartialEq)] +impl From>> for MonkeyError { + fn from(value: Err>) -> Self { + MonkeyError::ParsingError(value.to_string()) + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] enum Operation { - Plus(i64), - Times(i64), Squared, + Times(i64), + Plus(i64), } impl Operation { - pub fn parse(line: &str) -> Operation { - match line.split_whitespace().collect::>()[..] { - ["*", "old"] => Operation::Squared, - ["*", value] => Operation::Times(value.parse().unwrap()), - ["+", value] => Operation::Plus(value.parse().unwrap()), - _ => unreachable!(), - } + fn parse(input: &str) -> IResult<&str, Self> { + let (input, _) = tag("new = old ")(input)?; + alt(( + value(Operation::Squared, tag("* old")), + preceded(char('*'), trim0(i64.map(|a| Operation::Times(a)))), + preceded(char('+'), trim0(i64.map(|a| Operation::Plus(a)))), + ))(input) } pub fn calc(&self, old: i64) -> i64 { match self { - Operation::Plus(value) => old + *value, - Operation::Times(value) => old * value, Operation::Squared => old.pow(2), + Operation::Times(value) => old * value, + Operation::Plus(value) => old + value, } } } @@ -74,90 +84,96 @@ struct Monkey { bad_monkey: usize, } -static MONKEY: Lazy = Lazy::new(|| Regex::new(r"Monkey (\d+)").unwrap()); -static STARTING: Lazy = - Lazy::new(|| Regex::new(r"Starting items: (\d+(?:, \d+)*)").unwrap()); -static OP: Lazy = - Lazy::new(|| Regex::new(r"Operation: new = old ([+*] \d+|\* old)").unwrap()); -static TEST: Lazy = Lazy::new(|| Regex::new(r"Test: divisible by (\d+)").unwrap()); -static NEXT: Lazy = - Lazy::new(|| Regex::new(r"If (?:true|false): throw to monkey (\d+)").unwrap()); - impl Monkey { - fn parse_line<'a>(re: &Regex, line: &'a str) -> Result<&'a str, MonkeyError> { - let caps = re - .captures(line) - .ok_or(MonkeyError::UnknownLine(line.to_owned()))?; - Ok(caps.get(1).unwrap().as_str()) + fn number_parse(input: &str) -> IResult<&str, usize> { + let input = ignore(tag("Monkey "))(input)?; + let (input, number) = trim0(u32.map(|val| val as usize))(input)?; + let input = ignore(char(':'))(input)?; + + Ok((input, number)) } - pub fn parse_one( - iterator: &mut dyn Iterator, - ) -> Result, MonkeyError> { - if let Some(line) = iterator.next() { - let number: usize = Monkey::parse_line(&MONKEY, line)?.parse()?; + fn starting_items_parse(input: &str) -> IResult<&str, Vec> { + let input = ignore(tag("Starting items:"))(input)?; + separated_list1(char(','), trim0(i64))(input) + } - let line = iterator.next().ok_or(MonkeyError::PrematureEndOfInput)?; - let start = Monkey::parse_line(&STARTING, line)?; - let items = start - .split(", ") - .map(|item| item.parse::()) - .collect::, _>>()?; + fn operation_parse(input: &str) -> IResult<&str, Operation> { + let input = ignore(tag("Operation:"))(input)?; + trim0(Operation::parse)(input) + } - let line = iterator.next().ok_or(MonkeyError::PrematureEndOfInput)?; - let operation = Operation::parse(Monkey::parse_line(&OP, line)?); + fn divisor_parse(input: &str) -> IResult<&str, i64> { + let input = ignore(tag("Test: divisible by"))(input)?; + trim0(i64)(input) + } - let line = iterator.next().ok_or(MonkeyError::PrematureEndOfInput)?; - let divisor: i64 = Monkey::parse_line(&TEST, line)?.parse()?; + fn target_parse(want_good: bool) -> impl FnMut(&str) -> IResult<&str, usize> { + move |input: &str| { + let input = ignore(tuple(( + tag("If"), + trim_left1(verify(true_false, |is_good| *is_good == want_good)), + tag(": throw to monkey"), + )))(input)?; + trim0(u32.map(|val| val as usize))(input) + } + } - let line = iterator.next().ok_or(MonkeyError::PrematureEndOfInput)?; - let good_monkey: usize = Monkey::parse_line(&NEXT, line)?.parse()?; + fn parse(input: &str) -> IResult<&str, Monkey> { + let input = empty_lines(input)?; + let (input, number) = eol_terminated(trim0(Monkey::number_parse))(input)?; + let (input, items) = eol_terminated(trim0(Monkey::starting_items_parse))(input)?; + let (input, operation) = eol_terminated(trim0(Monkey::operation_parse))(input)?; + let (input, divisor) = eol_terminated(trim0(Monkey::divisor_parse))(input)?; + let (input, good_monkey) = eol_terminated(trim0(Monkey::target_parse(true)))(input)?; + let (input, bad_monkey) = eol_terminated(trim0(Monkey::target_parse(false)))(input)?; - let line = iterator.next().ok_or(MonkeyError::PrematureEndOfInput)?; - let bad_monkey: usize = Monkey::parse_line(&NEXT, line)?.parse()?; - - iterator.next(); - - Ok(Some(Monkey { + Ok(( + input, + Monkey { number, items, operation, divisor, good_monkey, bad_monkey, - })) - } else { - Ok(None) - } + }, + )) } + #[inline] pub fn process(&self, value: i64) -> i64 { self.operation.calc(value) } + #[inline] pub fn check(&self, value: i64) -> bool { value % self.divisor == 0 } + + #[allow(dead_code)] + fn parse_one(lines: &str) -> Result { + Ok(extract_result(Monkey::parse)(lines)?) + } } struct Troop { monkeys: Vec, } -impl TryFrom<&str> for Troop { - type Error = MonkeyError; +impl FromStr for Troop { + type Err = MonkeyError; - fn try_from(value: &str) -> Result { - let mut iter = split_lines(value); - let mut monkeys = Vec::new(); - while let Some(monkey) = Monkey::parse_one(&mut iter)? { - monkeys.push(monkey); - } - Ok(Troop { monkeys }) + fn from_str(lines: &str) -> Result { + Ok(extract_result(Troop::parse)(lines)?) } } impl Troop { + fn parse(input: &str) -> IResult<&str, Troop> { + map(many1(Monkey::parse), |monkeys| Troop { monkeys })(input) + } + fn do_play(&self, rounds: usize, alter: F) -> i64 where F: Fn(i64) -> i64, @@ -204,7 +220,6 @@ mod test { fn test_parse() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; - let mut lines = split_lines(&lines); let expected = Monkey { number: 0, items: vec![79, 98], @@ -213,7 +228,7 @@ mod test { good_monkey: 2, bad_monkey: 3, }; - let result = Monkey::parse_one(&mut lines)?.unwrap(); + let result = Monkey::parse_one(&lines)?; assert_eq!(result, expected); Ok(()) @@ -224,7 +239,7 @@ mod test { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; let expected = 4; - let result = Troop::try_from(lines.as_str())?; + let result: Troop = lines.parse()?; assert_eq!(result.monkeys.len(), expected); Ok(()) diff --git a/src/days/day15/mod.rs b/src/days/day15/mod.rs index 8292730..81e69f8 100644 --- a/src/days/day15/mod.rs +++ b/src/days/day15/mod.rs @@ -1,37 +1,44 @@ use super::template::{DayTrait, ResultType}; -use crate::common::{file::split_lines, pos::Pos}; +use crate::common::{ + parser::{eol_terminated, extract_result, ignore, trim0}, + pos::Pos, +}; use itertools::Itertools; -use once_cell::sync::Lazy; -use regex::Regex; -use std::{collections::HashSet, num::ParseIntError}; +use nom::{ + bytes::complete::tag, + character::complete::{char, i64}, + error::Error, + multi::many0, + sequence::{separated_pair, tuple}, + Err, IResult, +}; +use std::collections::HashSet; use thiserror::Error; const DAY_NUMBER: usize = 15; pub struct Day; +fn parse_row(input: &str) -> IResult<&str, i64> { + eol_terminated(i64)(input) +} + impl DayTrait for Day { fn get_day_number(&self) -> usize { DAY_NUMBER } fn part1(&self, lines: &str) -> anyhow::Result { - let lines = split_lines(lines).collect_vec(); - if lines.len() < 2 { - Err(SensorError::ToFewLines)?; - } - let row = lines[0].parse()?; - let (sensors, beacons) = Day::parse_all(&lines[1..])?; + let (lines, row) = parse_row(lines).map_err(|_| SensorError::RowLineNotFound)?; + let (sensors, beacons) = Day::parse_all(lines)?; + let result = Day::count_coverage_at(&sensors, &beacons, row); Ok(ResultType::Integer(result)) } fn part2(&self, lines: &str) -> anyhow::Result { - let lines = split_lines(lines).collect_vec(); - if lines.len() < 2 { - Err(SensorError::ToFewLines)?; - } - let (sensors, _) = Day::parse_all(&lines[1..])?; + let (lines, _) = parse_row(lines).map_err(|_| SensorError::RowLineNotFound)?; + let (sensors, _) = Day::parse_all(lines)?; let mid_lines = Day::dividing_lines(&sensors); let points = mid_lines .iter() @@ -51,11 +58,12 @@ impl DayTrait for Day { } impl Day { - fn parse_all(lines: &[&str]) -> Result<(HashSet, HashSet>), SensorError> { + fn parse_all(lines: &str) -> Result<(HashSet, HashSet>), SensorError> { + let data = extract_result(many0(eol_terminated(Sensor::parse)))(lines)?; + let mut sensors = HashSet::new(); let mut beacons = HashSet::new(); - for line in lines { - let (sensor, beacon) = Sensor::parse(line)?; + for (sensor, beacon) in data { sensors.insert(sensor); beacons.insert(beacon); } @@ -91,17 +99,20 @@ impl Day { #[derive(Debug, Error)] enum SensorError { - #[error("Not an Integer")] - NotAnInt(#[from] ParseIntError), - - #[error("Unknown line: {0}")] - UnknownLine(String), + #[error("Error Parsing Input: {0}")] + ParsingError(String), #[error("Did not find exactly one point for the sensor: {0}")] NotExactlyOneResult(usize), - #[error("Too few lines in input.txt")] - ToFewLines, + #[error("Row line not found at start of Input")] + RowLineNotFound, +} + +impl From>> for SensorError { + fn from(error: Err>) -> Self { + SensorError::ParsingError(error.to_string()) + } } #[derive(Debug)] @@ -220,28 +231,30 @@ struct Sensor { radius: i64, } -static SENSOR: Lazy = - Lazy::new(|| Regex::new(r"x=(-?\d+), y=(-?\d+).*x=(-?\d+), y=(-?\d+)").unwrap()); - impl Sensor { - pub fn parse(line: &str) -> Result<(Sensor, Pos), SensorError> { - let caps = SENSOR - .captures(line) - .ok_or(SensorError::UnknownLine(line.to_owned()))?; - let sensor_x = caps.get(1).unwrap().as_str().parse()?; - let sensor_y = caps.get(2).unwrap().as_str().parse()?; - let beacon_x = caps.get(3).unwrap().as_str().parse()?; - let beacon_y = caps.get(4).unwrap().as_str().parse()?; - let sensor = Pos::new(sensor_x, sensor_y); - let beacon = Pos::new(beacon_x, beacon_y); - let radius = sensor.taxicab(&beacon); - Ok(( - Sensor { - pos: sensor, - radius, - }, - beacon, - )) + fn component<'a>(name: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, i64> { + move |input: &str| { + let input = ignore(tuple((tag(name), char('='))))(input)?; + trim0(i64)(input) + } + } + + fn parse_pos(input: &str) -> IResult<&str, Pos> { + let (input, (x, y)) = separated_pair( + Sensor::component("x"), + trim0(char(',')), + Sensor::component("y"), + )(input)?; + Ok((input, Pos::new(x, y))) + } + + pub fn parse(input: &str) -> IResult<&str, (Sensor, Pos)> { + let input = ignore(tag("Sensor at"))(input)?; + let (input, pos) = trim0(Sensor::parse_pos)(input)?; + let input = ignore(tag(": closest beacon is at"))(input)?; + let (input, beacon) = trim0(Sensor::parse_pos)(input)?; + let radius = pos.taxicab(&beacon); + Ok((input, (Sensor { pos, radius }, beacon))) } pub fn range_at(&self, y: i64) -> Option { @@ -279,7 +292,7 @@ mod test { }, Pos::new(-2, 15), ); - let result = Sensor::parse(input)?; + let result = extract_result(Sensor::parse)(input)?; assert_eq!(result, expected); Ok(()) diff --git a/src/days/day16/mod.rs b/src/days/day16/mod.rs index 67dd832..e068986 100644 --- a/src/days/day16/mod.rs +++ b/src/days/day16/mod.rs @@ -1,17 +1,20 @@ +use super::template::{DayTrait, ResultType}; +use crate::common::parser::{eol_terminated, extract_result, ignore, trim0, trim1, trim_left1}; +use itertools::Itertools; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{alpha1, i64}, + error::Error, + multi::{many1, separated_list1}, + Err, IResult, +}; +use std::collections::BinaryHeap; use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::hash::Hash; use std::iter::Sum; use std::ops::{Add, Deref, Mul, Sub}; -use std::{collections::BinaryHeap, num::ParseIntError}; - -use crate::common::file::split_lines; - -use super::template::{DayTrait, ResultType}; -use const_format::concatcp; -use itertools::Itertools; -use once_cell::sync::Lazy; -use regex::Regex; use thiserror::Error; const DAY_NUMBER: usize = 16; @@ -129,11 +132,8 @@ impl Deref for Index { #[derive(Debug, Error)] enum ValveError { - #[error("Not an Integer")] - NotAnInt(#[from] ParseIntError), - #[error("Not a valid description: {0}")] - CouldNotParse(String), + ParsingError(String), #[error("Not a valid valve: {0}")] ValveNotFound(String), @@ -142,17 +142,11 @@ enum ValveError { NoOptimumFound, } -const ID: &str = "[[:alpha:]]+"; -const COMMON: &str = concatcp!("^Valve (?", ID, r") has flow rate=(?\d+); "); -const PLURAL_STR: &str = concatcp!( - COMMON, - "tunnels lead to valves (?", - ID, - "(?:, ", - ID, - ")+)$" -); -const SINGULAR_STR: &str = concatcp!(COMMON, "tunnel leads to valve (?", ID, ")$"); +impl From>> for ValveError { + fn from(error: Err>) -> Self { + ValveError::ParsingError(error.to_string()) + } +} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct RawValve<'a> { @@ -170,29 +164,45 @@ impl<'a> RawValve<'a> { } } - fn from_regex(regex: &Regex, line: &'a str) -> Option, ValveError>> { - regex.captures(line).map(|caps| { - match (caps.name("id"), caps.name("rate"), caps.name("exits")) { - (Some(id), Some(rate), Some(exits)) => { - let rate = rate.as_str().parse()?; - let neighbors = exits.as_str().split(",").map(|s| s.trim_start()).collect(); - Ok(RawValve::create(id.as_str(), Flow(rate), neighbors)) - } - _ => Err(ValveError::CouldNotParse(line.to_string())), - } - }) + fn parse_start(input: &str) -> IResult<&str, (&str, Flow)> { + let input = ignore(tag("Valve"))(input)?; + let (input, valve) = trim1(alpha1)(input)?; + let input = ignore(tag("has flow rate="))(input)?; + let (input, flow) = trim0(i64)(input)?; + let input = ignore(tag(";"))(input)?; + Ok((input, (valve, Flow(flow)))) + } + + fn parse_singular(input: &str) -> IResult<&str, Vec<&str>> { + let input = ignore(tag("tunnel leads to valve "))(input)?; + let (input, valve) = alpha1(input)?; + + Ok((input, vec![valve])) + } + + fn parse_plural(input: &str) -> IResult<&str, Vec<&str>> { + let input = ignore(tag("tunnels lead to valves "))(input)?; + let (input, valve) = separated_list1(tag(","), trim0(alpha1))(input)?; + + Ok((input, valve)) + } + + fn parse(input: &str) -> IResult<&str, RawValve> { + let (input, (id, flow)) = RawValve::parse_start(input)?; + let (input, neighbors) = trim_left1(eol_terminated(alt(( + RawValve::parse_plural, + RawValve::parse_singular, + ))))(input)?; + + Ok((input, RawValve::create(id, flow, neighbors))) } } impl<'a> TryFrom<&'a str> for RawValve<'a> { type Error = ValveError; - fn try_from(value: &'a str) -> Result, Self::Error> { - static PLURAL: Lazy = Lazy::new(|| Regex::new(PLURAL_STR).unwrap()); - static SINGULAR: Lazy = Lazy::new(|| Regex::new(SINGULAR_STR).unwrap()); - RawValve::from_regex(&PLURAL, value) - .or_else(|| RawValve::from_regex(&SINGULAR, value)) - .unwrap_or_else(|| Err(ValveError::CouldNotParse(value.to_string()))) + fn try_from(value: &'a str) -> Result { + Ok(extract_result(RawValve::parse)(value)?) } } @@ -267,9 +277,7 @@ struct ValveSystem { impl ValveSystem { fn build(lines: &str, start: &str) -> Result { - let mut raw = split_lines(lines) - .map(|line| RawValve::try_from(line)) - .collect::, _>>()?; + let mut raw = extract_result(many1(RawValve::parse))(lines)?; raw.sort_unstable_by_key(|valve| valve.id); let start_idx = raw @@ -931,7 +939,7 @@ mod test { fn parse_plural() -> Result<()> { let line = "Valve BB has flow rate=13; tunnels lead to valves CC, AA"; let expected = RawValve::create("BB", Flow(13), vec!["CC", "AA"]); - let result = RawValve::try_from(line)?; + let result = line.try_into()?; assert_eq!(expected, result); Ok(()) @@ -941,7 +949,7 @@ mod test { fn parse_singular() -> Result<()> { let line = "Valve HH has flow rate=22; tunnel leads to valve GG"; let expected = RawValve::create("HH", Flow(22), vec!["GG"]); - let result = RawValve::try_from(line)?; + let result = line.try_into()?; assert_eq!(expected, result); Ok(())