Introducing Crunchy Data Warehouse: A next-generation Postgres-native data warehouse. Crunchy Data Warehouse Learn more
Tutorial Instructions
This challenge was actually simpler than the previous day's ones, as we were able to lean heavily on some Postgres features such as:
split_part()
int4range
data type@>
<@
and &&
For Day 4, we are given a text file containing two ranges of numbers on each line, like so:
71-89,66-70
24-70,23-55
19-85,18-86
50-90,50-95
55-55,56-72
3-65,5-66
98-99,66-99
14-67,14-14
4-79,78-79
13-98,10-98
Part one of the puzzle asks us to see which of the lines has one range of numbers completely inside another one. In other words, either all the numbers in the left range are contained by the numbers in the right range, or vice-versa.
This one was very easy, for Postgres contains support for doing operations such
as seeing if one group of things is inside another. As these are ranges of
integers, it seemed best to turn these into int4range
columns, in which we
only provide the start and stop values.
SELECT * FROM aoc_day4 LIMIT 10;
a | b
-------+-------
71-89 | 66-70
24-70 | 23-55
19-85 | 18-86
50-90 | 50-95
55-55 | 56-72
3-65 | 5-66
98-99 | 66-99
14-67 | 14-14
4-79 | 78-79
13-98 | 10-98
Next we need to change each of those rows into an actual range of integers. We
will use the int4range()
function to do this, which expects a bottom range,
and a top range. Because the puzzle also contains items such as 6-6
, we also
need to tell the int4range() function that we want our edges to be inclusive,
which we can do by giving a third argument of []
:
SELECT a, b,
int4range(split_part(a,'-',1)::int, split_part(a,'-',2)::int, '[]') AS aa
,int4range(split_part(b,'-',1)::int, split_part(b,'-',2)::int, '[]') AS bb
FROM aoc_day4 LIMIT 10;
a | b | aa | bb
-------+-------+----------+----------
71-89 | 66-70 | [71,90) | [66,71)
24-70 | 23-55 | [24,71) | [23,56)
19-85 | 18-86 | [19,86) | [18,87)
50-90 | 50-95 | [50,91) | [50,96)
55-55 | 56-72 | [55,56) | [56,73)
3-65 | 5-66 | [3,66) | [5,67)
98-99 | 66-99 | [98,100) | [66,100)
14-67 | 14-14 | [14,68) | [14,15)
4-79 | 78-79 | [4,80) | [78,80)
13-98 | 10-98 | [13,99) | [10,99)
The output of those last two columns looks a little weird, but it does represent
exactly the range we want: inclusive of the "lower bound" number as represented
by a left bracket [
, and exclusive of the "upper bound" number, as shown by a
right parenthesis )
.
Postgres has a lot of support for doing things with ranges, including precisely
what the puzzle is asking for, which is determining if one range fits completely
into another one. For that, we will use the built-in @>
and <@
range functions,
which test if a left range contains the second, and if the left range is
contained by the second. Both of these return true or false, so we get this:
WITH q AS (
SELECT a, b,
int4range(split_part(a,'-',1)::int, split_part(a,'-',2)::int, '[]') AS aa
,int4range(split_part(b,'-',1)::int, split_part(b,'-',2)::int, '[]') AS bb
FROM aoc_day4
)
SELECT *, aa @> bb AS l, aa <@ bb AS m FROM q LIMIT 10;
a | b | aa | bb | l | m
-------+-------+----------+----------+---+---
71-89 | 66-70 | [71,90) | [66,71) | f | f
24-70 | 23-55 | [24,71) | [23,56) | f | f
19-85 | 18-86 | [19,86) | [18,87) | f | t
50-90 | 50-95 | [50,91) | [50,96) | f | t
55-55 | 56-72 | [55,56) | [56,73) | f | f
3-65 | 5-66 | [3,66) | [5,67) | f | f
98-99 | 66-99 | [98,100) | [66,100) | f | t
14-67 | 14-14 | [14,68) | [14,15) | t | f
4-79 | 78-79 | [4,80) | [78,80) | t | f
13-98 | 10-98 | [13,99) | [10,99) | f | t
Now it's just a matter of counting up how many rows satisfy either condition:
WITH q AS (
SELECT a, b,
int4range(split_part(a,'-',1)::int, split_part(a,'-',2)::int, '[]') AS aa
,int4range(split_part(b,'-',1)::int, split_part(b,'-',2)::int, '[]') AS bb
FROM aoc_day4
)
,r AS (SELECT *, aa @> bb AS l, aa <@ bb AS m FROM q)
SELECT count(*) FROM r WHERE l OR m;
count
-------
573
Done with part one! For part two, we need to get a count, but now we need to
know how many of the two sections overlap at all. This was a three-second solve,
as Postgres also has an overlaps operator, so we simply replace our containment
operators with the overlaps operator &&
:
WITH q AS (
SELECT a, b,
int4range(split_part(a,'-',1)::int, split_part(a,'-',2)::int, '[]') AS aa
,int4range(split_part(b,'-',1)::int, split_part(b,'-',2)::int, '[]') AS bb
FROM aoc_day4
)
,r AS (SELECT *, aa && bb AS yes_overlap FROM q)
SELECT count(*) FROM r WHERE yes_overlap;
count
-------
867
That's the end! Onwards to Day 5...
Loading terminal...
Loading terminal...