utils/
input.rs

1//! Items relating to puzzle input.
2
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6/// Enum for distinguishing between example and real inputs.
7///
8/// Some puzzles require this as different constants may be used for example inputs to simplify the
9/// problem. For example [2022 day 15](https://adventofcode.com/2022/day/15) part 1, which uses
10/// `y=10` in the example, but `y=2000000` for real inputs.
11///
12/// Most puzzle solutions should ignore this value.
13#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
14pub enum InputType {
15    Example,
16    Real,
17}
18
19/// Error type that shows the error's location in the input, returned by puzzle `new` functions.
20///
21/// # Examples
22///
23/// ```
24/// # use utils::input::InputError;
25/// let input = "12 34\n56 78\n90 abc";
26/// let error = InputError::new(input, 15, "expected number");
27/// assert_eq!(error.to_string(), "
28/// invalid input: expected number
29///   --> line 3 column 4
30///   |
31/// 3 | 90 abc
32///   |    ^
33/// ".trim_start());
34/// ```
35#[derive(Debug)]
36pub struct InputError {
37    line_number: usize,
38    column_number: usize,
39    line: String,
40    source: Box<dyn Error>,
41}
42
43impl InputError {
44    /// Create a new [`InputError`].
45    ///
46    /// See [`ToIndex`] implementations for details on supported indexes.
47    #[cold]
48    pub fn new(input: &str, index: impl ToIndex, source: impl Into<Box<dyn Error>>) -> Self {
49        let index = index.input_index(input);
50        let (line_number, column_number, line) = Self::line_position(input, index);
51        let line = line.replace('\t', " ");
52
53        InputError {
54            line_number,
55            column_number,
56            line,
57            source: source.into(),
58        }
59    }
60
61    #[cold]
62    fn line_position(input: &str, index: usize) -> (usize, usize, String) {
63        let start = input[..index].rfind('\n').map_or(0, |p| p + 1);
64        let end = input[start..].find('\n').map_or(input.len(), |p| p + start);
65        let line = input[start..end].trim_end_matches('\r');
66
67        let line_number = input[..start].matches('\n').count() + 1;
68        let column_number = index - start + 1;
69
70        (line_number, column_number, line.to_string())
71    }
72
73    /// Returns the source error.
74    #[must_use]
75    pub fn into_source(self) -> Box<dyn Error> {
76        self.source
77    }
78}
79
80impl Display for InputError {
81    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
82        let pad = " ".repeat(self.line_number.to_string().len());
83
84        write!(
85            f,
86            "invalid input: {}\n  --> line {} column {}\n{pad} |\n{} | {}\n{pad} |{}^\n",
87            self.source,
88            self.line_number,
89            self.column_number,
90            self.line_number,
91            self.line,
92            " ".repeat(self.column_number),
93        )
94    }
95}
96
97impl Error for InputError {
98    fn source(&self) -> Option<&(dyn Error + 'static)> {
99        Some(&*self.source)
100    }
101}
102
103/// Helper trait to simplify error location tracking.
104///
105/// Used in [`InputError::new`].
106pub trait ToIndex {
107    fn input_index(self, input: &str) -> usize;
108}
109
110impl ToIndex for &str {
111    /// Find index of this substring in the provided input.
112    ///
113    /// Uses the pointer offset, meaning it works if this substring is not the first occurrence in
114    /// the string. This allows recovering the error position without tracking an offset into the
115    /// string, which is useful when using [`Iterator`]s such as [`str::lines`] on an input.
116    ///
117    /// # Panics
118    ///
119    /// This function panics if this string is not a substring inside the provided string.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// # use utils::input::ToIndex;
125    /// let string = "abcabc";
126    /// assert_eq!(string[4..].input_index(string), 4);
127    /// ```
128    ///
129    /// ```should_panic
130    /// # use utils::input::ToIndex;
131    /// let string = "abcabc";
132    /// let mut other = String::new();
133    /// other.push('b');
134    /// other.push('c');
135    /// other.input_index(string);
136    /// ```
137    fn input_index(self, input: &str) -> usize {
138        self.as_bytes().input_index(input)
139    }
140}
141
142impl ToIndex for &[u8] {
143    /// Find index of this subslice in the provided input.
144    ///
145    /// For use with functions that iterate over a string's bytes.
146    /// See the [`&str`](#impl-ToIndex-for-%26str) implementation.
147    fn input_index(self, input: &str) -> usize {
148        let self_ptr = self.as_ptr() as usize;
149        let input_ptr = input.as_ptr() as usize;
150        match self_ptr.checked_sub(input_ptr) {
151            Some(offset) if offset + self.len() <= input.len() => offset,
152            _ => panic!("invalid string index: {self_ptr:#x} is not a substring of {input_ptr:#x}"),
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/// Extension trait to simplify converting errors and locations into [`InputError`]s.
217///
218/// Note that constructing [`InputError`] is expensive, and therefore conversion should be done as
219/// late as possible, to avoid unnecessary work if the error is discarded (for example,
220/// by [`Parser::or`](crate::parser::Parser::or)).
221pub trait MapWithInputExt {
222    type Output;
223    fn map_with_input(self, input: &str) -> Self::Output;
224}
225
226impl<E: Into<Box<dyn Error>>, I: ToIndex> MapWithInputExt for (E, I) {
227    type Output = InputError;
228
229    #[cold]
230    fn map_with_input(self, input: &str) -> Self::Output {
231        InputError::new(input, self.1, self.0)
232    }
233}
234
235impl<T, E: Into<Box<dyn Error>>, I: ToIndex> MapWithInputExt for Result<T, (E, I)> {
236    type Output = Result<T, InputError>;
237
238    #[inline]
239    fn map_with_input(self, input: &str) -> Self::Output {
240        self.map_err(|err| err.map_with_input(input))
241    }
242}