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#[derive(Debug)]
37pub struct InputError {
38    line_number: usize,
39    column_number: usize,
40    line: String,
41    source: Box<dyn Error>,
42}
43
44impl InputError {
45    /// Create a new [`InputError`].
46    ///
47    /// See [`ToIndex`] implementations for details on supported indexes.
48    #[cold]
49    pub fn new(input: &str, index: impl ToIndex, source: impl Into<Box<dyn Error>>) -> Self {
50        let index = index.input_index(input);
51        let (line_number, column_number, line) = Self::line_position(input, index);
52        let line = line.replace('\t', " ");
53
54        InputError {
55            line_number,
56            column_number,
57            line,
58            source: source.into(),
59        }
60    }
61
62    #[cold]
63    fn line_position(input: &str, index: usize) -> (usize, usize, String) {
64        let start = input[..index].rfind('\n').map_or(0, |p| p + 1);
65        let end = input[start..].find('\n').map_or(input.len(), |p| p + start);
66        let line = input[start..end].trim_end_matches('\r');
67
68        let line_number = input[..start].matches('\n').count() + 1;
69        let column_number = index - start + 1;
70
71        (line_number, column_number, line.to_string())
72    }
73
74    /// Returns the source error.
75    #[must_use]
76    pub fn into_source(self) -> Box<dyn Error> {
77        self.source
78    }
79}
80
81impl Display for InputError {
82    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
83        let pad = " ".repeat(self.line_number.to_string().len());
84
85        write!(
86            f,
87            "invalid input: {}\n  --> line {} column {}\n{pad} |\n{} | {}\n{pad} |{}^\n",
88            self.source,
89            self.line_number,
90            self.column_number,
91            self.line_number,
92            self.line,
93            " ".repeat(self.column_number),
94        )
95    }
96}
97
98impl Error for InputError {
99    fn source(&self) -> Option<&(dyn Error + 'static)> {
100        Some(&*self.source)
101    }
102}
103
104/// Helper trait to simplify error location tracking.
105///
106/// Used in [`InputError::new`].
107pub trait ToIndex {
108    fn input_index(self, input: &str) -> usize;
109}
110
111impl ToIndex for &str {
112    /// Find index of this substring in the provided input.
113    ///
114    /// Uses the pointer offset, meaning it works if this substring is not the first occurrence in
115    /// the string. This allows recovering the error position without tracking an offset into the
116    /// string, which is useful when using [`Iterator`]s such as [`str::lines`] on an input.
117    ///
118    /// # Panics
119    ///
120    /// This function panics if this string is not a substring inside the provided string.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// # use utils::input::ToIndex;
126    /// let string = "abcabc";
127    /// assert_eq!(string[4..].input_index(string), 4);
128    /// ```
129    ///
130    /// ```should_panic
131    /// # use utils::input::ToIndex;
132    /// let string = "abcabc";
133    /// let mut other = String::new();
134    /// other.push('b');
135    /// other.push('c');
136    /// other.input_index(string);
137    /// ```
138    fn input_index(self, input: &str) -> usize {
139        self.as_bytes().input_index(input)
140    }
141}
142
143impl ToIndex for &[u8] {
144    /// Find index of this subslice in the provided input.
145    ///
146    /// For use with functions that iterate over a string's bytes.
147    /// See the [`&str`](#impl-ToIndex-for-%26str) implementation.
148    fn input_index(self, input: &str) -> usize {
149        let self_ptr = self.as_ptr() as usize;
150        let input_ptr = input.as_ptr() as usize;
151        match self_ptr.checked_sub(input_ptr) {
152            Some(offset) if offset + self.len() <= input.len() => offset,
153            _ => panic!("invalid string index: {self_ptr:#x} is not a substring of {input_ptr:#x}"),
154        }
155    }
156}
157
158impl ToIndex for char {
159    /// Find the first instance of this character in the string.
160    ///
161    /// Intended for puzzles where the entire input should be a certain set of characters, so
162    /// if an invalid character is found, the instance in the error doesn't matter.
163    ///
164    /// # Panics
165    ///
166    /// This function panics if this character is not present in the string
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// # use utils::input::ToIndex;
172    /// let string = "abca bc";
173    /// assert_eq!(' '.input_index(string), 4);
174    /// ```
175    ///
176    /// ```should_panic
177    /// # use utils::input::ToIndex;
178    /// let string = "abcdef";
179    /// ' '.input_index(string);
180    /// ```
181    fn input_index(self, input: &str) -> usize {
182        input
183            .find(self)
184            .unwrap_or_else(|| panic!("invalid string index: char {self:?} not found in {input:?}"))
185    }
186}
187
188impl ToIndex for usize {
189    /// Index into the input string.
190    ///
191    /// # Panics
192    ///
193    /// This function panics if the index is out of range for the provided string.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// # use utils::input::ToIndex;
199    /// let string = "abcdef";
200    /// assert_eq!(4.input_index(string), 4);
201    /// ```
202    ///
203    /// ```should_panic
204    /// # use utils::input::ToIndex;
205    /// let string = "abcdef";
206    /// 10.input_index(string);
207    /// ```
208    fn input_index(self, input: &str) -> usize {
209        assert!(
210            self <= input.len(),
211            "invalid string index: index {self} out of range"
212        );
213        self
214    }
215}
216
217/// Extension trait to simplify converting errors and locations into [`InputError`]s.
218///
219/// Note that constructing [`InputError`] is expensive, and therefore conversion should be done as
220/// late as possible, to avoid unnecessary work if the error is discarded (for example,
221/// by [`Parser::or`](crate::parser::Parser::or)).
222pub trait MapWithInputExt {
223    type Output;
224    fn map_with_input(self, input: &str) -> Self::Output;
225}
226
227impl<E: Into<Box<dyn Error>>, I: ToIndex> MapWithInputExt for (E, I) {
228    type Output = InputError;
229
230    #[cold]
231    fn map_with_input(self, input: &str) -> Self::Output {
232        InputError::new(input, self.1, self.0)
233    }
234}
235
236impl<T, E: Into<Box<dyn Error>>, I: ToIndex> MapWithInputExt for Result<T, (E, I)> {
237    type Output = Result<T, InputError>;
238
239    #[inline]
240    fn map_with_input(self, input: &str) -> Self::Output {
241        self.map_err(|err| err.map_with_input(input))
242    }
243}
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]
263pub 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]
291pub 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}