Advent of Code 2023 - Day One
Overview
And We're Off... to a rough start
I was very excited to blast through last night's problem and get a decent night's sleep. Advent of Code had other plans.
Yesterday seemed simple enough, it was even easy to guess what the second half problem
modification was going to be. However, the example data didn't use the "edge" case that
many people had to deal with in frustration elation.
Here's my solution - we'll break down parts one and two after the break. GitHub
Part 1 - A Little Help from My (Regex) Friend
Part one was easy enough - grab the first and last single digit integer from each string, combine them into a two digit number (meaning if there was only one, double it), add up all the values, and there's your answer.
This will be a common refrain over all these blogs but: I'm sure there are better/faster ways to do this. Regex matching all single digit integers is easy enough
1$regx = [Regex]'\d'
2$numbers = $regx.Matches($string)
If you follow me on LinkedIn/BlueSky I posted some tips/tricks about accessing array
objects by index - so grabbing the first and last match is easy. This even has the
benefit of doubling the entry if there is only one match. We combine both into an
int
and bob's your uncle, we add all the values together:
1# [0] gives us the first match, [-1] gives us the last match
2[int]"$($numbers[0].Value)$($numbers[-1].Value)"
We didn't have too many "lines" to parse through, so no real need for crazy optimization. However, the strings had random characters, but often those "random" characters were the dictionary word for the numbers (one, two, three). You can guess what comes next...
Part 2 - Wait, there's something that Regex doesn't do natively?
Part two sounded easy on paper. You just need to convert the dictionary words in the string
to an integer and then we'll just grab the first and last integers again. However, you do
this, then submit your answer, and you realize you forgot to account for a problem. A few
of the numbers share a letter at the beginning or the end (threEight, twOne, etc.) So if
you do a replace, you end up getting something like 3ight
or 2ne
. Hmm...
Since we only care about the first number and last number, we really only need to find the first one and the last one and then replace with the number. Cool, so finding the first match and replacing is easy:
1$regx = '(one|two|three|four|five|six|seven|eight|nine)'
2$newString = $string -replace $regx, {"$([int][singleDigits]::$($_))$_"}
What is [singleDigits]
? It was just a quick enum so that I could pass the text value to
it and get it's integer representation. It's super basic:
1enum singleDigits {
2 zero
3 one
4 two
5 three
6 four
7 five
8 six
9 seven
10 eight
11 nine
12}
So the above replaces the first match based on the Regex string, with the int value AND the
original value. So eight18
would return 8eight18
. Why keep the original number? Well we
may still need it to find the last number if they overlap (remember from above).
Great, so we've replaced the first match, it should be easy to replace the last match with
Regex right? Well Regex doesn't find the last match very well. PowerShell master Chris Dent
did show me how you can set the dotnet Regex searcher to work from right to left like this:
[Regex]::Matrch($string, $pattern, 'RightToLeft')
. Of course I didn't consider that was
an option (should have read the documentation).
In my infinite wisdom late at night I just said, well, let's iterate through all the matches
and find out which one has the highest index, and then do a little substring mumbo jumbo. It's
ugly (and I admit in my code that I don't like it).
1function Update-Digits ($string) {
2 $regx = '(one|two|three|four|five|six|seven|eight|nine)'
3 $newString = $string -replace $regx, {"$([int][singleDigits]::$($_))$_"}
4
5 ## Ugh... reverse matching? I don't like this solution...
6 $words = "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"
7 $lastIndex = -1
8 $lastLength = 0
9 $lastValue = ""
10 foreach($word in $words)
11 {
12 $index = $newString.LastIndexOf($word)
13 if($index -gt $lastIndex){$lastIndex = $index; $lastLength = $word.Length; $lastValue = $word}
14 }
15 if($lastIndex -eq -1){
16 return $newString
17 }
18 if($lastIndex -eq 0){
19 return "$([int][singleDigits]::$lastValue)$($newString[($lastLength)..($newString.Length)] -join '')"
20 }
21 if($lastIndex -gt 0){
22 return "$($newString[0..$lastIndex] -join '')$([int][singleDigits]::$lastValue)$($newString[($lastIndex + $lastLength)..($newString.Length)] -join '')"
23 }
24}
So in the end it works... but I'm not happy with it
Day 2 - Part 2 - Attempt 2
After some caffeine and thought, I realized I overthought the whole shebang. I could just do what I did for part 1, but this time capture all the string matches (including overlaps):
1$regx = [regex]'(?=(one|two|three|four|five|six|seven|eight|nine))'
2$numbers = $regx.Matches($string)
Then if I find any matches, replace the first and last (again utilizing the negative index) and just put the numbers in place (accounting for a +1 to the index for the second number since we're adding a character to the string for the first number). No weird replaces or a bunch of substring junk:
1function Update-Digits ($string) {
2 $regx = [regex]'(?=(one|two|three|four|five|six|seven|eight|nine))'
3 $numbers = $regx.Matches($string)
4 if($numbers)
5 {
6 $firstMatch = $numbers[0].Groups[1].Value
7 $firstIndex = $numbers[0].Groups[1].Index
8 $lastMatch = $numbers[-1].Groups[1].Value
9 $lastIndex = $numbers[-1].Groups[1].Index
10 return $string.Insert($firstIndex, [int][singleDigits]::$firstMatch).Insert(($lastIndex + 1), [int][singleDigits]::$lastMatch)
11 }
12 return $string
13}
Not sure this is anymore performant, but honestly it feels cleaner and it runs fast enough. So I can close this day in my head and sleep well until 12AM Eastern when Day 2 really starts.
Thanks for tagging along with my crazy thought process and until next time, Happy Scripting!