<Advent of Code 2024 day 3> (spoilers) :thread:
# advent-of-code
a
n
Regex for quick'n'dirty. I'm sure I'll come up with something far more elegant later.
b
same
m
Part 1
Part 2
I always forget if the regex method I'm looking for is called
string.something(regex)
or
regex.something(string)
.
💯 1
m
Once again it pays of knowing how to quickly construct a regex on the fly.
j
regex is the good place to start and to stop today 🙂
❤️ 3
💯 1
r
My part 1 solution is like everyone else's solution I guess. For part 2 I split the string at the
do()
and then remove everything after
don't()
so that I can use the regex again. But it only works if you put everything in one line 😅
Copy code
fun part1(input: List<String>): Int {
    val regEx = """mul\((\d{1,3}),(\d{1,3})\)""".toRegex()
    return input.flatMap { line ->
        regEx.findAll(line).map {
            val (first, second) = it.destructured
            first.toInt() * second.toInt()
        }
    }.sum()
}

fun part2(input: List<String>): Int {
    val regEx = """mul\((\d{1,3}),(\d{1,3})\)""".toRegex()
    return input.flatMap { line ->
        line.split("do()")
    }.map { line ->
        if(line.contains("don't()")) {
            val firstDont = line.indexOf("don't()")
            line.take(firstDont)
        } else {
            line
        }
    }.flatMap { line ->
        regEx.findAll(line).map {
            val (first, second) = it.destructured
            first.toInt() * second.toInt()
        }
    }.sum()
}
o
Initially used a mutable variable for pt2 to get a quick solution, but modified with recursion, I would like to see if there are library functions to help with keeping states like this.
m
@Ozioma Ogbe I think using string literals e.g.
"""string"""
might suit you better, especially when writing regexes
👍 1
j
I wasted lots of time for part1, because the input was split into multiple lines. (facepalm)
plus1 4
n
@Ozioma Ogbe if you use fold you can pass in a tuple holding the running sum, plus state info like whether multiplication is currently enabled.
👏 1
y
I first went with manual state-machine parser and that costed me 15 mins :( Regex solution was much much easier, but I just forgot about existence of regexes for some reason https://github.com/y9san9/aoc24/blob/main/src/main/kotlin/day3/Day3.kt
😄 1
t
I think am a
for
loop guy…
Copy code
private val regEx = "(mul\\((\\d+),(\\d+)\\)|don't\\(\\)|do\\(\\))".toRegex()
private fun part1(): Int {
    var mulSum = 0
    for (mul in regEx.findAll(input)) {
        val (key, x, y) = mul.destructured
        if (key.contains("mul")) {
            mulSum += (x.toInt() * y.toInt())
        }
    }
    return mulSum
}

private fun part2(): Int {
    var mulSum = 0
    var isEnabled = true
    for (mul in regEx.findAll(input)) {
        val (key, x, y) = mul.destructured
        when {
            key == "don't()" -> {
                isEnabled = false
            }

            key == "do()" -> {
                isEnabled = true
            }

            key.contains("mul") && isEnabled -> {
                mulSum += (x.toInt() * y.toInt())
            }
        }
    }
    return mulSum
}
n
@Jonathan Kolberg This is a common refrain. I really think it's easier just to call .lines() when you need lines.
b
odd, i rewrote mine using
fold
and
tailrec
, fold was fastest but i was surprised how much slower tailrec was
m
@bj0 what if you use list instead of sequence and sublist instead of drop?
b
i switched from list to sequence thinking it would speed it up, but i hadn't tried sublist
ok sublist made it much faster, on par with the others
interesting, i would have thought
.drop
on sequence would have been fast
e
Copy code
// String.longs is a helper
fun main() {
    solve("Mull It Over") {
        val input = text
        val regex = Regex("""(mul\(\d+,\d+\))|(do\(\))|(don't\(\))""")
        fun String.calc() = this.longs().zipWithNext().first().let { (a, b) -> a * b }

        part1(161289189) {
            regex.findAll(input)
                .mapNotNull { g -> g.value.takeIf { it.contains("mul") } }
                .sumOf(String::calc)
        }

        part2(83595109) {
            regex
                .findAll(input)
                .map { it.value }
                .fold(0L to true) { acc, next ->
                    when (next) {
                        "don't()" -> acc.first to false
                        "do()" -> acc.first to true
                        else ->
                            (if (acc.second) {
                                next.longs().zipWithNext().first().let { (a, b) -> a * b }
                            } else
                                acc.first) to acc.second
                    }
                }
                .first
        }
    }
}
p
Unbenannt.kt
p
I love named groups, makes regexp tiny bit more bearable for me 😀
Copy code
fun part1(input: List<String>): Int {
        val regex = """mul\((?<first>\d+),(?<second>\d+)\)""".toRegex()
        return input.sumOf { str ->
            regex.findAll(str).sumOf { result ->
                val first = result.groups["first"]!!.value.toInt()
                val second = result.groups["second"]!!.value.toInt()
                first * second
            }
        }
    }
j
I love named groups as well, too bad I always forget the syntax 🙂
also I'm glad for tuning in aoc streams on youtube yesterday and the day before, so I re-learned about
"""...."""
as a nice way to avoid necessity of backslash escaping.
Copy code
val regex2 = Regex("""(mul\((\d{1,3}),(\d{1,3})\))|(do\(\))|(don't\(\))""") // look, ma! no more \\
n
I learned Regex through documentation that was not updated for Java 7, apparently, because until now I was always under the impression that Java (and hence Kotlin) did not support named groups! Kind of a game changer!
m
named groups did change semi recently, I remember doing an AoC a year or two ago where I needed them but they werent present. according to the docs this was kotlin 1.2? am I this old?
p
I took this as an excuse to use when-guards. I'll probably tidy it up later and simplify/remove them though.
j
Finally able to really use the part1 solution in the part2 solution. Regex for part 1, and then split base on do/don’t for part 2
Copy code
fun part1(input: String): Int = Regex("""mul\((\d{1,3}),(\d{1,3})\)""").findAll(input).map { it.destructured }
        .sumOf { (a, b) -> a.toInt() * b.toInt() }

    fun part2(input: String): Int = input.split("do()").map { it.substringBefore("don't()") }.sumOf { part1(it) }
K 1
👍 1
k
For part2, we can also first remove the disabled part then calc the sum of mul():
Got part 1 wrong at first because I didn’t handle multiple lines 🙃
k
👍 1
a
had to step out after part 1. Question: for part 2 - does the "default do()" reset on each line? so if two lines are
Copy code
mul(8,1)don't()mul(5,3)
mul(3,2)do()mul(10,1)
is the answer 8 + 6 + 10 or 8 + 10 ? Answer: the answer is 8 + 10 since the instructions say "enables/disables future
mul
instructions"
not just "for the rest of the line"
@phldavies thanks for bringing up when-guards. I hope it's mentioned in today's stream it's brand new in 2.1.0 (and maybe RC's): What's new in Kotlin 2.1.0 → Guard conditions in when with a subject
k
@Jaap Beetstra Isn't it easy to remove parts with slice and regex that don't need to be calculated?
v
This time I was desperately stuck for a while until realising that the input was multiline. 😅 A single regex plus sequences: https://raw.githubusercontent.com/vkopichenko/Contests/refs/heads/main/adventofcode/src/2024/y24day3.kt
plus1 1
a
I'm a bit disappointed surprised that Mr. Wastl didn't try to give us 4-digit numbers to multiply and force an error - since the instructions say 1-3 digits but everyone doing
d+
instead of
d{1,3}
also get the right answer
m
Introducing gotchas like that is not fun for anyone. The difficulty of the puzzle should be in the puzzle, not technicalities of the wording of the text.
🙌 1
💯 1
There are many ways in which the puzzle could be made harder if needed, without basing the difficulty on a missable detail.
a
Generally, that's nice and I agree to not having such gotchas and keeping the puzzle interesting. But in AoC, there typically are gotchas in the wording. And since it says "where
X
and
Y
are each 1-3 digit numbers"
instead of just "where X and Y are numbers" - odd that it mentions the length so specifically but doesn't use it
m
I assume it mentions it so that you know you can use an
int
(or equivalent), not a
long
or
BigInteger
.
👍 2
It doesn't say the numbers are integers either, so your regex doesn't work either if we want to go that route.
¯\_(ツ)_/¯
k
Final. Slice with Regex in part2 is the best and fastest choice.
a
fair point. buit how often do we get inputs which are safe Int's but the answer needs Long's ?
m
Too often. 😄
😄 3
a
one morning, 6-9 months ago, Mr. Eric's coffee was not fresh 🙃
k
I also did it first with long, but I saw that int would work.
m
That's a safe approach
1
k
I was thinking of doing it with BigInteger 😉 ,but the test is small.
j
I did 2019 completely in BigInteger and although it had its benefits, I never repeated it later.
p
sounds horrible 😄
m
Is it? BigInteger luckily has the overloaded operators. It only sucks in standard API functions that want an Int as input
j
@Karloti Slice with regex looks very nice indeed. I usually avoid regexes (too hard to read for me), that’s why I went with
split
/
substringBefore
; it was more natural for me. But very nice solution with the double regex.
k
@Jaap Beetstra I looked at your solution and assumed it would consume a huge amount of memory, but I'm not entirely sure. There is a possibility that this is not the case
I made the mistake of not reading the file as a text, which required to use jointToString(0
k
@Karloti what if in input there is don't without following do? wouldn't that brake your split regex?
p
~19.5us per evaluation of Part2 - no regex, purely scanning (no new strings)
🙌 1
k
@Jaap Beetstra There won't be a problem.
👍 1
@phldavies This will probably be the fastest solution, even though the others are also of linear complexity.Very good parsing
e
❤️ 1
I started with regex for both parts, but ended up changing part 2 into something simpler with split/substring. runs faster than part 1 blob stuck out tongue
t
I remembered enough RegEx to do this with a minimum of frustration! 🙂CodeBlog
e
It should be noted that today’s input file is spread over multiple lines that must be concatenated.
they don't need to be concatenated if you don't split them in the first place blob think smart (you will need
.toRegex(RegexOption.DOT_MATCHES_ALL)
if you want
.
to include
\n
, or replace it with something else that does, if you want to use that part of the solution)
🙌 1
t
Yeah that’s fair. Let me look at it. Thanks!
e
well it can be considered just a difference in setup. my solutions currently all take a single
String
as input, and most of them do
.linesSequence()
or similar immediately, but in some prior years I did the line splitting outside the solution
b
i just did
findAll
on the raw input line, no splits, joins, or multiline options and it worked
e
it depends on approach. I had no issues either, but I see that Todd's blog post mentions
.*?
which would be a problem
n
@Karloti Your don't().*?do() swallowing pattern inspired me to combine that with Rexegg's Best Regex Trick Ever to make a single pattern that can be summed without splits or keeping track of state. (Edited to fix corner case spotted by @bj0)
Copy code
override fun part2(): Long =
    Regex("""(?s)don't\(\).*?(?:do\(\)|$)|mul\((\d+),(\d+)\)""")
        .findAll(input)
        .sumOf { mr -> (mr.groupValues[1].toLongOrNull() ?: 0L) * (mr.groupValues[2].toLongOrNull() ?: 0L) }
b
@Neil Banman that trick assumes it your input ends with a
do()
enabled block, which mine doesn't
🙌 1
🙌🏻 1
👍 1
i had to modify it to
"...(do\(\)|$)|mul..."
d
That's a great technique
c
Hi all 👋 I just wanted to share my approach for part 02. Basically I used exclusion rather than inclusion. I knew I could take out strings there were in between don't and the next do, so this is the solution:
Copy code
private fun part02(lines: List<String>) {
    val donts = Regex("don't\\(\\)(.*?)do\\(\\)")
    val regex = Regex("mul\\(\\d{1,3},\\d{1,3}\\)")

    // We need to put lines together if we want to make this work.
    val word = StringBuilder()
    lines.forEach { line -> word.append(line) }

    val pattern = Pattern.compile(donts.pattern)
    val processedLine = pattern.matcher(word.toString()).replaceAll("")

    var sum = 0L
    regex.findAll(processedLine).forEach {
        val aha = it.value.substringBefore(",").substringAfter("(").toInt() *
                it.value.substringAfter(",").substringBefore(")").toInt()
        sum += aha
    }
    println(sum)
}
I'm sure it can be improved since this is my first iteration. The tricky with this exercise was definitely considering thes lines as a whole 😬
> that trick assumes it your input ends with a
do()
enabled block, which mine doesn't ‼️ you are right @bj0
n
Because I can't leave well enough alone I applied explicit greedy alternation to my pattern to make my part 2 1/3 faster and 10x more impenetrable. It's a rounding error on one pass but if I run part 2 100,000 times, it goes from 30s to 20s. Just use this very obvious and easy-to-read pattern!
Copy code
(?s)don't\(\)(?:[^d]++|d(?!o\(\)))*+(?:do\(\)|${'$'})|mul\((\d+),(\d+)\)
The problem with the old way is that the "don't-swallower" code is lazy and slow. The
.*?
makes it constantly backtrack to make sure that it lets do() match with the remaining part of the pattern. There's no unique characters to exclude, so it's tough to do anything about it. But we can exclude every character except 'd,' and only then do a negative lookahead to exclude any do()s.
e
BTW you can make your regex (more) readable:
Copy code
val regex = """
    don't\(\)
    (?: [^d]++ | d (?! o\(\) ) )*+
    (?: do\(\) | $ ) |
    mul\(
    (\d+) , (\d+)
    \)
""".toRegex(setOf(RegexOption.COMMENTS, RegexOption.DOT_MATCHES_ALL))
m
at some point I think I'm going to make a 'regex -> class' builder Because a lot of these input parsing puzzles want you to map on operation to whatever it contains. In my somewhat cleaner approach I did this:
Copy code
sealed interface Instruction {
    data object DO : Instruction
    data object DONT : Instruction
    data class MUL(val value: Int) : Instruction
    companion object {
        fun of(matchResult: MatchResult): Instruction {
            val (inst, a, b) = matchResult.destructured
            return when(inst){
                "do()" -> DO
                "don't()" -> DONT
                else -> MUL(a.toInt() * b.toInt())
            }
        }
    }
}
and I remember from Shivers intcode from some years ago, you had to iteratively add more and more instructions
n
@ephemient Thanks, I'd forgotten about that, or rather I never really considered that I could use it outside of building the pattern. I first learned regex while building Word macros in VBA to streamline my then-job of writing administrative decisions. I used the "ignore whitespace" flag in regex101.com a lot to figure out some tricky lookarounds. But VBA regex doesn't support that flag so I made other regex replace patterns to convert them back so that I could use them in my macros.
s
Hi everybody, I was busy so I am still in day 3 part 2, and I used the following approach: Replace everything between "dont()" and "do()" functions, and solve the rest like part 1. However, my code outputs lower result for the input, but the correct result for test input.
Copy code
fun filterEnabledMultiplications(input: String): String {
    val pattern = Regex("""don't\(\).+do\(\)""")
    return input.replace(pattern, "")
}
image.png
e
that is wrong for
Copy code
don't()do()mul(1,1)do()
n
Also wrong for don’t()mul(1,1)<EOF>
💯 1
e
my example makes the answer too low, Niel's example makes the answer too high
n
works()on.contingency?no,money()down()!!
❤️ 1
a
Hi. Been bussy so quite far behind. But perhaps someone here can help me understand why I get the wrong answer for part 2, even though I get the right answer using the test string they provide? I can not figure out what I've done wrong.
Copy code
import java.io.File

fun main() {
    val filename = "src/data.txt"

    val testInput = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
    val testInput2 = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"

    val regex = Regex("mul\\(\\d{1,3},\\d{1,3}\\)")
    val controlRegex = Regex("do\\(\\)|don't\\(\\)")
    val mulAndInstructionsRegex = Regex("mul\\(\\d{1,3},\\d{1,3}\\)|do\\(\\)|don't\\(\\)")

    val fileLines: List<String> = File(filename).readLines()
    val matchesFromFile: List<Int> = fileLines.flatMap { line -> extractMulAndDoMultiplication(line, regex) }
    val matchesFromTestInput: List<Int> = extractMulAndDoMultiplication(testInput, regex)
    println(matchesFromTestInput.sum())
    println("Answer part 1: ${matchesFromFile.sum()}")

    var r = controll(testInput2, mulAndInstructionsRegex, controlRegex)
    val mathcesWithControl: List<Int> = fileLines.flatMap { line -> controll(line, mulAndInstructionsRegex, controlRegex) }
    println(r.sum())
    println("Answer part 2: ${mathcesWithControl.sum()}")
}

fun extractMulAndDoMultiplication(input: String, regex: Regex): List<Int> {
    return regex.findAll(input).toList().map { it.value }.map { match -> doMul(match) }
}

fun doMul(input: String): Int {
    val regex = Regex("\\d+")
    val integersAsList = regex.findAll(input).toList().map { it.value.toInt() }
    return integersAsList[0] * integersAsList[1]
}

fun controll(input: String, regex: Regex, controlRegex: Regex): List<Int> {
    val result: MutableList<Int> = mutableListOf<Int>()
    var canProcess: Boolean = true
    val mulAndInstructions: List<MatchResult> = regex.findAll(input).toList()

    for (matchResult in mulAndInstructions) {
        val value = matchResult.value
        println("Processing instruction: $value, canProcess: $canProcess")
        if (controlRegex.matches(value)) {
            canProcess = value == "do()"
            println("Control instruction: $value, updated canProcess to: $canProcess")
        }else if(canProcess ) {
            result.add(doMul(value))
            println("Performed multiplication for: $value")
        }
    }
    return result
}
Nevermind. When I made everything into just one long string in the data.txt file it started to work
m
#day03 visualization
👍 3