3 Commits

Author SHA1 Message Date
3e07c8563e Liberally use try, document idea 2024-12-03 20:30:19 +01:00
608f3dc621 Implement 2024 day 3 part 2 in Terraform
No one's going to stop me, not even common sense
2024-12-03 19:33:47 +01:00
ff2e421437 formatting 2024-12-03 19:09:04 +01:00
7 changed files with 113 additions and 11 deletions

49
2024/bonus/README.md Normal file
View File

@@ -0,0 +1,49 @@
# Bonus Challenge
This part of the repo holds the bonus challenge for 2024: implement as much as possible in pure
Terraform.
## Usage
The infrastructure definitions expect the input files to be located at `../inputs/[\d]{2}.txt`.
After storing the input files there, the code should be runnable as follows:
```console
$ terraform init
< a lot of modules being installed >
$ terraform plan
Changes to Outputs:
+ day01_1 = 42
+ day01_2 = 12
+ day02_1 = …
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
```
Note that Terraform may freeze for tens of seconds while it's running the computations. This is
normal, and all Terraform code ought to think a little before doing anything. It would save people
some bad rollbacks.
## Development
I use [Terraform tests](https://developer.hashicorp.com/terraform/language/tests) to automatically
run my terraform code on the sample inputs. It's almost a normal development workflow.
The only rule is that the code should be all terraform, no cheating by shelling out to external
programs. Using providers is allowed, as long as the providers don't actually interact with external
systems and are reasonably self-contained. I will try to limit my use of those regardless.
## Why
DevOps will continue until morale improves. But really, a friend remarked that my Python solutions
were strangely normal for me, so I opted to use a language that I do use professionally.
Terraform is a unique beast. It can do a lot, but it is also very limited, and intentionally so.
There's a standard library of functions you might want to use, but all of them work in strange ways
and there isn't that much to begin with. You can never mutate any variables, you can only declare
new ones. You don't have recursion and your only source of loops are list- and map comprehensions,
or multiple instantiations or a module.
These make for a very constrained programming environment, and constrained programming is fun. It
makes you think outside the box.

View File

@@ -3,20 +3,20 @@ variable "input" {
} }
locals { locals {
cleaned_input = replace(var.input, "/ +/", " ") cleaned_input = replace(var.input, "/ +/", " ")
lines = split("\n", trim(local.cleaned_input, "\n")) lines = split("\n", trim(local.cleaned_input, "\n"))
lines_split = [for line in local.lines: split(" ", line)] lines_split = [for line in local.lines : split(" ", line)]
left = [for line in local.lines_split: parseint(line[0], 10)] left = [for line in local.lines_split : parseint(line[0], 10)]
right = [for line in local.lines_split: parseint(line[1], 10)] right = [for line in local.lines_split : parseint(line[1], 10)]
left_sorted = sort(local.left) left_sorted = sort(local.left)
right_sorted = sort(local.right) right_sorted = sort(local.right)
diffs = [for i in range(length(local.left_sorted)): abs(local.left_sorted[i] - local.right_sorted[i])] diffs = [for i in range(length(local.left_sorted)) : abs(local.left_sorted[i] - local.right_sorted[i])]
counts = {for num in local.right: num => num...} counts = { for num in local.right : num => num... }
matching = [for left in local.left: left * length(lookup(local.counts, left, []))] matching = [for left in local.left : left * try(length(local.counts[left]), 0)]
} }
output "part1" { output "part1" {
@@ -24,5 +24,5 @@ output "part1" {
} }
output "part2" { output "part2" {
value = sum(local.matching) value = sum(local.matching)
} }

View File

@@ -8,7 +8,7 @@ module "is_valid" {
report = concat( report = concat(
count.index > 0 ? slice(var.report, 0, count.index) : [], count.index > 0 ? slice(var.report, 0, count.index) : [],
count.index < length(var.report) - 1 ? slice(var.report, count.index + 1, length(var.report)) : [] try(slice(var.report, count.index + 1, length(var.report)), [])
) )
} }

View File

@@ -4,8 +4,21 @@ variable "input" {
locals { locals {
muls = regexall("mul\\((\\d+),(\\d+)\\)", var.input) muls = regexall("mul\\((\\d+),(\\d+)\\)", var.input)
ops = regexall("(don't\\(\\)|do\\(\\)|mul\\((\\d+),(\\d+)\\))", var.input)
}
module "should_execute" {
count = length(local.ops)
source = "./should_execute"
index = count.index
ops = local.ops
} }
output "part1" { output "part1" {
value = sum([for mul in local.muls : parseint(mul[1], 10) * parseint(mul[0], 10)]) value = sum([for mul in local.muls : parseint(mul[1], 10) * parseint(mul[0], 10)])
} }
output "part2" {
value = sum(module.should_execute[*].value)
}

View File

@@ -0,0 +1,19 @@
variable "ops" {
type = list(list(string))
}
variable "index" {
type = number
}
locals {
is_mul = startswith(var.ops[var.index][0], "mul")
subslice = reverse(slice(var.ops[*][0], 0, var.index))
do_pos = try(index(local.subslice, "do()"), var.index)
dont_pos = try(index(local.subslice, "don't()"), var.index + 1)
}
output "value" {
value = (local.is_mul && local.do_pos < local.dont_pos) ? (parseint(var.ops[var.index][1], 10) * parseint(var.ops[var.index][2], 10)) : 0
}

View File

@@ -36,3 +36,7 @@ module "day03" {
output "day03_1" { output "day03_1" {
value = module.day03.part1 value = module.day03.part1
} }
output "day03_2" {
value = module.day03.part2
}

View File

@@ -57,3 +57,20 @@ run "day3_1" {
error_message = "Part1 output is wrong" error_message = "Part1 output is wrong"
} }
} }
run "day3_2" {
command = plan
module {
source = "./day03"
}
variables {
input = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
}
assert {
condition = output.part2 == 48
error_message = "Part2 output is wrong"
}
}