DataWeave scripts used in the adventofcode.com site for 2024.
I used the DataWeave extension for VS Code to create these scripts. Most of them should run in the DataWeave Playground with no issue. However, some of the more complex examples have to run with the DataWeave CLI.
To run any script with the CLI, you can use the following syntax (or use my ProstDev Tools VSCode extension):
dw run -i payload=<path to payload file> -f <path to transform.dwl file>
For example:
dw run -i payload=scripts/day1/part1/inputs/payload.csv -f scripts/day1/part1/transform.dwl
If there's no input, just remove the -i payload=<file>
part.
Tip
Check out Ryan's private leaderboard!
Challenge: Historian Hysteria
Example input:
3 4
4 3
2 5
1 3
3 9
3 3
Within each pair, figure out how far apart the two numbers are; you'll need to add up all of those distances. For example, if you pair up a 3 from the left list with a 7 from the right list, the distance apart is 4; if you pair up a 9 with a 3, the distance apart is 6.
To find the total distance between the left list and the right list, add up the distances between all of the pairs you found. In the example above, this is 2 + 1 + 0 + 1 + 2 + 5, a total distance of 11!
Script
%dw 2.0
input payload application/csv separator=" ", header=false
output application/json
var a = payload.column_0 orderBy $
var b = payload.column_3 orderBy $
---
(0 to sizeOf(a)-1) map (abs(a[$] - b[$])) then sum($)
This time, you'll need to figure out exactly how often each number from the left list appears in the right list. Calculate a total similarity score by adding up each number in the left list after multiplying it by the number of times that number appears in the right list.
So, for these example lists, the similarity score at the end of this process is 31 (9 + 4 + 0 + 0 + 9 + 9).
Script
import countBy from dw::core::Arrays
var p = read(payload, "application/csv", {header:false,separator:" "})
var a = p.column_0
var b = p.column_3
---
a map ((item) ->
(b countBy ($==item)) * item
) then sum($)
Challenge: Red-Nosed Reports
Example input:
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9
The engineers are trying to figure out which reports are safe. The Red-Nosed reactor safety systems can only tolerate levels that are either gradually increasing or gradually decreasing. So, a report only counts as safe if both of the following are true:
- The levels are either all increasing or all decreasing.
- Any two adjacent levels differ by at least one and at most three.
So, in this example, 2 reports are safe.
Script
import every, countBy from dw::core::Arrays
var decreasing = "decreasing"
var increasing = "increasing"
---
((payload splitBy "\n") map (
(($ splitBy " ") reduce ((number, a=[]) ->
if (isEmpty(a)) a+{
prevNum: number,
operation: null,
isSafe: true
} else (a[-1].operation match {
case null -> a+{
prevNum: number,
operation: if (a[-1].prevNum-number > 0) decreasing else increasing,
isSafe: [1, 2, 3] contains abs(a[-1].prevNum - number)
}
case "$(decreasing)" -> a+{
prevNum: number,
operation: a[-1].operation,
isSafe: [1, 2, 3] contains (a[-1].prevNum - number)
}
else -> a+{
prevNum: number,
operation: a[-1].operation,
isSafe: [1, 2, 3] contains (number - a[-1].prevNum)
}
})
)).isSafe every $
)) countBy $
Now, the same rules apply as before, except if removing a single level from an unsafe report would make it safe, the report instead counts as safe.
Thanks to the Problem Dampener, 4 reports are actually safe!
Horrible code. But I did what I had to do :(
Script
import every, countBy from dw::core::Arrays
var decreasing = "decreasing"
var increasing = "increasing"
var newp = ((payload splitBy "\n") map (
(($ splitBy " ") reduce ((number, a=[]) ->
if (isEmpty(a)) a+{
prevNum: number,
operation: null,
isSafe: true
} else (a[-1].operation match {
case null -> a+{
prevNum: number,
operation: if (a[-1].prevNum-number > 0) decreasing else increasing,
isSafe: [1, 2, 3] contains abs(a[-1].prevNum - number)
}
case "$(decreasing)" -> a+{
prevNum: number,
operation: a[-1].operation,
isSafe: [1, 2, 3] contains (a[-1].prevNum - number)
}
else -> a+{
prevNum: number,
operation: a[-1].operation,
isSafe: [1, 2, 3] contains (number - a[-1].prevNum)
}
})
))
))
var safeOnes = newp filter ((item) -> item.isSafe every $)
var unsafeOnes = newp -- safeOnes
fun getScenarios(data) = data map ($ reduce ((number, a=[]) ->
if (isEmpty(a)) a+{
prevNum: number,
operation: null,
isSafe: true
} else (a[-1].operation match {
case null -> a+{
prevNum: number,
operation: if (a[-1].prevNum-number > 0) decreasing else increasing,
isSafe: [1, 2, 3] contains abs(a[-1].prevNum - number)
}
case "$(decreasing)" -> a+{
prevNum: number,
operation: a[-1].operation,
isSafe: [1, 2, 3] contains (a[-1].prevNum - number)
}
else -> a+{
prevNum: number,
operation: a[-1].operation,
isSafe: [1, 2, 3] contains (number - a[-1].prevNum)
}
})
))
---
unsafeOnes map ((firstArray) ->
getScenarios(firstArray.prevNum map ((number, numIndex) ->
firstArray.prevNum filter ((item, index) -> index != numIndex)
)) filter ((item) -> item.isSafe every $)
) filter (!isEmpty($))
then sizeOf($)+sizeOf(safeOnes)
Challenge: Mull It Over
Example inputs:
xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))
xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))
It seems like the goal of the program is just to multiply some numbers. It does that with instructions like mul(X,Y), where X and Y are each 1-3 digit numbers. For instance, mul(44,46) multiplies 44 by 46 to get a result of 2024. Similarly, mul(123,4) would multiply 123 by 4.
However, because the program's memory has been corrupted, there are also many invalid characters that should be ignored, even if they look like part of a mul instruction. Sequences like mul(4*, mul(6,9!, ?(12,34), or mul ( 2 , 4 ) do nothing.
Adding up the result of each instruction produces 161 (24 + 55 + 118 + 85).
Script
(flatten(payload scan /mul\(\d+,\d+\)/)) map do {
var nums = flatten($ scan /\d+/)
---
nums[0] * nums[1]
} then sum($)
There are two new instructions you'll need to handle:
- The do() instruction enables future mul instructions.
- The don't() instruction disables future mul instructions.
Only the most recent do() or don't() instruction applies. At the beginning of the program, mul instructions are enabled.
This time, the sum of the results is 48 (2*4 + 8*5)
.
Script
(payload scan /(mul|don't|do)\(\d*,?\d*\)/) reduce ((op, a={r:0,"do":true}) ->
op[0][0 to 2] match {
case "mul" -> do {
var nums = flatten(op[0] scan /\d+/)
var newR = a.r + ((nums[0] default 0) * (nums[1] default 0))
---
{
r: if (a."do") newR else a.r,
"do": a."do"
}
}
case "don" -> { r: a.r, "do": false }
else -> { r: a.r, "do": true }
}
) then $.r
Challenge: Ceres Search
Example input:
MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX
As the search for the Chief continues, a small Elf who lives on the station tugs on your shirt; she'd like to know if you could help her with her word search (your puzzle input). She only has to find one word: XMAS.
This word search allows words to be horizontal, vertical, diagonal, written backwards, or even overlapping other words. It's a little unusual, though, as you don't merely need to find one instance of XMAS - you need to find all of them.
In this word search, XMAS occurs a total of 18 times.
Script
var lines = payload splitBy "\n"
var XMAS = "XMAS"
fun getLetter(x:Number,y:Number) = if ((x<0) or (y<0)) "" else (lines[x][y] default "")
fun getStr(str:String,x:Number,y:Number) = if ((x<0) or (y<0)) "" else (str[x to y])
---
flatten
(lines map ((lineStr, lineidx) ->
(lineStr splitBy "") map ((letter, letteridx) ->
if (letter=="X") (
// right
(if (XMAS == getStr(lineStr,letteridx,letteridx+3)) 1 else 0)
// left
+ (if (XMAS == getStr(lineStr,letteridx,letteridx-3)) 1 else 0)
// down
+ (if (XMAS == (letter ++ getLetter(lineidx+1,letteridx) ++ getLetter(lineidx+2,letteridx) ++ getLetter(lineidx+3,letteridx))) 1 else 0)
// // up
+ (if (XMAS == (letter ++ getLetter(lineidx-1,letteridx) ++ getLetter(lineidx-2,letteridx) ++ getLetter(lineidx-3,letteridx))) 1 else 0)
// down-right
+ (if (XMAS == (letter ++ getLetter(lineidx+1,letteridx+1) ++ getLetter(lineidx+2,letteridx+2) ++ getLetter(lineidx+3,letteridx+3))) 1 else 0)
// down-left
+ (if (XMAS == (letter ++ getLetter(lineidx+1,letteridx-1) ++ getLetter(lineidx+2,letteridx-2) ++ getLetter(lineidx+3,letteridx-3))) 1 else 0)
// up-right
+ (if (XMAS == (letter ++ getLetter(lineidx-1,letteridx+1) ++ getLetter(lineidx-2,letteridx+2) ++ getLetter(lineidx-3,letteridx+3))) 1 else 0)
// up-left
+ (if (XMAS == (letter ++ getLetter(lineidx-1,letteridx-1) ++ getLetter(lineidx-2,letteridx-2) ++ getLetter(lineidx-3,letteridx-3))) 1 else 0)
)
else 0
)
)) then sum($)
The Elf looks quizzically at you. Did you misunderstand the assignment?
Looking for the instructions, you flip over the word search to find that this isn't actually an XMAS puzzle; it's an X-MAS puzzle in which you're supposed to find two MAS in the shape of an X. One way to achieve that is like this:
M.S
.A.
M.S
In this example, an X-MAS appears 9 times.
Script
var lines = payload splitBy "\n"
fun getLetter(x:Number,y:Number) = if ((x<0) or (y<0)) "" else (lines[x][y] default "")
var xmas = ["MAS", "SAM"]
---
flatten
(lines map ((lineStr, lineidx) ->
(lineStr splitBy "") map ((letter, letteridx) ->
if (letter=="A") do {
var topleft = getLetter(lineidx-1, letteridx-1)
var topright = getLetter(lineidx-1, letteridx+1)
var bottomleft = getLetter(lineidx+1, letteridx-1)
var bottomright = getLetter(lineidx+1, letteridx+1)
var cross1 = topleft ++ letter ++ bottomright
var cross2 = topright ++ letter ++ bottomleft
---
if ( (xmas contains cross1) and (xmas contains cross2) ) 1 else 0
}
else 0
)
)) then sum($)
Challenge: Print Queue
Example input:
47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13
75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47
The first section specifies the page ordering rules, one per line. The first rule, 47|53, means that if an update includes both page number 47 and page number 53, then page number 47 must be printed at some point before page number 53. (47 doesn't necessarily need to be immediately before 53; other pages are allowed to be between them.)
The second section specifies the page numbers of each update. Because most safety manuals are different, the pages needed in the updates are different too. The first update, 75,47,61,53,29, means that the update consists of page numbers 75, 47, 61, 53, and 29.
For some reason, the Elves also need to know the middle page number of each update being printed. Because you are currently only printing the correctly-ordered updates, you will need to find the middle page number of each correctly-ordered update.
These have middle page numbers of 61, 53, and 29 respectively. Adding these page numbers together gives 143.
Script
import every from dw::core::Arrays
import substringBefore, substringAfter from dw::core::Strings
var p = payload splitBy "\n\n"
var orderingRules = p[0]
var updatesLines = p[1]splitBy "\n"
fun flatScan(a,b) = flatten(a scan b)
---
updatesLines map ((line, lineidx) -> do {
var arr = (line splitBy ",")
var isCorrect = (arr map ((num, numindex) ->
((orderingRules flatScan "$(num)\|\d+|\d+\|$(num)") map (
((arr[numindex+1 to -1] default "") contains ($ substringBefore "|"))
or ((if(numindex==0) "" else arr[numindex-1 to 0]) contains ($ substringAfter "|"))
)) every (!$)
)) every ($)
---
if (isCorrect) arr[round(sizeOf(arr)/2)-1] as Number
else 0
}) then sum($)
For each of the incorrectly-ordered updates, use the page ordering rules to put the page numbers in the right order. For the above example, here are the three incorrectly-ordered updates and their correct orderings:
- 75,97,47,61,53 becomes 97,75,47,61,53.
- 61,13,29 becomes 61,29,13.
- 97,13,75,29,47 becomes 97,75,47,29,13.
After taking only the incorrectly-ordered updates and ordering them correctly, their middle page numbers are 47, 29, and 47. Adding these together produces 123.
Challenge: Guard Gallivant
Example input:
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
The map shows the current position of the guard with ^ (to indicate the guard is currently facing up from the perspective of the map). Any obstructions - crates, desks, alchemical reactors, etc. - are shown as #.
Lab guards in 1518 follow a very strict patrol protocol which involves repeatedly following these steps:
- If there is something directly in front of you, turn right 90 degrees.
- Otherwise, take a step forward.
In this example, the guard will visit 41 distinct positions on your map.
Script
output application/json
type Coordinates = {x:Number,y:Number}
var matrix = (payload splitBy "\n") map ($ splitBy "")
var guardPositions = ["^", "<", ">", "v"]
fun getChar(matrix:Array<Array<String>>,y:Number,x:Number):String = if ((x<0) or (y<0)) "" else (matrix[y][x] default "")
fun turnGuard(guard:String):String = guard match {
case "^" -> ">"
case ">" -> "v"
case "v" -> "<"
case "<" -> "^"
}
fun getInFrontCoord(guard:String,y:Number,x:Number):Coordinates = guard match {
case "^" -> {y:y-1,x:x}
case ">" -> {y:y,x:x+1}
case "<" -> {y:y,x:x-1}
case "v" -> {y:y+1,x:x}
}
fun getRoute(matrix:Array, coords=[]) = flatten(matrix map ((line, lineindex) ->
flatten(line map ((char, charindex) -> do {
@Lazy
var inFrontCoords = getInFrontCoord(char, lineindex, charindex)
@Lazy
var inFrontChar = getChar(matrix,inFrontCoords.y, inFrontCoords.x)
@Lazy
var guardCoords:Coordinates = {x:charindex,y:lineindex}
---
if (guardPositions contains char) inFrontChar match {
case "." -> getRoute(matrix update {
case c at [inFrontCoords.y][inFrontCoords.x] -> char
case g at [guardCoords.y][guardCoords.x] -> "."
}, coords + guardCoords)
case "#" -> getRoute(matrix update {
case g at [guardCoords.y][guardCoords.x] -> turnGuard(char)
}, coords)
else -> coords
}
else null
}))
))
---
sizeOf(getRoute(matrix) distinctBy $)
Adding a single new obstruction won't cause a time paradox. They'd like to place the new obstruction in such a way that the guard will get stuck in a loop, making the rest of the lab safe to search.
To have the lowest chance of creating a time paradox, The Historians would like to know all of the possible positions for such an obstruction. The new obstruction can't be placed at the guard's starting position - the guard is there right now and would notice.
In the above example, there are only 6 different positions where a new obstruction would cause the guard to get stuck in a loop.
You need to get the guard stuck in a loop by adding a single new obstruction. How many different positions could you choose for this obstruction?
Challenge: Bridge Repair
Example input:
190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20
Operators are always evaluated left-to-right, not according to precedence rules. Furthermore, numbers in the equations cannot be rearranged. Glancing into the jungle, you can see elephants holding two different types of operators: add (+) and multiply (*).
Only three of the above equations can be made true by inserting operators:
- 190: 10 19 has only one position that accepts an operator: between 10 and 19. Choosing + would give 29, but choosing * would give the test value (10 * 19 = 190).
- 3267: 81 40 27 has two positions for operators. Of the four possible configurations of the operators, two cause the right side to match the test value: 81 + 40 * 27 and 81 * 40 + 27 both equal 3267 (when evaluated left-to-right)!
- 292: 11 6 16 20 can be solved in exactly one way: 11 + 6 * 16 + 20.
The engineers just need the total calibration result, which is the sum of the test values from just the equations that could possibly be true. In the above example, the sum of the test values for the three equations listed above is 3749.
Script
%dw 2.0
import drop from dw::core::Arrays
import lines from dw::core::Strings
fun flatScan(a, b) = flatten(a scan b)
fun getResults(values, r=0) = do {
var this = values[0]
var next = values[1]
var newValues = values drop 1
---
if (isEmpty(next)) r
else if (r==0) flatten([getResults(newValues, this+next), getResults(newValues, this*next)])
else flatten([getResults(newValues, r+next), getResults(newValues, r*next)])
}
---
lines(payload) map ((equation, equationIndex) -> do {
var nums = equation flatScan /\d+/
var result = nums[0] as Number
var values = nums[1 to -1] map ($ as Number)
---
if (getResults(values) contains result) result else 0
}) then sum($)
The concatenation operator (||) combines the digits from its left and right inputs into a single number. For example, 12 || 345 would become 12345. All operators are still evaluated left-to-right.
Now, apart from the three equations that could be made true using only addition and multiplication, the above example has three more equations that can be made true by inserting operators:
- 156: 15 6 can be made true through a single concatenation: 15 || 6 = 156.
- 7290: 6 8 6 15 can be made true using 6 * 8 || 6 * 15.
- 192: 17 8 14 can be made true using 17 || 8 + 14.
Adding up all six test values (the three that could be made before using only + and * plus the new three that can now be made by also using ||) produces the new total calibration result of 11387.
Script
%dw 2.0
import drop from dw::core::Arrays
import lines from dw::core::Strings
fun flatScan(a, b) = flatten(a scan b)
fun getResults(values, r=0) = do {
var this = values[0]
var next = values[1]
var newValues = values drop 1
---
if (isEmpty(next)) r
else if (r==0) flatten([getResults(newValues, this+next), getResults(newValues, this*next), getResults(newValues, "$this$next" as Number)])
else flatten([getResults(newValues, r+next), getResults(newValues, r*next), getResults(newValues, "$r$next" as Number)])
}
---
lines(payload) map ((equation, equationIndex) -> do {
var nums = equation flatScan /\d+/
var result = nums[0] as Number
var values = nums[1 to -1] map ($ as Number)
---
if (getResults(values) contains result) result else 0
}) then sum($)
Challenge: Resonant Collinearity
Example input:
............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............
Scanning across the city, you find that there are actually many such antennas. Each antenna is tuned to a specific frequency indicated by a single lowercase letter, uppercase letter, or digit.
The signal only applies its nefarious effect at specific antinodes based on the resonant frequencies of the antennas. In particular, an antinode occurs at any point that is perfectly in line with two antennas of the same frequency - but only when one of the antennas is twice as far away as the other. This means that for any pair of antennas with the same frequency, there are two antinodes, one on either side of them.
Because the topmost A-frequency antenna overlaps with a 0-frequency antinode, there are 14 total unique locations that contain an antinode within the bounds of the map.
How many unique locations within the bounds of the map contain an antinode?
Challenge: Disk Fragmenter
Example input:
2333133121414131402
The disk map uses a dense format to represent the layout of files and free space on the disk. The digits alternate between indicating the length of a file and the length of free space.
So, a disk map like 12345 would represent a one-block file, two blocks of free space, a three-block file, four blocks of free space, and then a five-block file. A disk map like 90909 would represent three nine-block files in a row (with no free space between them).
Each file on disk also has an ID number based on the order of the files as they appear before they are rearranged, starting with ID 0. So, the disk map 12345 has three files: a one-block file with ID 0, a three-block file with ID 1, and a five-block file with ID 2.
Script
import divideBy from dw::core::Arrays
var p = (payload splitBy "")
fun repeat(text: String, times: Number): Array =
if(times <= 0) [] else (1 to times) map text
var files:Array = flatten((p divideBy 2) map ((item, index) ->
repeat(index, item[0]) ++ repeat(".", item[1] default 0)
))
var filesClean:Array = files - "."
var thisthing = (files reduce ((item, acc={ r:[], idx:-1 }) -> item match {
case "." -> {
r: acc.r + filesClean[acc.idx],
idx: acc.idx - 1
}
else -> {
r: acc.r + item,
idx: acc.idx
}
}))
---
thisthing.r[0 to thisthing.idx] map ($*$$) then sum($)
The eager amphipod already has a new plan: rather than move individual blocks, he'd like to try compacting the files on his disk by moving whole files instead.
This time, attempt to move whole files to the leftmost span of free space blocks that could fit the file. Attempt to move each file exactly once in order of decreasing file ID number starting with the file with the highest file ID number. If there is no span of free space to the left of a file that is large enough to fit the file, the file does not move.
The process of updating the filesystem checksum is the same; now, this example's checksum would be 2858.
Challenge: Hoof It
Example input:
89010123
78121874
87430965
96549874
45678903
32019012
01329801
10456732
The topographic map indicates the height at each position using a scale from 0 (lowest) to 9 (highest).
Based on un-scorched scraps of the book, you determine that a good hiking trail is as long as possible and has an even, gradual, uphill slope. For all practical purposes, this means that a hiking trail is any path that starts at height 0, ends at height 9, and always increases by a height of exactly 1 at each step. Hiking trails never include diagonal steps - only up, down, left, or right (from the perspective of the map).
This larger example has 9 trailheads. Considering the trailheads in reading order, they have scores of 5, 6, 5, 3, 1, 3, 5, 3, and 5. Adding these scores together, the sum of the scores of all trailheads is 36.
Script
var lines = payload splitBy "\n"
fun getChar(x:Number,y:Number):String = if ((x<0) or (y<0)) "" else (lines[x][y] default "")
fun getRoutes(num:Number,numidx:Number,lineidx:Number) = do {
var nextNum = num+1
---
if (num ~= 9) [{x:numidx,y:lineidx}]
else
(if (getChar(lineidx-1,numidx) ~= nextNum) getRoutes(nextNum,numidx,lineidx-1) else [])
++
(if (getChar(lineidx+1,numidx) ~= nextNum) getRoutes(nextNum,numidx,lineidx+1) else [])
++
(if (getChar(lineidx,numidx-1) ~= nextNum) getRoutes(nextNum,numidx-1,lineidx) else [])
++
(if (getChar(lineidx,numidx+1) ~= nextNum) getRoutes(nextNum,numidx+1,lineidx) else [])
}
---
flatten(lines map ((line, lineidx) ->
(line splitBy "") map ((num, numidx) ->
num match {
case "0" -> sizeOf(getRoutes(0,numidx,lineidx) distinctBy $)
else -> 0
}
)
)) then sum($)
The paper describes a second way to measure a trailhead called its rating. A trailhead's rating is the number of distinct hiking trails which begin at that trailhead.
Considering its trailheads in reading order, they have ratings of 20, 24, 10, 4, 1, 4, 5, 8, and 5. The sum of all trailhead ratings in this larger example topographic map is 81.
Script
var lines = payload splitBy "\n"
fun getChar(x:Number,y:Number):String = if ((x<0) or (y<0)) "" else (lines[x][y] default "")
fun getRoutes(num:Number,numidx:Number,lineidx:Number) = do {
var nextNum = num+1
---
if (num ~= 9) 1
else
(if (getChar(lineidx-1,numidx) ~= nextNum) getRoutes(nextNum,numidx,lineidx-1) else 0)
+
(if (getChar(lineidx+1,numidx) ~= nextNum) getRoutes(nextNum,numidx,lineidx+1) else 0)
+
(if (getChar(lineidx,numidx-1) ~= nextNum) getRoutes(nextNum,numidx-1,lineidx) else 0)
+
(if (getChar(lineidx,numidx+1) ~= nextNum) getRoutes(nextNum,numidx+1,lineidx) else 0)
}
---
flatten(lines map ((line, lineidx) ->
(line splitBy "") map ((num, numidx) ->
num match {
case "0" -> getRoutes(0,numidx,lineidx)
else -> 0
}
)
)) then sum($)
Challenge: Plutonian Pebbles
Example input:
Initial arrangement:
125 17
After 1 blink:
253000 1 7
After 2 blinks:
253 0 2024 14168
After 3 blinks:
512072 1 20 24 28676032
After 4 blinks:
512 72 2024 2 0 2 4 2867 6032
After 5 blinks:
1036288 7 2 20 24 4048 1 4048 8096 28 67 60 32
After 6 blinks:
2097446912 14168 4048 2 0 2 4 40 48 2024 40 48 80 96 2 8 6 7 6 0 3 2
At first glance, they seem like normal stones: they're arranged in a perfectly straight line, and each stone has a number engraved on it.
The strange part is that every time you blink, the stones change.
Sometimes, the number engraved on a stone changes. Other times, a stone might split in two, causing all the other stones to shift over a bit to make room in their perfectly straight line.
As you observe them for a while, you find that the stones have a consistent behavior. Every time you blink, the stones each simultaneously change according to the first applicable rule in this list:
- If the stone is engraved with the number 0, it is replaced by a stone engraved with the number 1.
- If the stone is engraved with a number that has an even number of digits, it is replaced by two stones. The left half of the digits are engraved on the new left stone, and the right half of the digits are engraved on the new right stone. (The new numbers don't keep extra leading zeroes: 1000 would become stones 10 and 0.)
- If none of the other rules apply, the stone is replaced by a new stone; the old stone's number multiplied by 2024 is engraved on the new stone.
No matter how the stones change, their order is preserved, and they stay on their perfectly straight line.
In this example, after blinking six times, you would have 22 stones. After blinking 25 times, you would have 55312 stones!
Script
fun removeExtraZeros(num:String):String = num as Number as String
fun blink(arr) = flatten(arr map ((num, numidx) ->
num match {
case "0" -> "1"
case n if isEven(sizeOf(n)) -> do {
var i = sizeOf(n)/2
---
[removeExtraZeros(n[0 to i-1]),removeExtraZeros(n[i to -1])]
}
else -> ($ * 2024) as String
}
))
fun blinkTimes(arr,times:Number=1) = times match {
case 0 -> arr
else -> blink(arr) blinkTimes times-1
}
---
sizeOf((payload splitBy " ") blinkTimes 25)
The Historians sure are taking a long time. To be fair, the infinite corridors are very large.
How many stones would you have after blinking a total of 75 times?
Script
output application/json
fun removeExtraZeros(num:String):String = num as Number as String
fun blink(obj:Object) = namesOf(obj) reduce ((item, result={}) -> do {
var itemValue = obj."$item"
---
item match {
case "0" -> result update {
case new at ."1"! -> (new default 0) + itemValue
}
case n if isEven(sizeOf(n)) -> do {
var i = sizeOf(n)/2
var n1 = removeExtraZeros(n[0 to i-1])
var n2 = removeExtraZeros(n[i to -1])
var isSame = n1 == n2
---
if (isSame) result update {
case new at ."$n1"! -> (new default 0) + (itemValue * 2)
}
else result update {
case new1 at ."$n1"! -> (new1 default 0) + itemValue
case new2 at ."$n2"! -> (new2 default 0) + itemValue
}
}
else -> result update {
case new at ."$($ * 2024)"! -> (new default 0) + itemValue
}
}
})
fun blinkTimes(obj:Object,times:Number) = times match {
case 0 -> obj
else -> blink(obj) blinkTimes times-1
}
fun arrToObj(arr:Array,result={}) = arr match {
case [head ~ tail] -> tail arrToObj (result update {
case x at ."$head"! -> (x default 0) + 1
})
case [] -> result
}
---
arrToObj(payload splitBy " ") blinkTimes 75
then sum(valuesOf($))
The Historians sure are taking a long time. To be fair, the infinite corridors are very large.
How many stones would you have after blinking a total of 75 times?
Challenge: Garden Groups
Example input:
RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE
Each garden plot grows only a single type of plant and is indicated by a single letter on your map. When multiple garden plots are growing the same type of plant and are touching (horizontally or vertically), they form a region.
In order to accurately calculate the cost of the fence around a single region, you need to know that region's area and perimeter.
The area of a region is simply the number of garden plots the region contains. The perimeter of a region is the number of sides of garden plots in the region that do not touch another garden plot in the same region.
Due to "modern" business practices, the price of fence required for a region is found by multiplying that region's area by its perimeter.
It contains:
- A region of R plants with price 12 * 18 = 216.
- A region of I plants with price 4 * 8 = 32.
- A region of C plants with price 14 * 28 = 392.
- A region of F plants with price 10 * 18 = 180.
- A region of V plants with price 13 * 20 = 260.
- A region of J plants with price 11 * 20 = 220.
- A region of C plants with price 1 * 4 = 4.
- A region of E plants with price 13 * 18 = 234.
- A region of I plants with price 14 * 22 = 308.
- A region of M plants with price 5 * 12 = 60.
- A region of S plants with price 3 * 8 = 24.
So, it has a total price of 1930.
Challenge: LAN Party
Example input:
kh-tc
qp-kh
de-cg
ka-co
yn-aq
qp-ub
cg-tb
vc-aq
tb-ka
wh-tc
yn-cg
kh-ub
ta-co
de-co
tc-td
tb-wq
wh-td
ta-ka
td-qp
aq-cg
wq-ub
ub-vc
de-ta
wq-aq
wq-vc
wh-yn
ka-de
kh-ta
co-tc
wh-qp
tb-vc
td-yn
The network map provides a list of every connection between two computers.
Each line of text in the network map represents a single connection; the line kh-tc represents a connection between the computer named kh and the computer named tc. Connections aren't directional; tc-kh would mean exactly the same thing.
Start by looking for sets of three computers where each computer in the set is connected to the other two computers.
Find all the sets of three inter-connected computers. How many contain at least one computer with a name that starts with t? The answer is 7.
Script
import lines from dw::core::Strings
output application/json
var connections = lines(payload) map ($ splitBy "-") reduce ((item, a={}) ->
a update {
case x at ."$(item[0])"! -> (a[item[0]] default []) + item[1]
case y at ."$(item[1])"! -> (a[item[1]] default []) + item[0]
}
)
var matches = flatten(namesOf(connections) map ((computer1) ->
flatten(connections[computer1] map ((computer2) -> do {
var threeMatchesOne = (connections[computer2] filter ((computer3) -> connections[computer3] contains computer1))
---
if (!isEmpty(threeMatchesOne))
threeMatchesOne filter ((computer3) -> (computer1 startsWith "t") or (computer2 startsWith "t") or (computer3 startsWith "t"))
map ((computer3) ->
[computer1, computer2, computer3] orderBy $
)
else []
}))
)) distinctBy $
---
sizeOf(matches)
Since it doesn't seem like any employees are around, you figure they must all be at the LAN party. If that's true, the LAN party will be the largest set of computers that are all connected to each other. That is, for each computer at the LAN party, that computer will have a connection to every other computer at the LAN party.
In the above example, the largest set of computers that are all connected to each other is made up of co, de, ka, and ta. Each computer in this set has a connection to every other computer in the set.
The LAN party posters say that the password to get into the LAN party is the name of every computer at the LAN party, sorted alphabetically, then joined together with commas. (The people running the LAN party are clearly a bunch of nerds.) In this example, the password would be co,de,ka,ta.
Challenge: Crossed Wires
Example input:
x00: 1
x01: 0
x02: 1
x03: 1
x04: 0
y00: 1
y01: 1
y02: 1
y03: 1
y04: 1
ntg XOR fgs -> mjb
y02 OR x01 -> tnw
kwq OR kpj -> z05
x00 OR x03 -> fst
tgd XOR rvg -> z01
vdt OR tnw -> bfw
bfw AND frj -> z10
ffh OR nrd -> bqk
y00 AND y03 -> djm
y03 OR y00 -> psh
bqk OR frj -> z08
tnw OR fst -> frj
gnj AND tgd -> z11
bfw XOR mjb -> z00
x03 OR x00 -> vdt
gnj AND wpb -> z02
x04 AND y00 -> kjc
djm OR pbm -> qhw
nrd AND vdt -> hwm
kjc AND fst -> rvg
y04 OR y02 -> fgs
y01 AND x02 -> pbm
ntg OR kjc -> kwq
psh XOR fgs -> tgd
qhw XOR tgd -> z09
pbm OR djm -> kpj
x03 XOR y03 -> ffh
x00 XOR y04 -> ntg
bfw OR bqk -> z06
nrd XOR fgs -> wpb
frj XOR qhw -> z04
bqk OR frj -> z07
y03 OR x01 -> nrd
hwm AND bqk -> z03
tgd XOR rvg -> z12
tnw OR pbm -> gnj
The device seems to be trying to produce a number through some boolean logic gates. Each gate has two inputs and one output. The gates all operate on values that are either true (1) or false (0).
- AND gates output 1 if both inputs are 1; if either input is 0, these gates output 0.
- OR gates output 1 if one or both inputs is 1; if both inputs are 0, these gates output 0.
- XOR gates output 1 if the inputs are different; if the inputs are the same, these gates output 0.
Combining the bits from all wires starting with z produces the binary number 0011111101000. Converting this number to decimal produces 2024.
Script
import fromBinary from dw::core::Numbers
import lines from dw::core::Strings
output application/json
var splitPayload = payload splitBy "\n\n"
var wires:Object = (lines(splitPayload[0]) map ($ splitBy ": ")) reduce ((item, a={}) ->
a ++ {
(item[0]): item[1] ~= 1
}
)
fun getWiresObject(gates:Array<String>, wiresRec) = {
(gates map ((gate) -> do {
@Lazy
var split = gate splitBy " "
@Lazy
var wire1 = split[0]
@Lazy
var wire2 = split[2]
@Lazy
var wire3 = split[-1]
---
if (!isEmpty(wiresRec[wire1]) and !isEmpty(wiresRec[wire2])) {
(wire3): split[1] match {
case "AND" -> wiresRec[wire1] and wiresRec[wire2]
case "OR" -> wiresRec[wire1] or wiresRec[wire2]
case "XOR" -> wiresRec[wire1] != wiresRec[wire2]
}
}
else {
keepTrying: gate
}
}))
}
fun getWiresTailRec(gates:Array<String>, wiresRec=wires) = do {
var result = getWiresObject(gates, wiresRec)
var newGates = result.*keepTrying
var newWires = wiresRec ++ result
---
if (isEmpty(newGates)) newWires
else getWiresTailRec(newGates, newWires - "keepTrying")
}
---
getWiresTailRec(lines(splitPayload[1]))
filterObject ($$ startsWith "z")
orderBy $$
mapObject (($$): if ($) 1 else 0)
then fromBinary(valuesOf($)[-1 to 0] joinBy "")
(explanation is too long and confusing x-x it's easier to refer to the website)
Challenge: Code Chronicle
Example input:
#####
.####
.####
.####
.#.#.
.#...
.....
#####
##.##
.#.##
...##
...#.
...#.
.....
.....
#....
#....
#...#
#.#.#
#.###
#####
.....
.....
#.#..
###..
###.#
###.#
#####
.....
.....
.....
#....
#.#..
#.#.#
#####
The locks are schematics that have the top row filled (#) and the bottom row empty (.); the keys have the top row empty and the bottom row filled. If you look closely, you'll see that each schematic is actually a set of columns of various heights, either extending downward from the top (for locks) or upward from the bottom (for keys).
For locks, those are the pins themselves; you can convert the pins in schematics to a list of heights, one per column. For keys, the columns make up the shape of the key where it aligns with pins; those can also be converted to a list of heights.
In this example, converting both locks to pin heights produces:
- 0,5,3,4,3
- 1,2,0,5,3
Converting all three keys to heights produces:
- 5,0,2,1,3
- 4,3,4,0,2
- 3,0,2,0,1
Then, you can try every key with every lock:
- Lock 0,5,3,4,3 and key 5,0,2,1,3: overlap in the last column.
- Lock 0,5,3,4,3 and key 4,3,4,0,2: overlap in the second column.
- Lock 0,5,3,4,3 and key 3,0,2,0,1: all columns fit!
- Lock 1,2,0,5,3 and key 5,0,2,1,3: overlap in the first column.
- Lock 1,2,0,5,3 and key 4,3,4,0,2: all columns fit!
- Lock 1,2,0,5,3 and key 3,0,2,0,1: all columns fit!
So, in this example, the number of unique lock/key pairs that fit together without overlapping in any column is 3.
Script
import every, countBy from dw::core::Arrays
output application/json
fun getMap(arr) = arr map ((item) -> do {
var lines = (item splitBy "\n")[1 to -2]
---
(0 to sizeOf(lines)-1) map (
sum(lines map ((line) ->
if (line[$] == "#") 1 else 0
))
)
})
var arr = (payload splitBy "\n\n")
var locks = getMap(arr filter ($ startsWith "#####"))
var keys = getMap(arr filter ($ startsWith "....."))
---
sum(locks map ((lock, locki) ->
(keys map ((key, keyi) ->
(key map ((keypin, keypini) ->
5-keypin >= lock[keypini]
)) every $
)) countBy $
))
Need to finish the rest of the challenges to be unlocked x-x