Advent of Code 2019 in SQL
Table of Contents
1 Advent of Code 2019 in SQL
SQL supports recursion in Common Table Expressions (CTEs) and so is Turing complete. If it's Turing complete, we should be able to write any function in SQL, right? Lets put that proposition to the test by implementing Advent of Code 2019 problems in SQL!
2 Day 1: Rocket Fuel
Okay, so in Day 1 we have a rocket fuel equation: for every V units of
payload on a rocket, we require floor(V / 3) - 2
units of fuel.
For Day 1 Part 2, each V units of fuel recursively requires another
floor(V / 3) - 2
units of additional fuel.
select "Part 1", sum(val / 3 - 2) from input_day1; with weight as ( select 0 as is_fuel, val from input_day1 union all select 1, val / 3 - 2 from weight where val > 0 ) select 'Part 2', sum(is_fuel * val) from weight;
Part 1 | 6546942 |
Part 2 | 9814454 |
Okay, that was pretty easy! Maybe writing logic in SQL isn't that bad!
3 Day 2: Virtual Machine Interpreter
Uh oh. This seems a lot more involved than Day 1.
We're given as input an array of numbers. Starting at index 0, we should interpret the first number as an opcode:
- 1 means ADD - add the operands
- 2 means MUL - multiply the operands
- 99 means HALT - stop and do not read any more data.
Next, Three numbers representing the addresses into the array. Two operand values are loaded from the first two addresses, they are added or multiplied, and the result is written to the third address.
For example, the input
1 | 0 | 4 | 0 | 99 |
is $0 <- ADD $0 $4
. The operand values are 1
and 99
and the result 100
is written to index 0
, so the program state becomes
100 | 0 | 4 | 0 | 99 |
The program counter is now pointing to index 4
, which contains 99
, so the program terminates.
In Part 1, Run the given program after putting 12 in index 1 and 2 in index 2, returning the final value in index 0;
In Part 2, figure out for which starting values < 100 at index 1 and 2 result in a final value in index 0 of 19690720
.
3.1 The solution
After writing about 200 lines of really messy SQL and having huge issues debugging, I decided to encode the array as a JSON column, since most SQL databases these days have some kind of JSON format. This way, reading and writing memory is simply calls to json_replace
and json_extract
.
with initial as (select json('[1,0,0,3,1,1,2,3,1,3,4,3,1,5,0,3,2,1,10,19,1,19,6,23,2,23,13,27,1,27,5,31,2,31,10,35,1,9,35,39,1,39,9,43,2,9,43,47,1,5,47,51,2,13,51,55,1,55,9,59,2,6,59,63,1,63,5,67,1,10,67,71,1,71,10,75,2,75,13,79,2,79,13,83,1,5,83,87,1,87,6,91,2,91,13,95,1,5,95,99,1,99,2,103,1,103,6,0,99,2,14,0,0]') as arr), program as ( select -1 gen, 0 pc, json_replace(json_replace(arr, '$[1]', 12), '$[2]', 2) mem from initial union all select gen + 1, pc + 4, json_replace(mem, '$[' || json_extract(mem, '$['||(pc + 3)||']') || ']', case when json_extract(mem, '$['||pc||']') = 1 then json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 1)||']') || ']')+ json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 2)||']') || ']') else json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 1)||']') || ']')* json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 2)||']') || ']') end) from program where json_extract(mem, '$[' || pc || ']') <> 99 ) select 'Part 1', json_extract(mem, '$[0]') from program order by gen desc limit 1; ; with recursive initial0 as (select json('[1,0,0,3,1,1,2,3,1,3,4,3,1,5,0,3,2,1,10,19,1,19,6,23,2,23,13,27,1,27,5,31,2,31,10,35,1,9,35,39,1,39,9,43,2,9,43,47,1,5,47,51,2,13,51,55,1,55,9,59,2,6,59,63,1,63,5,67,1,10,67,71,1,71,10,75,2,75,13,79,2,79,13,83,1,5,83,87,1,87,6,91,2,91,13,95,1,5,95,99,1,99,2,103,1,103,6,0,99,2,14,0,0]') as arr), initial as ( select -1 n, arr mem from initial0 union all select n + 1, json_replace(json_replace(mem, '$[1]', (n + 1) / 100), '$[2]', (n + 1) % 100) from initial where n < 10000 ), program as ( select -1 gen, 0 pc, mem, n from initial union all select gen + 1, pc + 4, json_replace(mem, '$[' || json_extract(mem, '$['||(pc + 3)||']') || ']', case when json_extract(mem, '$['||pc||']') = 1 then json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 1)||']') || ']')+ json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 2)||']') || ']') else json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 1)||']') || ']')* json_extract(mem, '$[' || json_extract(mem, '$['||(pc + 2)||']') || ']') end), n from program where json_extract(mem, '$[' || pc || ']') <> 99 ) select 'Part 2', json_extract(mem, '$[0]'), n from program where json_extract(mem, '$[0]') = 19690720 order by gen desc limit 10000;
Part 1 | 3790645 | |
Part 2 | 19690720 | 6577 |
4 Day 3 - Crossed Wires
Given a series of Directions and Lengths, calculate the positions where two wires cross. Both wires start at the origin, so the crossing represents a loop of wire.
In Part 1, find the closest intersection by manhattan distance from origin.
In Part 2: find the shortest intersection by total wire distance in loop.
.headers on ;with raw_input(wire, input_str) as (select wire, input_str from input_day3 where test_case = 2) ,w1 as ( -- split an entry into direction string and length select wire, substr(input_str, 0, instr(input_str, ',')) d, substr(input_str, instr(input_str, ',')+1) str from raw_input union all select wire, case instr(str, ',') when 0 then str else substr(str, 0, instr(str, ',')) end, case instr(str, ',') when 0 then NULL else substr(str, instr(str, ',') + 1) end from w1 where str is NOT NULL ) , wire as ( select wire, substr(d, 1, 1) dir, cast (substr(d, 2) as int) len, row_number() over () i from w1 ) ,steps as ( -- Expand "D5" into "DDDDD" select i, dir, wire, len from wire union all select i, dir, wire, len - 1 from steps where len > 1) , ordered as ( -- turn directions into vectors select case dir when 'R' then 1 when 'L' then -1 else 0 end dx, case dir when 'U' then 1 when 'D' then -1 else 0 end dy, wire, row_number() over (partition by wire order by i) t from steps ) , positions as ( select t, sum(dx) over (partition by wire order by t) x, sum(dy) over (partition by wire order by t) y, wire from ordered ) select min(a.t + b.t) min_total_steps, min(abs(a.x) + abs(a.y)) min_distance from positions a join positions b on a.wire < b.wire and a.x = b.x and a.y = b.y;
mintotalsteps | mindistance |
3454 | 217 |
5 Day 4 - Secure Container
Given a minimum and maximum of a number range, find all password candidates in that range. A number is a password candidate if:
- It has a pair of repeated digits
- It is nondecreasing
For Part 1: return the number of candidates For Part 2: The pair of repeated digits cannot be part of a longer series of repeats. Return the number of candidates also satisfying this new rule.
5.1 Solution
This seems like a pretty straightforward problem, so I'm going to avoid using JSON and do everything relationally.
with input (minval, maxval) as (values (109165, 576723)), --with input (minval, maxval) as (values (100, 130)), vals as ( select minval n from input union all select n + 1 from vals where n < (select maxval from input) ), digits as ( select n, n / 10 remainder, 0 pos, n % 10 d from vals union all select n, remainder / 10, pos + 1, remainder % 10 d from digits where remainder > 0 ), all_pairs as ( select d1.n, d1.d d1, d2.d d2, d1.pos e1, d2.pos e2 from digits d1 join digits d2 on d1.n = d2.n and d1.pos = d2.pos + 1 ), doubled_pair_part1 as (select distinct n from all_pairs where d1 = d2), doubled_pair_part2 as ( select distinct p1.n from all_pairs p1 left outer join all_pairs p2 on p2.n = p1.n and p2.d1 = p2.d2 and p2.e2 = p1.e1 left outer join all_pairs p3 on p3.n = p1.n and p3.d1 = p3.d2 and p1.e2 = p3.e1 where p1.d1 = p1.d2 and p2.n IS NULL and p3.n is NULL ), decreasing_pair as (select distinct n from all_pairs where d1 > d2) select 'Part 1', count(*) from doubled_pair_part1 dubble where not exists ( select * from decreasing_pair where decreasing_pair.n = dubble.n) union all select 'Part 2', count(*) from doubled_pair_part2 dubble where not exists( select * from decreasing_pair where decreasing_pair.n = dubble.n) ;
Part 1 | 2814 |
Part 2 | 1991 |
6 Conclusions
- ???
- This was fun
- Don't immediately rewrite all your code in SQL just yet
7 Bonus: Mandelbrot
;with x as ( select -1.8 x union all select x + 0.03 from x where x < 0.7), y as ( select -1 y union all select y + 0.1 from y where y < 1), mandelbrot as ( select ' ' c, 0 gen, 0 x, 0 y, x x0, y y0 from x cross join y union all select case when gen >= 30 then '#' else ' ' end, gen + 1, x * x - y * y + x0, 2 * x * y + y0, x0, y0 from mandelbrot where gen <= 30 and x < 2 and y < 2 ), last_entries as ( select c, x0, y0, gen, row_number() over (partition by x0, y0 order by gen desc) rn from mandelbrot ) select '.' || group_concat(c, '') from last_entries where rn = 1 group by y0;
". # " ". ## " ". ####### " ". ####### " ". ### ################### # # " ". ############################## " ". ################################# " ". # ################################### " ". ########### ####################################### " ". ###################################################### " ".##################################################################### " ". ###################################################### " ". ########### ####################################### " ". # #################################### " ". ################################# " ". ############################## " ". ### ################### # # " ". ###### " ". ####### " ". ## " ". # " ". "