15
votes
Day 10: Syntax Scoring
Today's problem description: https://adventofcode.com/2021/day/10
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python
with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
Python golf, day 10. Tips/improvements always welcome. A suspiciously straightforward problem today, but I'm not complaining. There are lots of magic numbers in here in an attempt to be concise. They are derived from the ASCII values of characters, for which Python's
ord()
came in handy.Part 1
Part 2
Elixir
Quick one! My solution is kinda messy, but I'm proud of it. Pattern matching and quasi-macros to generate all the necessary clauses of the validation functions made this quick to implement.
I might come back and add some explanation of this code since Elixir syntax is a little... out there compared to the C-like languages, but for now I'm going to bed.
Both parts
Hint
Use a stack!
Edit: each part runs in 130-150 μs, so that's fun :D
Edit: Explaining some of the syntax for those interested:
Pattern matching
Pattern matching is a core concept in Elixir, and allows for pretty concise and expressive code. It's sort of like JavaScript's destructuring on steroids—you can bind nested values in a data structure to variables while also asserting the shape of the structure. Pattern matching is most often done with the
case
expression, but it's also possible to use directly in function heads and in many other parts of the language.At its core, pattern matching lets you write assertive code based on how the data looks, which can be more expressive than in other languages where the main tool at your disposal is
if
+ an expression that evaluates to a boolean.Pattern matching
Case expression
Multiple function clauses
In Elixir, much more logic can live in the function head than in a lot of other languages—you can pattern match on the arguments there rather than inside the function body. This allows you to split out different branches of logic of a function into completely separate clauses, changing the mental model of function definitions to be closer to simple mapping operations. "Function
f
, applied to a value that looks likeX
, producesX'
. Functionf
, applied to a value that looks likeY
, producesY'
. ..."Pattern matching is also generally a constant-time operation due to restrictions on the assertions you can make, and when it's done in the function head it gets optimized even more during compilation. This means you can gain a pretty significant performance boost by moving a lot of logic into the function head.
Multiple function clauses
Macros; generating function clauses
The final piece of the puzzle here is Elixir's metaprogramming facilities. I've only dipped my toes in macros since they come with a lot of caveats, but one relatively safe use is generating a lot of similar function clauses from a list of "seed" values at compile time. This essentially boils down to programmatically generating the patterns instead of writing them all out manually. It also often comes with a big performance boost since the work is done at compile time. You can see an extreme example of this in Elixir's Unicode tokenizer—at compile time, it parses all of the codepoints from a text file and creates a huge number of function clauses.
Macros
Other syntax tidbits
|>
: pipe operator.div(abs(n), 2) == n |> abs() |> div(2)
&
: function capture operator. Captures a named function to be bound to a variable or otherwise passed around. Syntax is&function_name/arity
; alternatively it can be used for quickly creating anonymous functions or partial application/closures by using&1
,&2
etc as positional arguments:increment = &Kernel.+(&1, 1)
?
: codepoint operator. Gets the unicode codepoint of a single character.?a == 97
,?0 == 48
''
: charlist. As opposed to a string"foo"
, a charlist'foo'
is literally a list of codepoints.'foo' == [?f, ?o, ?o] == [102, 111, 111]
. Great for when you need to work through a string one character at a time._
: "any" pattern match. Lets you assert the structure of a piece of data without caring about its value.{:ok, _} = {:ok, 5}
|
(in a list): head/tail separator. Elixir lists are singly-linked lists and the syntax for getting the head and the tail is[head | tail]
.[1, 2, 3] == [1 | [2 | [3 | []]]]
JavaScript
Repo Link
Part 1
This was pretty straightforward, especially since in one of the classes I teach, I use a very similar example to have students practice with using a stack :).
Part 2
For this part, I had a lot of fun using JavaScripts functional programming capabilities.
As always, I am in Google Sheets. Here's a single formula for part 1. I am so sorry to anybody who has to see that.
Part 1
Part 2
lets flip it and reverrrrrse it.
This is basically creating a QUERY with the MAX(LEN sequence across the top. It transposes it, sorts it in reverse, transposes again, offsets by one... then goes into this weird concat system.
With that out of the way I've got a phat table that is a breeze to work with.
First column
Next column, dragged across for the fourteen remaining columns,
Take the MEDIAN and we're good as gold.
lol - market it as abstract code art
haha. its so crazy, right? Check this to see all of the output -- I'll have an NFT up in no time. :)
I succumbed and implemented quicksort for this; I should probably clean it up a bit as I might have to paste it in a bunch of other future puzzles.
Part 1
Part 2
Part 1
Edit::
Sometimes the excitement in the arena gets to you.
After spending time away, having some dinner and relaxing a bit, I realised I'm dumb. Part 1
parse
already collects the opened items yet I use it to filter out corrupt lines in Part 2 and then iterate again re-collecting them ... like a doofus. Now I return both the collected openers and the corruption character together; part 1 only uses the corruption, part 2 skips any corruptions and calculates the score of the opened items left on the stack.I'll leave my idiocy on display here for posterity - you can see the improved version in the repo..
This is ugly code, but it's deliberate to get the runtime down as much as I could. Any tips to make it faster are very welcome. In Lisp a stack is just a list so
parse
maintains a stack of opening chars and the offending character that causes corruption.I did toy with the idea of using Racket's own parser (
read
) loose on the problem but it's not really the right tool for the job and involves catching exceptions rather than failing gracefully.Part 2
I used
parse
from Part 1 to filter out the corrupt lines. 'completion' pushes and pops the opening characters on the stack (list !) - safe in the knowledge it isn't corrupt. Whatever is left over on the stack at the end is those openers that need completing.Testing
Did this feel especially difficult, because I feel Eric provided a pretty impressive test-suite in this question - above and beyond what's normally given. For posterity here's my test-suite taken directly from the question
This one was kind of fun, and the most switch statements I've used so far. I thought for about 10 seconds at the start about building a regex pattern to look for these matches, but then shuddered and moved on to what I thought would be easier. And, to my great surprise, my code was in a pretty good spot for part 2 using only the most basic array functions!
Part 1
Part 2
Can't wait to see a golfed solution.
Python
Python
Edit: shorted.
This one was nice and pretty easy, I'm expecting things to get a lot harder as we get into this coming weekend.
Part 1 Ruby
This is a pretty classic parsing situation, we just need to use a stack to keep track of how many layers of brackets deep we are, and what kind of closing bracket we expect to see next. Each time we find a new opening bracket we add the appropriate closing bracket to our stack, and each time we find a closing bracket we just remove the top item from our stack and check to see if it's the same.
Part 2 Ruby
Somewhat surprisingly, not really any harder than part 1. We do pretty much the exact same thing here, except that we know all the lines are "well formed" and don't have any incorrect closing brackets anywhere (since we just wrote a function we can use to filter them out).
We just run the exact same logic to build up a stack of expected closing brackets, popping them back off as we find them in the line, and whatever is left over in our stack of expected values is what we use to score the line.
Is it just me or does this year feel really parsing-heavy? I feel like previous years focused more on math and logic and less on string manipulation
Day 10 Part A - Python
Regex putting in work once again. I iterated through the strings replacing completed sets of brackets until I couldn't anymore, either because I ran out of characters or the string stayed the same size after trying all possible combinations.
Once the string was reduced, whipped up some regex of possible invalid combinations and looked for those. Though having looked through more of the data, I'm pretty sure I could have just looked for the first remaining closing bracket. From there it was was trivial to sum up the score
I don't know if I got lucky or it was by design, but none of my strings seemed to have multiple copies of invalid characters. That could have thrown a wrench into my plans.
Day 10 Part B - Python
My method for Part A put me in a good position for Part B, since it left me with the remainder, which was the mirror of what I wanted. Just flip the string and do some replacement to swap the characters with their closing counterparts.
Tips
If you haven't already, I HIGHLY recommend looking into learning Regular Expressions. https://regexone.com/ was where I learned how to use them, it's really helpful. And https://regexr.com/ is a great sandbox for testing your patterns and debugging them
In part A I found it much easier to remove matches one at a time, rather than trying to process them in place
Depending on how you approach Part A, Part B might just require flipping some strings around
Why use
spoilers
a VecDeque instead of just a Vec? The Rust docs say to use a Vec when you want a stack, but is popping from a VecDeque actually faster?Rust
Pleased to have another relatively easy one after the rough day 8.
Rust
We need a day 8 support group
its comforting to know that a lot of people struggled with day 8. I'm working in a spreadsheet and figured it was impossible handle it somewhat elegantly, so I didn't want to bother. It turned out nice, but I think that's about the limit of my abilities :)
Day 10 (Rust)
Data structures & Parsing
If the line is corrupt,
LineState
will contain the offending char. If the line is incomplete, it contains the characters for completing the line in reverse order (i.e. as a stack).Helper function
This uses a stack to keep track of which closing character is next.
Solution
You didn't have to sort your part 2 scores to get the right answer?
Nope. Well, kind of. :)
I used
select_nth_unstable()
which uses a variant of the quickselect algorithm. It finds out what element would be at the given position if you were to sort the list. The input list is partially sorted and returned as part of the result, but I ignore all that and just get the final value in the middle of the list.Rust
Not too bad, though I possibly could've limited the nesting a bit.
Rust
Spoiler thoughts
This one was not too bad! It looks like a lot of people used regex for this, but since it was so simple and I used a sequence as a stack it made part 2 not too difficult. I just had to modify my output and filter it to get the right information to the right places, and then remember to reverse the stack. I do wish Nim had a more graceful way to iterate backwards, given how nice a lot of the other syntax is.
Part 1
Part 2 diff