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