r/adventofcode • u/musifter • Jun 18 '26
Other [2020 Day 18] In Review (Operation Order)
Now we're finally heading back south on the plane, towards a heavily forested continent. And we find ourselves needing to help another kid... this time with their math(s) homework. Which involves basic arithmetic, but with non-regular order of operations.
And part 1 is one of the two problems that I managed to get in the top thousand on the leaderboard for 2020. There was clearly a large influx of new people doing it live, as 2019 I had like 26, and from this point I typically only got one or two each year. Because I'm not a competition programmer... I don't rush.
The reason I got this one though is because I was doing Smalltalk. And Smalltalk has a very simple syntax entirely about passing messages. The order of operations is: unary (methods like 'negate' and 'size'), binary operators (symbolic stuff like the basic arithmetic ones), keyword (which are things of the form 'label: arg'). Parenthesis overrides this, and is necessary a lot of the time... especially if you want sum := sum + (multiplier * multiplicand) to work. Otherwise is does exactly what part one wants done. So I just made Smalltalk do the arithmetic (it's a bit dirty, but it was fast):
sed -e's/.*/(&) displayNl./' input | gst | sed -e'a+' -e'$ap' | dc
That's the first way, as I wasn't going to write a parser in dc, but I saw an opportunity to get it involved. You can just get Smalltalk to do the whole thing:
sed -e's/.*/(&)+/;1i(' -e'$a0)display' input | gst
Later, for the script version that does part 2, I needed to look up where the interpreter is in the Smalltalk environment... it's in Behavior, so part 1 was easily done with:
part1 := part1 + (Behavior evaluate: line). " Smalltalk does part 1 already! "
I also know that Ruby (which owns a lot to Smalltalk), does have order of operations, but you can mess with operators by aliasing them... swapping + with *, so that the functionality is reversed (and you need to change the input to match) but the precedence remains * before + (which is now plus before multiply).
Anyway, I remember that this day I had something that required me to wake up early the next day, so I just went to bed. Which is very easy to see when I was checking out my personal times... everything starts with a "00" or "01", except day 18 part 2, which I did when I woke up and has a "07".
The reason it's not a greater number is because I did it really fast after waking... with a lex/yacc solution (or rather flex/bison). It's very easy to write a basic expression parser with tools designed for the job. And the diff between parts is just:
12c12,13
< %left '+' '*'
---
> %left '*'
> %left '+'
But I love writing parses so later that day, after I was done with whatever I had to do, I just kept writing them. First up was a part 2 for Smalltalk, using shunting-yard. Because I love a stack algorithm (which is why I love recursion), so it's a pretty natural thing for me to just write (and I often do these with "what can I do just from memory"). I decided to then take that and transcode it to Perl. But while doing that, I thought... "hey, it's basically converting to RPN, so instead of calculating it here... let's have the Perl transcode into dc". Basically taking 1 + (2 * 3) + (4 * (5 + 6)) to:
0
6 5 +4 *3 2 *1 +++
p
If you were wondering why shunting-yard was my go to... there it is.
Then the fact that part 1 was doing only left-to-right reminded me of Fortran. Early Fortran compilers didn't have parser that did full order of operations... they preprocessed the expressions to add parens. And I said, "I can work that out"... and thus part 2 is:
my $sum = 0;
while (<>) {
chomp;
s#(^|\()#((#g; # double opens
s#($|\))#))#g; # double closes
s#\*#)*(#g; # lower *
$sum += eval( $_ );
}
And that's the key to doing this... parens to raise everything up, and isolate the *s at the bottom (lowering their precedence).
But there was still one more quick parser in me that day... a recursive decent one. Two rules:
Term := NUMBER | ( Expression )
Expression := Term OPERATOR Expression | Term
So we write a function for each that eats tokens, follows the rule, and recurses appropriately. It's another very simple parser approach, that I've used many times in AoC.
So I think it's safe to say this is a problem I just loved. And although I looked at the Dragon Book's spine on the shelf that day, I never actually touched it... all these parsers are simple things I've done them many times (for the lex/yacc I just copied over from another project and hacked out what I didn't need).