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        let self_ptr = self.as_ptr() as usize;
151        let input_ptr = input.as_ptr() as usize;
152        match self_ptr.checked_sub(input_ptr) {
153            Some(offset) if offset + self.len() <= input.len() => offset,
154            _ => panic!("invalid string index: {self_ptr:#x} is not a substring of {input_ptr:#x}"),
155        }
156    }
157}
158
159impl ToIndex for char {
160    /// Find the first instance of this character in the string.
161    ///
162    /// Intended for puzzles where the entire input should be a certain set of characters, so
163    /// if an invalid character is found, the instance in the error doesn't matter.
164    ///
165    /// # Panics
166    ///
167    /// This function panics if this character is not present in the string
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// # use utils::input::ToIndex;
173    /// let string = "abca bc";
174    /// assert_eq!(' '.input_index(string), 4);
175    /// ```
176    ///
177    /// ```should_panic
178    /// # use utils::input::ToIndex;
179    /// let string = "abcdef";
180    /// ' '.input_index(string);
181    /// ```
182    fn input_index(self, input: &str) -> usize {
183        input
184            .find(self)
185            .unwrap_or_else(|| panic!("invalid string index: char {self:?} not found in {input:?}"))
186    }
187}
188
189impl ToIndex for usize {
190    /// Index into the input string.
191    ///
192    /// # Panics
193    ///
194    /// This function panics if the index is out of range for the provided string.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// # use utils::input::ToIndex;
200    /// let string = "abcdef";
201    /// assert_eq!(4.input_index(string), 4);
202    /// ```
203    ///
204    /// ```should_panic
205    /// # use utils::input::ToIndex;
206    /// let string = "abcdef";
207    /// 10.input_index(string);
208    /// ```
209    fn input_index(self, input: &str) -> usize {
210        assert!(
211            self <= input.len(),
212            "invalid string index: index {self} out of range"
213        );
214        self
215    }
216}
217
218/// Strips the final newline from a borrowed string.
219///
220/// Equivalent to `s.strip_suffix("\r\n").or_else(|| s.strip_suffix("\n")).unwrap_or(s)`.
221///
222/// # Examples
223/// ```
224/// # use utils::input::strip_final_newline;
225/// assert_eq!(
226///     strip_final_newline("abc\ndef\n"),
227///     "abc\ndef"
228/// );
229/// assert_eq!(
230///     strip_final_newline("12\r\n34\r\n\r\n"),
231///     "12\r\n34\r\n"
232/// );
233/// ```
234#[must_use]
235#[inline]
236pub const fn strip_final_newline(s: &str) -> &str {
237    match s.as_bytes() {
238        // Use split_at as string slicing isn't const
239        [.., b'\r', b'\n'] => s.split_at(s.len() - 2).0,
240        [.., b'\n'] => s.split_at(s.len() - 1).0,
241        _ => s,
242    }
243}
244
245/// Convert a string to both LF and CRLF if it contains a newline.
246///
247/// # Examples
248/// ```
249/// # use utils::input::to_lf_crlf;
250/// assert_eq!(
251///     to_lf_crlf("abc\ndef\nghi"),
252///     ("abc\ndef\nghi".into(), Some("abc\r\ndef\r\nghi".into()))
253/// );
254/// assert_eq!(
255///     to_lf_crlf("12\r\n34\r\n56\r\n78"),
256///     ("12\n34\n56\n78".into(), Some("12\r\n34\r\n56\r\n78".into()))
257/// );
258/// assert_eq!(
259///     to_lf_crlf("abc123"),
260///     ("abc123".into(), None),
261/// );
262/// ```
263#[must_use]
264pub fn to_lf_crlf(s: &str) -> (Cow<'_, str>, Option<Cow<'_, str>>) {
265    let (mut has_lf, mut has_crlf) = (false, false);
266    let mut prev = 0;
267    for b in s.bytes() {
268        has_lf |= b == b'\n' && prev != b'\r';
269        has_crlf |= b == b'\n' && prev == b'\r';
270        prev = b;
271    }
272    if !has_lf && !has_crlf {
273        return (Cow::Borrowed(s), None);
274    }
275
276    let lf = if has_crlf {
277        Cow::Owned(s.replace("\r\n", "\n"))
278    } else {
279        Cow::Borrowed(s)
280    };
281    let crlf = if has_lf {
282        Cow::Owned(lf.replace('\n', "\r\n"))
283    } else {
284        Cow::Borrowed(s)
285    };
286    (lf, Some(crlf))
287}