Day 4: Part 1 - A lot to parse.
Well hello there! I didn’t see you come in…
This instalment of the MidWit’s Advent-ures in Rust is the first in what, if previous posts are anything to go by, could be a 700 year odyssey of earnestly taking the following 19 lines of code, which is a working solution to today’s puzzle1…
use aoc_common::read_challenge_input;
use md5::compute;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let input = read_challenge_input("./input.txt")?;
let five_os = (0..).find(|i| {
format!("{:?}",compute(format!("{}{}", input.trim(), i)))
.starts_with("00000")
});
let six_os = (0..).find(|i| {
format!("{:?}",compute(format!("{}{}", input.trim(), i)))
.starts_with("000000")
});
println!("five :{:?}", five_os.unwrap());
println!("six :{:?}", six_os.unwrap());
Ok(())
}and over engineering the b’jesus out of it.
You see dear reader, in my last post I touched on the idea of “propagating” Errors and using the Result<(), Box<dyn Error>> idiom. I joked about how interesting I found it, took a brief aside to describe my loose understanding of it… and then I proceeded to write an absolute nonbo of a solve that left it in main.rs without actually using it…
Hubris! Thy wit is Mid!
Thus, I intend to remedy this folly with what will seem like an even greater one, at least in an era of efficiency for its own sake.
The plan is to lean into writing a “productionesque” program, complete with
- bespoke
Errors, Errorpropagation,- data parsing,
- human-readable error messages,
- and proper separation of concerns.
There are lots of reasons I want to do this, but the most important one is to more fully commit to learning Rust, warts and all. To let myself forget about shipping a working solve, and push myself to learn by craft.
So far, the community’s responses to my posts have been incredibly welcoming and generous. Every comment has shown me something I’ve never seen before, or helped me to refine a hard won mental model. And so, rather than just trying to get lots of half-reps in, my hope is to use the next few posts as a space to correct my form based on what the community has pointed me towards.
Having shown you the destination already, I hope you’ll join me on the journey… after all… It’s dangerous to go alone.
Let’s make a newtype
The input for the puzzle is a string of lowercase ascii characters of length 5. As you can see up above, I can just pass this in as a String primitive, but if I want to build something robust then I can use those features to build a newtype with guaranteed input.
All the way back on day 1 I set myself a goal of using test driven development to drive what I’m doing. I then swiftly abandoned it…cough. Welp! That gets to be the first act of redemption on this road out of perdition.
First tests
I think the thing that feels weird about TDD is that you have to write quite a bit of code before you get to actually run anything. Coming from the social sciences and the repl-world (or at least the way I operate in that world) this kinda inverts my process. Instead of writing in response to feedback, TDD gets you to scope the feedback first.
Now I really like the idea of “seeing the end in the beginning”2, but it does delay my precious Booms. I’m just hoping that, like in real life, delaying the dopamine increases the effect. In order to get that beautiful hit though I need the following tests to pass.
- Test failure caused by a numeric character
- Test failure caused by upper case input
- Test failure caused by the presence of white space
- Test failure caused by short input
- Test failure caused by non-alphabetic symbols (‘-’ etc)
- Test failure caused by non ascii chars (‘β’, etc)
- Test successfully parsing a
Stringto aKey
Now, I’d like to think that I have as much discipline as the next guy who’s writing a learning blog to procrastinate actually working on his PhD, but rather than waiting until I have all 7 tests composed I’m going to start with wiring up the case where the input is too short. This will give me a little win relatively early, and then I can wire up the rest of the tests etc.
Ok, we have a test and to be honest3, writing it was really useful. Firstly it let me decide that I was going to call my method parse, which wasn’t a sure thing and secondly, it already has a clear Error variant example that I can use4. So the next thing to do is write the Key and the first version of the KeyParseError
Error version 1
use std::error::Error;
use std::fmt;
#[derive(Debug, PartialEq, Clone)]
pub enum KeyParseError {
TooShort,
}
impl fmt::Display for KeyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyParseError::TooShort => write!(f,
"The input is less than 5 characters long and as such will not satisfy the puzzle requirements. Check the input text."
)
}
}
}Ok, so using the standard pattern, we have a custom Error that will let me enforce some restrictions on our input. Let’s get Key written and work on parse().
Key
Now, if I’m getting this correct, these 4 lines of code are actually pretty powerful, please feel free to tell me if I miss something.
We start out with the old derive line and I’ve just given up and derived Clone before friend computer tells me I need it. Honestly, the fact that the option is there is good, but it does still feel a little like I’m cheating the system. That’s a me problem though.
We start actually writing the Struct itself on line 2, as a c-like Struct rather than an Enum. Defining it as pub will let me use it in my main.rs (more on that in later posts, don’t worry). Key only has one field: text which is of type String.
The fact that field isn’t public means that I can’t directly construct a Key with something like
Rust defaults to the lowest level of access unless otherwise specified. So unlike in python, where we might use something like pydantic to set defaults or limitations on the field (which I love also), the text field is protected from Ne’er-do-wells, Lascars, and poorly formatted data. Unless5… there is a constructor method that lets us set that value safely. That’s the Key::parse() associated function from our test.
parse round 1
Alright, here we have an associated method which, because it is implemented directly on the Struct is “allowed” to reach into the privacy of Key and create an instance. I’m fluffing a lot of description here, “reach into” is a reach. That’s the gist of it though.
Alright, the last ~170 lines of the post have all been set up for a passing test…

