utils/input.rs
1//! Items relating to puzzle input.
2
3use std::borrow::Cow;
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7/// Enum for distinguishing between example and real inputs.
8///
9/// Some puzzles require this as different constants may be used for example inputs to simplify the
10/// problem. For example [2022 day 15](https://adventofcode.com/2022/day/15) part 1, which uses
11/// `y=10` in the example, but `y=2000000` for real inputs.
12///
13/// Most puzzle solutions should ignore this value.
14#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
15pub enum InputType {
16 Example,
17 Real,
18}
19
20/// Error type that shows the error's location in the input, returned by puzzle `new` functions.
21///
22/// # Examples
23///
24/// ```
25/// # use utils::input::InputError;
26/// let input = "12 34\n56 78\n90 abc";
27/// let error = InputError::new(input, 15, "expected number");
28/// assert_eq!(error.to_string(), "
29/// invalid input: expected number
30/// --> line 3 column 4
31/// |
32/// 3 | 90 abc
33/// | ^
34/// ".trim_start());
35/// ```
36#[must_use]
37#[derive(Debug)]
38pub struct InputError {
39 line_number: usize,
40 column_number: usize,
41 line: String,
42 source: Box<dyn Error>,
43}
44
45impl InputError {
46 /// Create a new [`InputError`].
47 ///
48 /// See [`ToIndex`] implementations for details on supported indexes.
49 #[cold]
50 pub fn new(input: &str, index: impl ToIndex, source: impl Into<Box<dyn Error>>) -> Self {
51 let index = index.input_index(input);
52 let (line_number, column_number, line) = Self::line_position(input, index);
53 let line = line.replace('\t', " ");
54
55 InputError {
56 line_number,
57 column_number,
58 line,
59 source: source.into(),
60 }
61 }
62
63 #[cold]
64 fn line_position(input: &str, index: usize) -> (usize, usize, String) {
65 let start = input[..index].rfind('\n').map_or(0, |p| p + 1);
66 let end = input[start..].find('\n').map_or(input.len(), |p| p + start);
67 let line = input[start..end].trim_end_matches('\r');
68
69 let line_number = input[..start].matches('\n').count() + 1;
70 let column_number = index - start + 1;
71
72 (line_number, column_number, line.to_string())
73 }
74
75 /// Returns the source error.
76 #[must_use]
77 pub fn into_source(self) -> Box<dyn Error> {
78 self.source
79 }
80}
81
82impl Display for InputError {
83 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
84 let pad = " ".repeat(self.line_number.to_string().len());
85
86 write!(
87 f,
88 "invalid input: {}\n --> line {} column {}\n{pad} |\n{} | {}\n{pad} |{}^\n",
89 self.source,
90 self.line_number,
91 self.column_number,
92 self.line_number,
93 self.line,
94 " ".repeat(self.column_number),
95 )
96 }
97}
98
99impl Error for InputError {
100 fn source(&self) -> Option<&(dyn Error + 'static)> {
101 Some(&*self.source)
102 }
103}
104
105/// Helper trait to simplify error location tracking.
106///
107/// Used in [`InputError::new`].
108pub trait ToIndex {
109 fn input_index(self, input: &str) -> usize;
110}
111
112impl ToIndex for &str {
113 /// Find index of this substring in the provided input.
114 ///
115 /// Uses the pointer offset, meaning it works if this substring is not the first occurrence in
116 /// the string. This allows recovering the error position without tracking an offset into the
117 /// string, which is useful when using [`Iterator`]s such as [`str::lines`] on an input.
118 ///
119 /// # Panics
120 ///
121 /// This function panics if this string is not a substring inside the provided string.
122 ///
123 /// # Examples
124 ///
125 /// ```
126 /// # use utils::input::ToIndex;
127 /// let string = "abcabc";
128 /// assert_eq!(string[4..].input_index(string), 4);
129 /// ```
130 ///
131 /// ```should_panic
132 /// # use utils::input::ToIndex;
133 /// let string = "abcabc";
134 /// let mut other = String::new();
135 /// other.push('b');
136 /// other.push('c');
137 /// other.input_index(string);
138 /// ```
139 fn input_index(self, input: &str) -> usize {
140 self.as_bytes().input_index(input)
141 }
142}
143
144impl ToIndex for &[u8] {
145 /// Find index of this subslice in the provided input.
146 ///
147 /// For use with functions that iterate over a string's bytes.
148 /// See the [`&str`](#impl-ToIndex-for-%26str) implementation.
149 fn input_index(self, input: &str) -> usize {
150 match input.as_bytes().subslice_range(self) {
151 Some(range) => range.start,
152 None => panic!("invalid string index: {self:p} is not a substring of {input:p}"),
153 }
154 }
155}
156
157impl ToIndex for char {
158 /// Find the first instance of this character in the string.
159 ///
160 /// Intended for puzzles where the entire input should be a certain set of characters, so
161 /// if an invalid character is found, the instance in the error doesn't matter.
162 ///
163 /// # Panics
164 ///
165 /// This function panics if this character is not present in the string
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// # use utils::input::ToIndex;
171 /// let string = "abca bc";
172 /// assert_eq!(' '.input_index(string), 4);
173 /// ```
174 ///
175 /// ```should_panic
176 /// # use utils::input::ToIndex;
177 /// let string = "abcdef";
178 /// ' '.input_index(string);
179 /// ```
180 fn input_index(self, input: &str) -> usize {
181 input
182 .find(self)
183 .unwrap_or_else(|| panic!("invalid string index: char {self:?} not found in {input:?}"))
184 }
185}
186
187impl ToIndex for usize {
188 /// Index into the input string.
189 ///
190 /// # Panics
191 ///
192 /// This function panics if the index is out of range for the provided string.
193 ///
194 /// # Examples
195 ///
196 /// ```
197 /// # use utils::input::ToIndex;
198 /// let string = "abcdef";
199 /// assert_eq!(4.input_index(string), 4);
200 /// ```
201 ///
202 /// ```should_panic
203 /// # use utils::input::ToIndex;
204 /// let string = "abcdef";
205 /// 10.input_index(string);
206 /// ```
207 fn input_index(self, input: &str) -> usize {
208 assert!(
209 self <= input.len(),
210 "invalid string index: index {self} out of range"
211 );
212 self
213 }
214}
215
216/// Strips the final newline from a borrowed string.
217///
218/// Equivalent to `s.strip_suffix("\r\n").or_else(|| s.strip_suffix("\n")).unwrap_or(s)`.
219///
220/// # Examples
221/// ```
222/// # use utils::input::strip_final_newline;
223/// assert_eq!(
224/// strip_final_newline("abc\ndef\n"),
225/// "abc\ndef"
226/// );
227/// assert_eq!(
228/// strip_final_newline("12\r\n34\r\n\r\n"),
229/// "12\r\n34\r\n"
230/// );
231/// ```
232#[must_use]
233#[inline]
234pub const fn strip_final_newline(s: &str) -> &str {
235 match s.as_bytes() {
236 // Use split_at as string slicing isn't const
237 [.., b'\r', b'\n'] => s.split_at(s.len() - 2).0,
238 [.., b'\n'] => s.split_at(s.len() - 1).0,
239 _ => s,
240 }
241}
242
243/// Convert a string to both LF and CRLF if it contains a newline.
244///
245/// # Examples
246/// ```
247/// # use utils::input::to_lf_crlf;
248/// assert_eq!(
249/// to_lf_crlf("abc\ndef\nghi"),
250/// ("abc\ndef\nghi".into(), Some("abc\r\ndef\r\nghi".into()))
251/// );
252/// assert_eq!(
253/// to_lf_crlf("12\r\n34\r\n56\r\n78"),
254/// ("12\n34\n56\n78".into(), Some("12\r\n34\r\n56\r\n78".into()))
255/// );
256/// assert_eq!(
257/// to_lf_crlf("abc123"),
258/// ("abc123".into(), None),
259/// );
260/// ```
261#[must_use]
262pub fn to_lf_crlf(s: &str) -> (Cow<'_, str>, Option<Cow<'_, str>>) {
263 let (mut has_lf, mut has_crlf) = (false, false);
264 let mut prev = 0;
265 for b in s.bytes() {
266 has_lf |= b == b'\n' && prev != b'\r';
267 has_crlf |= b == b'\n' && prev == b'\r';
268 prev = b;
269 }
270 if !has_lf && !has_crlf {
271 return (Cow::Borrowed(s), None);
272 }
273
274 let lf = if has_crlf {
275 Cow::Owned(s.replace("\r\n", "\n"))
276 } else {
277 Cow::Borrowed(s)
278 };
279 let crlf = if has_lf {
280 Cow::Owned(lf.replace('\n', "\r\n"))
281 } else {
282 Cow::Borrowed(s)
283 };
284 (lf, Some(crlf))
285}