This year I took part in “Advent of Code” - a challenge with the series of puzzles to solve using any programming language. I tried two years ago but resigned after the first day. This year was different, as we set the internal leaderboards, and I had a motivation to test my skills. My initial idea was to use only the PowerShell, but after some talks, I thought “maybe it’s a good moment to start learning go
lang”?
The answer was: no.
I started solving puzzles with PowerShell and learned go
along. But as puzzles began to be more difficult, I focused only on PowerShell. go
has to wait a bit.
I collected 28 stars out of 50 possible. Which I think is not that bad result. (You see 29 on the picture in the header because I started searching how others solved the problems and implemented first overdue task).
But - to the point. Before AoC I thought I know and understand PowerShell pretty well. During the AoC, I had to revisit it. Some tasks I usually do without thinking started to cause troubles when solving the puzzles. Like: hashtables didn’t want to cooperate with numbers, or: read the file; the first part is X, the second is Y. I was too much used to my default set of techniques, and it was sometimes hard to think outside the box.
This post summarises what I learned (or reminded) during the AoC, sometimes with links for the broader explanation.
1. Reading files
Not much new stuff. As usual - use Get-Content
, but become more familiar with -Raw
parameter to read all data as one piece of text instead a string[]
array. The -Raw
parameter allows easy splitting data in the file that has to be separated via empty line. Like:
line1;val1;val2
line2;val1;val2
part2:val1,val2
part2:val3:val4
part3|abc
part3|def
To separate the above code into three separate elements, use “-split “`r’n’r’n” “. Remember to use double quotes.
$customDeclarations = Get-Content "$PSScriptRoot\CustomDeclarations.txt" -Raw
$cd = $customDeclarations -split "`r`n`r`n"
2. Named regex
When using -match
we get $Matches
array with the numbered matches. Say we have these lines (taken from my Day02 puzzle input):
3-7 r: mxvlzcjrsqst
1-3 c: ccpc
6-12 f: mqcccdhxfbrhfpf
The task was to check if the letter appears between X and Y number of times in the password. Looking at the first line:
3-7
min/max appearancesr
lettermxvlzcjrsqst
password
We can read data using regex like (\d+)-(\d+) ([a-z]): ([a-z]+)
, but we have to remember the indices of the groups, like $Matches[3]
means the third group (the letter a-z).
$passwords = Get-Content .\Passwords.txt -First 3
$passwords | ForEach-Object {
$_ -match '(\d+)-(\d+) ([a-z]): ([a-z]+)' | Out-Null
$Matches
}
<#
Name Value
---- -----
4 mxvlzcjrsqst
3 r
2 7
1 3
0 3-7 r: mxvlzcjrsqst
4 ccpc
3 c
2 3
1 1
0 1-3 c: ccpc
4 mqcccdhxfbrhfpf
3 f
2 12
1 6
0 6-12 f: mqcccdhxfbrhfpf
#>
Instead, we can use named references in regexes using ?<name>
construction before the pattern, like:
(?<minLength>\d+)-(?<maxLength>\d+) (?<letter>[a-z]): (?<password>[a-z]+)
$passwords = Get-Content .\Passwords.txt -First 3
$passwords | ForEach-Object {
$_ -match '(?<minLength>\d+)-(?<maxLength>\d+) (?<letter>[a-z]): (?<password>[a-z]+)' | Out-Null
$Matches
}
<#
Name Value
---- -----
minLength 3
maxLength 7
letter r
password mxvlzcjrsqst
0 3-7 r: mxvlzcjrsqst
minLength 1
maxLength 3
letter c
password ccpc
0 1-3 c: ccpc
minLength 6
maxLength 12
letter f
password mqcccdhxfbrhfpf
0 6-12 f: mqcccdhxfbrhfpf
#>
The Out-Null
prevents the -match
result to appear on the screen (True
or False
)
3. Join lines
How to join a few lines in one? Use -replace
. Again - remember about double quotes.
$lines = '
hgt:176cm
iyr:2013
hcl:#fffffd ecl:amb
byr:2000
eyr:2034
cid:89 pid:934693255
'
$lines -replace "`n", ';'
# or: $lines -replace "`r`n", ';'
# hgt:176cm;iyr:2013;hcl:#fffffd ecl:amb;byr:2000;eyr:2034;cid:89 pid:934693255
4. Sort array of strings as numbers
AoC had almost all the puzzle inputs in the separate files. So I created the input files for each day and read it using Get-Content
. Some files contained a series of numbers, and when we read data from a file, we get all as a string. So when I wanted to sort the array I read from the file, I got unexpected results.
# simulating array read from file
$a = [string[]]@(2, 3, 1, 11, 15, 21)
$a | Sort-Object
<#
1
11
15
2
21
3
#>
PowerShell is not aware that those strings are numbers, so it orders them as strings. To sort as the number take a look in the documentation and use ScriptBlock as the -Parameter
:
# simulating array read from file
$a = [string[]]@(2, 3, 1, 11, 15, 21)
$a | Sort-Object { [int]$_ }
<#
1
2
3
11
15
21
#>
The Stack Overflow answer has a bit more about it and led me to the documentation.
5. Expanding arrays
It’s not a PowerShell trick or feature. Day 11 had a calculation of seats, and one of the tricks was getting info around the corners and edges. Like on a chequerboard - you have 64 squares. Each of them - excluding the edges - have 8 adjacent fields. The corner has three, and the edge has five. To check all the fields, you have to consider edges and corners as a different case. And it adds an overhead to the code.
It’s easier to add a “border” to the lattice (again: think chequerboard), and analyse the original data. Like this:
# original, 10 x 10
L.LL.LL.LL
LLLLLLL.LL
L.L.L..L..
LLLL.LL.LL
L.LL.LL.LL
L.LLLLL.LL
..L.L.....
LLLLLLLLLL
L.LLLLLL.L
L.LLLLL.LL
# with border (using dots), 12 x 12
............
.L.LL.LL.LL.
.LLLLLLL.LL.
.L.L.L..L...
.LLLL.LL.LL.
.L.LL.LL.LL.
.L.LLLLL.LL.
...L.L......
.LLLLLLLLLL.
.L.LLLLLL.L.
.L.LLLLL.LL.
............
Now my code will look clearer as I don’t use additional if
s or switch
es.
To add the border, I used this code:
# $seats0 is the original lattice
# add top and bottom border, the same length as the original row
$seats = @('.' * $columns) + $seats0 + @('.' * $columns)
# add left and right border to each row (including boundaries)
for($i = 0; $i -le $rows + 1; $i ++)
{
$seats[$i] = '.' + $seats[$i] + '.'
}
6. Hashtables and keys
Always be aware of the datatypes. A standard case with numbers:
$n = @(65, 66, 67, 68, 69)
$h = @{}
$n | ForEach-Object {$h[$_] = [char]$_}
$h
<#
Name Value
---- -----
69 E
68 D
67 C
66 B
65 A
#>
$h[69]
# E
$h.69
# E
$h.'69'
#(nothing)
But with the array of numbers as strings:
$n1 = @('65', '66', '67', '68', '69')
$h1 = @{}
$n1 | ForEach-Object {$h1[$_] = [char][int]$_}
<#
Name Value
---- -----
67 C
66 B
65 A
68 D
69 E
#>
$h1[67]
# (nothing)
$h1.67
# (nothing)
$h1.'67'
# C
$h1['67']
# C
Looks the same, but I had to use a string key, not a numeric, because of a different type.
7. Pre-fill an array with values
Sometimes I wanted to have an array with prefilled values. Like 10 values of 0:
$a = @(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
What if I know only the number of elements in an array (as a variable)? The fastest version:
$n = 57
$a = ,0 * $n
Also worth reading - an article on SimpleTalk.
8. Convert a number to a binary string
Use [Convert]::ToString($number, 2)
. Works with other bases too.
[Convert]::ToString(15, 2)
# 1111
[Convert]::ToString(15, 8)
# 17
[Convert]::ToString(15, 16)
# f
9. Pad numbers with zeroes
I want to prefix my number(s) with leading zeroes (like: 000000015
), and keep the lengths consistent - all prefixed numbers should have a length of 10. I used it to visualise a bitmask but works for every number.
-
Use formatting:
'{0:d10}' -f $number
; important: it has to be a number:$a = '15' '{0:d10}' -f $a # 15 '{0:d10}' -f [int]$a # 0000000015
-
Use
$number.PadLeft(10, '0')
; this time$number
has to be a string:'15'.PadLeft(10, '0') # 0000000015 $b = 15 $b.PadLeft(10, '0') # InvalidOperation: Method invocation failed because [System.Int32] does not contain a method named 'PadLeft'. 15.PadLeft(10, '0') # 15.PadLeft: The term '15.PadLeft' is not recognized as a name of a cmdlet, function, script file, or executable program. # Check the spelling of the name, or if a path was included, verify that the path is correct and try again. (15).PadLeft(10, '0') #InvalidOperation: Method invocation failed because [System.Int32] does not contain a method named 'PadLeft'.
-
Use
ToString('0000000000')
; works only with numbers:15.ToString('0000000000') # 15.ToString: The term '15.ToString' is not recognized as a name of a cmdlet, function, script file, or executable program. # Check the spelling of the name, or if a path was included, verify that the path is correct and try again. (15).ToString('0000000000') # 0000000015 '15'.ToString('0000000000') # MethodException: Cannot find an overload for "ToString" and the argument count: "1".
The list may expand in the future, as I plan to finish Advent of Code 2020, hopefully before AoC 2021.