Whooo!! There’s that precious precious dopamine!!…
clears throat…
I believe that an independent function, something like
could work if the function is defined in the same module as Key. If we chose to split it into a different module (say if we put all struct definitions in one place, and functions in another) we’d have to make the text field public and we would lose the safety, the warmth and security provided by the associated function. Also, implementing it this way means that Key:: will allow a writer’s tool chain to show them ::parse().
Also, I’ve never seen that in Rust examples (but I have in R… blurgh).
Draw the rest of the horse
I chose test_too_short in the spirit of early wins and gaining momentum, it was a pretty clear problem to solve. I did make the mistake of using .len() instead of .chars().count() until I saw this post6, but thankfully there’s an awful lot of guidance out there if you look around a little.
The next step is to wire up the rest of the code; to put the rest of the variants in the KeyParseError, finish the Display Trait implementation, and finally work through the rest of the guards in parse().
Before that though, here’s the other tests.
#[cfg(test)]
mod tests {
use core::assert_eq;
use super::*;
#[test]
fn test_too_short() {
let input = String::from("ab");
let failed = Key::parse(input);
assert_eq!(failed,Err(KeyParseError::TooShort));
}
#[test]
fn test_number() {
let input = String::from("1bcdef");
let failed = Key::parse(input);
assert_eq!(failed, Err(KeyParseError::ContainsNumeric));
}
#[test]
fn test_upper() {
let input = String::from("ABCDEF");
let failed = Key::parse(input);
assert_eq!(failed, Err(KeyParseError::UpperCase));
}
#[test]
fn test_white_space() {
let input = String::from("a cdef");
let failed = Key::parse(input);
assert_eq!(failed, Err(KeyParseError::ContainsWhiteSpace));
}
#[test]
fn test_non_ascii() {
let input = String::from("aβcedf");
let failed = Key::parse(input);
assert_eq!(failed, Err(KeyParseError::NonAscii));
}
#[test]
fn test_invalid() {
let input = String::from("a-cedf");
let failed = Key::parse(input);
assert_eq!(failed, Err(KeyParseError::InvalidAscii));
}
#[test]
fn test_new() {
let input = String::from("abcdef");
let parsed = Key::parse(input);
let received = Key{text:String::from("abcdef")};
assert_eq!(parsed.unwrap(),received);
}
}So we have all the other failure paths and a successful one ready to test. We’ll come to the successful one once we’re finished getting KeyParseError to Display.
#[derive(Debug, PartialEq, Clone)]
pub enum KeyParseError {
ContainsNumeric,
ContainsWhiteSpace,
UpperCase,
TooShort,
NonAscii,
InvalidAscii
}
impl fmt::Display for KeyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyParseError::ContainsNumeric => write!(f,
"One or more of the characters in the input is numeric"
),
KeyParseError::ContainsWhiteSpace => write!(f,
"One or more of the characters in the input is white space, check for trailing or leading spaces or newlines."
),
KeyParseError::TooShort => write!(f,
"The input is less than 5 characters long and as such will not satisfy the puzzle requirements. Check the input text."
),
KeyParseError::UpperCase => write!(f,
"One or more of characters in the input is upper case."
),
KeyParseError::NonAscii => write!(f,
"One or more of characters in the input is not an ascii encoded character."
),
KeyParseError::InvalidAscii => write!(f,
"One or more of characters in the input is ascii but not a valid alphabetical character"
),
}
}
}
impl Error for KeyParseError{}And there we have it; all 6 variants of the KeyParseError in place with a route to Display-ing them when they are needed. I have a thought about how to set them up to include the actual failing output to make it even easier for a human receiving the Error to diagnose what’s going on, but I’m not sure of it right now, and lord knows these posts can easily bloat on me. I’m going to just get the needful done, and then before I ship the code I’ll make sure that I take a run at that.
parse the rest of the horse
The TooShort failure path is one clear problem; count the characters. The others require that we look “into” the collection of characters and make sure that each one is valid: is it a lowercase character in the range of ‘a’ to ‘z’? I’m sure there’s lots of ways to do this, but I’ve chosen to do this with a loop and some good old ifs.
impl Key {
pub fn parse(input:String) -> Result<Key, KeyParseError> {
if input.chars().count() < 5 {
return Err(KeyParseError::TooShort)
}
for c in input.chars() {
if c.is_ascii_digit() {
return Err(KeyParseError::ContainsNumeric)
} else if c.is_ascii_whitespace() {
return Err(KeyParseError::ContainsWhiteSpace)
} else if c.is_ascii_uppercase() {
return Err(KeyParseError::UpperCase)
} else if !c.is_ascii() {
return Err(KeyParseError::NonAscii)
} else if !c.is_ascii_lowercase() {
return Err(KeyParseError::InvalidAscii)
}
}
Ok(Key {text:input})
}
}Now, I’ve been trying to avoid the “line-by-line” tutorial style explanations here, because, well read the post tags. But part of learning is explaining what you think so you know and why you think you did what you did… So I’m going to do that here. If I’m missing something significant, or even if there’s just a smoother way that I haven’t thought of I trust the community (after 6 posts) to tell me as much.
This is the first time in this series that I’ve reached for the if/else if/else pattern instead of the match approach, and I won’t pretend that I didn’t try the match approach first. Badly. A few times.
Your name’s on the guest list
As far as I understand it a match statement allows us to check against patterns, and importantly to patterns that friend computer7 can reason about, to hold fast against our folly. This would allow me to do something like
which solves the problem generally, it allows all valid characters and rejects all others. Neat.
But… I had hoped to be able to write some more useful errors than just “there’s some bad input here”. I know the pain of bad error messaging, and really wanted to try to avoid it. In fact the quality of a lot of the error messages that friend computer gives us is one of the things that attracted me to Rust.
we don’t like yer type around here
My next thought was to simply extend the match to catch the specific form of the invalid input.
but then… things got… persnickety.
When I came to the whitespace failure mode I figured that I could look around for the various specific characters that fall into that category and match all of them.
// -- snip --
for c in input.chars() {
match c {
'a'..='z' => {},
'0'..='9' => return Err(KeyParseError::ContainsNumeric),
'A'..='Z' => return Err(KeyParseError::UpperCase),
' ' | '\t' | '\n' | '\r' => return Err(KeyParseError::ContainsWhiteSpace),
// -- snip --
}
}
Ok(Key { text: input })
// -- snip --But honestly, though admittedly unfinished, that tastes bad to me. Partly because I’m 100% sure there is some additional way of representing white space that I just don’t know about, and partly because it is a lot to type.
ascii methods
Which in turn led me to the .is_ascii... set of methods and match guards.
// -- snip --
for c in input.chars() {
match c {
'a'..='z' => {},
'0'..='9' => return Err(KeyParseError::ContainsNumeric),
'A'..='Z' => return Err(KeyParseError::UpperCase),
c if c.is_ascii_whitespace() => return Err(KeyParseError::ContainsWhiteSpace),
c if !c.is_ascii() => return Err(KeyParseError::NonAscii),
_ => return Err(KeyParseError::InvalidAscii),
}
// -- snip --The code above passes, and it gives me what I want. So we might be expecting our concluding Boom here, no?
Well, you already know I didn’t go with this in the end, and again, it’s because it tastes funny.
On the one hand I’m using a feature I learned about through Rust (I’d never heard of the python switch before), but on the other this implementation feels like it’s working against that. As it says in The Book, guards allow us to extend the match expression to account for things we can’t (or don’t know how to). However,
The downside of this additional expressiveness is that the compiler doesn’t try to check for exhaustiveness when match guard expressions are involved.
This means I’d need a _ wildcard even if we as humans know that the match is exhaustive… but I’m very dubious on the idea of “knowing” in this context. I can always be pretty sure, especially in this domain that there’s a Rumsfeld8 out there waiting to shoot me in the face in a fore… to cause trouble. The wildcard in the case above is genuinely doing work by catching ascii encoded symbols, but as a reflexive habit it seems foolish to rely on it.
And damnit I’m here to remedy past follies, not make new ones.
So, in a sense, the match just ends up feeling like the if/else version wearing a ball gown that doesn’t fit9, and so I settled on the if/else. It may not be the right decision, but it was a decision.
Among the consequences of that decision though is that I need something like if !c.is_ascii_lowercase() as the echo of _ => return Err(KeyParseError::InvalidAscii), in my if/else ladder. Otherwise I don’t have a way to guard against ascii symbols (like ‘-’) which would otherwise fall through the rest of the conditions. It’s good to really see that the match and the if/else patterns aren’t just a question of ergonomics, they are related to each other, but more like close cousins than siblings.
Ok, in the words of Lady Madonna, let’s see how they run…
Test outcome
// -- snip --
running 7 tests
test key::tests::test_new ... ok
test key::tests::test_invalid ... ok
test key::tests::test_non_ascii ... ok
test key::tests::test_number ... ok
test key::tests::test_upper ... ok
test key::tests::test_too_short ... ok
test key::tests::test_white_space ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sAnd the wind cried Boom.
Seven passing tests, input parsed (not just validated), pretty good human-readable error messages, and the first step towards using a Result in my main function.

Improving the output
One of my least favourite things in my day-to-day work is the Deno stacktrace Quarto spits out. For a product that makes my life a whole lot better, it really knows how to be unhelpful when you need it10. They’re hard to parse unless you know them, and they don’t provide a lot of insight to the user, many of whom aren’t devs or coders or engineers that can parse errors well.
In the period just before the rise of the machines python had made real progress improving tracebacks and Rust had done a great job writing really helpful error messages. They still required some learning to be able to parse them, but they were just generally more kind.
I love that.
One of the things that worries me now that all of us are engaging with LLMs more is that we’ll stop writing messages for humans, or that humans will go back to being an after thought, with messages that make sense to LLMs (well, a normally distributed amount of the time) and leave humans out of the loop.
I always wondered how people who were so manifestly smart, and so oriented on improving people’s lives could render messages error messages that amount to “Something is wrong” for a lot of their users (like the scientist or a student trying to render a paper without spending years in MS word fighting margins). I think that this exercise has given me some insight into that.
The reality is that this whole project is a hobby for me. I care about it, and I want to use it to prep me for a Devious Plan™, but it’s not how I earn my crust.
In working through the difference between the match with an “allow list” approach I could see how tempting it would be to write
and move on. It has an awful lot of things in its favour.
- much shorter to write
- easier to test
- less complication (through seeing the actual complexity)
- technically safer
However, it leaves a lot of work on the user. It solves the problem in an ideal world, and yes, AOC is in many ways that, but it is setting up a situation that I myself would hate to be in. And have been in fact.
So I chosen to go with an implementation that lets me give the user a little more guidance about what they need to do to fix it.
On the one hand that leans away from the technical excellence that Rust espouses, but it leans towards the more humane approach that Rust takes to the people using the software. I might be misreading the room here, and I might have made the wrong decision. But that’s what was behind it.
The problem I’m now faced with is: now that I’ve made that decision it invites even more labour.
is helping halfway really helping?
In the case where there’s only 5 characters, my error messages aren’t bad; “There’s a number in the input, go fix that” at least gives the user a clear sense of what to look for. But what if the input was longer? Well, I could just write a case like TooLong to set an upper limit on the Key, and maybe I should, but even in the case where I did that it would be useful to try to show the user where the error is coming from.
So what about showing the user which character is causing the problem and where it is in the chain? This is an easy fix, but it adds a lot more text to my codebase.
Rather than enumerate every step, I’m going to put the whole new (and final, for this post) implementation below.
Final key.rs (for now…dun dun duuuuuuuuun)
use std::error::Error;
use std::fmt;
#[derive(Debug, PartialEq, Clone)]
pub enum KeyParseError {
ContainsNumeric { character: char, index: usize },
ContainsWhiteSpace { character: char, index: usize },
UpperCase { character: char, index: usize },
TooShort { length: usize },
NonAscii { character: char, index: usize },
InvalidAscii { character: char, index: usize },
}
impl fmt::Display for KeyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyParseError::ContainsNumeric { character, index } => write!(f,
"One or more of the characters in the input is numeric: the bad character is \"{}\", and it is at position {}",
character,
index
),
KeyParseError::ContainsWhiteSpace { character, index } => write!(f,
"One or more of the characters in the input is white space, check for trailing or leading spaces or newlines: the bad character is \"{}\", and it is at position {}",
character,
index
),
KeyParseError::TooShort { length }=> write!(f,
"The input is less than 5 characters long and as such will not satisfy the puzzle requirements: the input is {} characters long",
length,
),
KeyParseError::UpperCase { character, index } => write!(f,
"One or more of characters in the input is upper case: the bad character is \"{}\", and it is at position {}",
character,
index
),
KeyParseError::NonAscii { character, index } => write!(f,
"One or more of characters in the input is not ascii: the bad character is \"{}\", and it is at position {}",
character,
index
),
KeyParseError::InvalidAscii { character, index } => write!(f,
"One or more of characters in the input is ascii but not a valid alphabetical character: the bad character is \"{}\", and it is at position {}",
character,
index
),
}
}
}
impl Error for KeyParseError {}
#[derive(Debug, PartialEq, Clone)]
pub struct Key {
text: String,
}
impl Key {
pub fn parse(input: String) -> Result<Key, KeyParseError> {
if input.chars().count() < 5 {
return Err(KeyParseError::TooShort {
length: input.chars().count(),
});
}
for (i, c) in input.chars().enumerate() {
if c.is_ascii_digit() {
return Err(KeyParseError::ContainsNumeric {
character: c,
index: i,
});
} else if c.is_ascii_whitespace() {
return Err(KeyParseError::ContainsWhiteSpace {
character: c,
index: i,
});
} else if c.is_ascii_uppercase() {
return Err(KeyParseError::UpperCase {
character: c,
index: i,
});
} else if !c.is_ascii() {
return Err(KeyParseError::NonAscii {
character: c,
index: i,
});
} else if !c.is_ascii_lowercase() {
return Err(KeyParseError::InvalidAscii {
character: c,
index: i,
});
}
}
Ok(Key { text: input })
}
}
#[cfg(test)]
mod tests {
use core::assert_eq;
use super::*;
#[test]
fn test_too_short() {
let input = String::from("ab");
let failed = Key::parse(input.clone());
assert_eq!(
failed,
Err(KeyParseError::TooShort {
length: input.chars().count()
})
);
}
#[test]
fn test_number() {
let input = String::from("1bcdef");
let failed = Key::parse(input);
assert_eq!(
failed,
Err(KeyParseError::ContainsNumeric {
character: '1',
index: 0
})
);
}
#[test]
fn test_upper() {
let input = String::from("ABCDEF");
let failed = Key::parse(input);
assert_eq!(
failed,
Err(KeyParseError::UpperCase {
character: 'A',
index: 0
})
);
}
#[test]
fn test_white_space() {
let input = String::from("a cdef");
let failed = Key::parse(input);
assert_eq!(
failed,
Err(KeyParseError::ContainsWhiteSpace {
character: ' ',
index: 1
})
);
}
#[test]
fn test_non_ascii() {
let input = String::from("aβcedf");
let failed = Key::parse(input);
assert_eq!(
failed,
Err(KeyParseError::NonAscii {
character: 'β',
index: 1
})
);
}
#[test]
fn test_invalid() {
let input = String::from("a-cedf");
let failed = Key::parse(input);
assert_eq!(
failed,
Err(KeyParseError::InvalidAscii {
character: '-',
index: 1
})
);
}
#[test]
fn test_new() {
let input = String::from("abcdef");
let parsed = Key::parse(input);
let received = Key {
text: String::from("abcdef"),
};
assert_eq!(parsed.unwrap(), received);
}
}Ok, so after running rustfmt on the final file we have, and I’m genuinely, unreasonably happy about this11, literally 10xed our line numbers. That’s what everyone keeps talking about right!? Right!?!
Also, all the tests still pass. Boom!… but… y’know. 10x!
Reflections
Being a MidWit means that my life’s motto has always been “it can’t be that hard”. The process of recovery starts with recognising there’s a problem. This whole blog has been a process of unfolding the problem in ever greater measure, watching it balloon like a Lovecraftian horror that I couldn’t see until I saw it. And now it can’t be unseen.
Working through the parse() function and deciding on the if/else pattern over the match guard approach was a really useful experience. I feel like I made a rational decision based on my aims. Any feedback on it is very welcome, so far all the comments on these posts have been incredibly useful, pointing me at things I didn’t know could be used.
However, it also made me see the allure of LLMs in a different light. I’ve spoken before about how they’ve let me down, and how I get that we’re all trying to figure them out. As a learning tool they really can be both a blessing and a curse, and they require a lot of discipline. But, once I had applied { character: char, index: usize } in the KeyParseError it was kinda tedious to wire it into all the other areas of the code. And this isn’t a big file.
The problem is that it still feels like the place where it would be most useful: a very large file, is the place where hallucinations, errors, or unwarranted changes, would be the hardest to account for.
Man, that really doesn’t help that whole conundrum does it?
Anyway, that is the current version of key.rs and my next task is to create a function that will take in the Key and give me back a solution to puzzle. I will definitely have to revisit this file as a result of what happens when I write that up, and right now that seems like it would be fun.
It can’t be that hard.
Godspeed,
The MidWit:wqa
Footnotes
Specifically day 4 of the 2015 round of Advent of code. If you want to see the actual puzzle please go check the site, sign up and maybe buy some merch! They’re great puzzles, and a really good way to learn the basics of a language.↩︎
Like the watchman, if you know you know.↩︎
Sometimes it really feels like this is all just a process of um Ackshullying myself.↩︎
Just a note to say that I know that I could have a “TooLong” path as well, but I’ve decided to think of the
Keylike a password and just set a minimum length. Like… It’d be a really bad password, but it is an actual decision I’ve made, not just an oversight.↩︎Unless?↩︎
Remember Stack overflow?↩︎
In their infinite grace and wisdom…↩︎
An unknown unknown.↩︎
Even for me that was a bit Ted Lasso↩︎
It really is brilliant as a tool and I recommend and teach it to a lot of people, and the Quarto errors themselves aren’t bad, but they are often wrapped in a lot of noise that can be overwhelming.↩︎
I give you my word that I didn’t do this on purpose!↩︎