Skip to content

Commit 7e94e9c

Browse files
committed
Improve CRLF and trailing whitespace handling
Previously, each entry point trimmed the end of the input and converted CRLF to LF. Now each solution handles both LF and CRLF, and only the final newline is stripped.
1 parent 1718a98 commit 7e94e9c

File tree

17 files changed

+159
-40
lines changed

17 files changed

+159
-40
lines changed

crates/aoc/src/cli/arguments.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,7 @@ Options:
239239
let mut path = self.inputs_dir();
240240
path.push(format!("year{year:#}"));
241241
path.push(format!("day{day:#}.txt"));
242-
match fs::read_to_string(&path) {
243-
Ok(s) => Ok(s.trim_ascii_end().replace("\r\n", "\n")),
244-
Err(err) => Err((path.to_string_lossy().to_string(), err)),
245-
}
242+
fs::read_to_string(&path).map_err(|err| (path.to_string_lossy().to_string(), err))
246243
}
247244
}
248245

crates/aoc/src/cli/mode/stdin.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ pub fn main(args: &Arguments) -> Result<(), Box<dyn Error>> {
2929
.read_to_string(&mut input)
3030
.map_err(|err| format!("failed to read input: {err}"))?;
3131

32-
input = input.trim_ascii_end().replace("\r\n", "\n");
33-
3432
let (part1, part2) = f(&input).map_err(|err| format!("{year:#} {day:#}: {err}"))?;
3533
assert!(!part1.contains('\n'));
3634
assert!(!part2.contains('\n'));

crates/aoc/src/puzzles.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::all_puzzles;
22
use utils::date::{Day, Year};
3+
use utils::input::strip_final_newline;
34

45
// These imports are unused if none of the year features are enabled
56
#[allow(clippy::allow_attributes, unused_imports)]
@@ -27,6 +28,7 @@ macro_rules! matcher {
2728
/// Generated from [`all_puzzles!`].
2829
pub const PUZZLES: &[(Year, Day, PuzzleFn)] = &[$($(
2930
(crate::$year::$day::YEAR, crate::$year::$day::DAY, |input: &str| {
31+
let input = strip_final_newline(input);
3032
let solution = crate::$year::$day::new(input, InputType::Real)?;
3133
let part1 = solution.part1();
3234
let part2 = solution.part2();

crates/utils/src/framework.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,26 +137,49 @@ macro_rules! examples {
137137
#[test]
138138
fn new() {
139139
for (i, example) in $day::EXAMPLES.iter().enumerate() {
140-
let solution = $day::new(example.0, InputType::Example);
140+
let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
141+
142+
let solution = $day::new(&lf, InputType::Example);
141143
assert!(
142144
solution.is_ok(),
143145
"new failed for example {i}: {:?}",
144146
example.0,
145147
);
148+
149+
if let Some(crlf) = crlf {
150+
let solution = $day::new(&crlf, InputType::Example);
151+
assert!(
152+
solution.is_ok(),
153+
"new failed for example {i} with CRLF line endings: {:?}",
154+
example.0,
155+
);
156+
}
146157
}
147158
}
148159

149160
#[test]
150161
fn part1() {
151162
for (i, example) in $day::EXAMPLES.iter().enumerate() {
152163
if let Some(expected) = example.1 {
153-
let solution = $day::new(example.0, InputType::Example).unwrap();
164+
let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
165+
166+
let solution = $day::new(&lf, InputType::Example).unwrap();
154167
assert_eq!(
155168
solution.part1(),
156169
expected,
157170
"part 1 incorrect for example {i}: {:?}",
158171
example.0,
159172
);
173+
174+
if let Some(crlf) = crlf {
175+
let solution = $day::new(&crlf, InputType::Example).unwrap();
176+
assert_eq!(
177+
solution.part1(),
178+
expected,
179+
"part 1 incorrect for example {i} with CRLF line endings: {:?}",
180+
example.0,
181+
);
182+
}
160183
}
161184
}
162185
}
@@ -165,13 +188,25 @@ macro_rules! examples {
165188
fn part2() {
166189
for (i, example) in $day::EXAMPLES.iter().enumerate() {
167190
if let Some(expected) = example.2 {
168-
let solution = $day::new(example.0, InputType::Example).unwrap();
191+
let (lf, crlf) = $crate::input::to_lf_crlf(example.0);
192+
193+
let solution = $day::new(&lf, InputType::Example).unwrap();
169194
assert_eq!(
170195
solution.part2(),
171196
expected,
172197
"part 2 incorrect for example {i}: {:?}",
173198
example.0,
174199
);
200+
201+
if let Some(crlf) = crlf {
202+
let solution = $day::new(&crlf, InputType::Example).unwrap();
203+
assert_eq!(
204+
solution.part2(),
205+
expected,
206+
"part 2 incorrect for example {i} with CRLF line endings: {:?}",
207+
example.0,
208+
);
209+
}
175210
}
176211
}
177212
}
@@ -190,21 +225,21 @@ macro_rules! examples {
190225
};
191226
(@item {file: $file:literal, part1: $p1:expr, part2: $p2:expr $(,)?}) => {
192227
(
193-
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file)).trim_ascii_end(),
228+
$crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
194229
Some($p1),
195230
Some($p2),
196231
)
197232
};
198233
(@item {file: $file:literal, part1: $p1:expr $(,)?}) => {
199234
(
200-
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file)).trim_ascii_end(),
235+
$crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
201236
Some($p1),
202237
None,
203238
)
204239
};
205240
(@item {file: $file:literal, part2: $p2:expr $(,)?}) => {
206241
(
207-
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file)).trim_ascii_end(),
242+
$crate::input::strip_final_newline(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/", $file))),
208243
None,
209244
Some($p2),
210245
)

crates/utils/src/input.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Items relating to puzzle input.
22
3+
use std::borrow::Cow;
34
use std::error::Error;
45
use std::fmt::{Display, Formatter};
56

@@ -240,3 +241,74 @@ impl<T, E: Into<Box<dyn Error>>, I: ToIndex> MapWithInputExt for Result<T, (E, I
240241
self.map_err(|err| err.map_with_input(input))
241242
}
242243
}
244+
245+
/// Strips the final newline from a borrowed string.
246+
///
247+
/// Equivalent to `s.strip_suffix("\r\n").or_else(|| s.strip_suffix("\n")).unwrap_or(s)`.
248+
///
249+
/// # Examples
250+
/// ```
251+
/// # use utils::input::strip_final_newline;
252+
/// assert_eq!(
253+
/// strip_final_newline("abc\ndef\n"),
254+
/// "abc\ndef"
255+
/// );
256+
/// assert_eq!(
257+
/// strip_final_newline("12\r\n34\r\n\r\n"),
258+
/// "12\r\n34\r\n"
259+
/// );
260+
/// ```
261+
#[must_use]
262+
#[inline]
263+
pub const fn strip_final_newline(s: &str) -> &str {
264+
match s.as_bytes() {
265+
// Use split_at as string slicing isn't const
266+
[.., b'\r', b'\n'] => s.split_at(s.len() - 2).0,
267+
[.., b'\n'] => s.split_at(s.len() - 1).0,
268+
_ => s,
269+
}
270+
}
271+
272+
/// Convert a string to both LF and CRLF if it contains a newline.
273+
///
274+
/// # Examples
275+
/// ```
276+
/// # use utils::input::to_lf_crlf;
277+
/// assert_eq!(
278+
/// to_lf_crlf("abc\ndef\nghi"),
279+
/// ("abc\ndef\nghi".into(), Some("abc\r\ndef\r\nghi".into()))
280+
/// );
281+
/// assert_eq!(
282+
/// to_lf_crlf("12\r\n34\r\n56\r\n78"),
283+
/// ("12\n34\n56\n78".into(), Some("12\r\n34\r\n56\r\n78".into()))
284+
/// );
285+
/// assert_eq!(
286+
/// to_lf_crlf("abc123"),
287+
/// ("abc123".into(), None),
288+
/// );
289+
/// ```
290+
#[must_use]
291+
pub fn to_lf_crlf(s: &str) -> (Cow<'_, str>, Option<Cow<'_, str>>) {
292+
let (mut has_lf, mut has_crlf) = (false, false);
293+
let mut prev = 0;
294+
for b in s.bytes() {
295+
has_lf |= b == b'\n' && prev != b'\r';
296+
has_crlf |= b == b'\n' && prev == b'\r';
297+
prev = b;
298+
}
299+
if !has_lf && !has_crlf {
300+
return (Cow::Borrowed(s), None);
301+
}
302+
303+
let lf = if has_crlf {
304+
Cow::Owned(s.replace("\r\n", "\n"))
305+
} else {
306+
Cow::Borrowed(s)
307+
};
308+
let crlf = if has_lf {
309+
Cow::Owned(lf.replace('\n', "\r\n"))
310+
} else {
311+
Cow::Borrowed(s)
312+
};
313+
(lf, Some(crlf))
314+
}

crates/year2015/src/day19.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ const _: () = {
4444

4545
impl Day19 {
4646
pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
47-
let Some((rules_str, molecule)) = input.rsplit_once("\n\n") else {
47+
let Some((rules_str, molecule)) = input
48+
.rsplit_once("\n\n")
49+
.or_else(|| input.rsplit_once("\r\n\r\n"))
50+
else {
4851
return Err(InputError::new(
4952
input,
5053
0,

crates/year2015/src/day20.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ pub struct Day20 {
99

1010
impl Day20 {
1111
pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
12-
let threshold: u32 = input
13-
.trim_ascii_end()
14-
.parse()
15-
.map_err(|_| InputError::new(input, 0, "expected u32"))?;
12+
let threshold = parser::u32().parse_complete(input)?;
1613

1714
// (threshold/10) + 1 should always find the solutions, but attempt to find the solutions
1815
// with smaller & significantly faster upper bounds first

crates/year2015/src/day21.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ impl Day21 {
2424
pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
2525
let (boss_health, boss_damage, boss_armor) = parser::u32()
2626
.with_prefix("Hit Points: ")
27-
.then(parser::u32().with_prefix("\nDamage: "))
28-
.then(parser::u32().with_prefix("\nArmor: "))
27+
.then(parser::u32().with_prefix(parser::eol().then("Damage: ")))
28+
.then(parser::u32().with_prefix(parser::eol().then("Armor: ")))
2929
.parse_complete(input)?;
3030

3131
let mut min_gold_win = u32::MAX;

crates/year2015/src/day22.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ impl Day22 {
1111
pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
1212
let (boss_health, boss_damage) = parser::u32()
1313
.with_prefix("Hit Points: ")
14-
.then(parser::u32().with_prefix("\nDamage: "))
14+
.with_suffix(parser::eol())
15+
.then(parser::u32().with_prefix("Damage: "))
1516
.parse_complete(input)?;
1617

1718
Ok(Self {

crates/year2016/src/day02.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ impl<'a> Day02<'a> {
1010
pub fn new(input: &'a str, _: InputType) -> Result<Self, InputError> {
1111
if let Some(b) = input
1212
.bytes()
13-
.find(|b| !matches!(b, b'U' | b'D' | b'L' | b'R' | b'\n'))
13+
.find(|b| !matches!(b, b'U' | b'D' | b'L' | b'R' | b'\r' | b'\n'))
1414
{
1515
return Err(InputError::new(
1616
input,

0 commit comments

Comments
 (0)