
🔥 매년 12월 1일부터 12월 25일까지 한 문제씩 프로그래밍 퍼즐을 제공하는 사이트다. Eric Wastl이 2015년에 만든 사이트로, 재림절 달력(Advent calendar)을 매일 열어보듯이 크리스마스까지 꾸준히 문제를 해결해나간다는 컨셉이다. 퍼즐은 UTC-5 기준으로 자정, 한국시간으로는 오후 2시에 공개된다.
출처 : 나무위키 - Advent of Code
Advent of Code가 2023년 돌아왔습니다! 25일간 프로그래밍 퍼즐을 풀어봅시다.
Jetbrains는 이번에도 어김없이 Kotlin을 통해 25일간의 AoC에 참가하면 코틀린 독점 상품을 받을 수 있는 기회를 준다고 합니다!
자세한 내용은 코틀린 블로그에 개제된 Tackle Advent of Code 2023 With Kotlin and Win Prizes! 를 참고하세요.
오늘은 첫 번째 문제인 Trewbuchet?! 문제를 풀어봤습니다.
교정 문서라고 불리는 여러 텍스트들의 리스트로 구성되어 있습니다.
이 교정 문서에 있는 여러 텍스트들에는 복구해야 할 특별한 교정 값이 있습니다.
각 텍스트에서 교정 값이란, 해당 문장에 있는 첫 번째 숫자와 마지막 숫자를 합쳐 두 자리 숫자를 만드는 것입니다.
1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet
위와 같은 교정 문서가 있다면, 각각의 텍스트의 교정 숫자는 12, 38, 15, 77이 됩니다. 해당 값들을 모두 더하면 142가 됩니다.
교정 문서에 있는 문장들의 교정 숫자를 모두 더한 값을 반환하세요.
단순히 해당 문자열을 순회해서 맨 앞과 뒤에 있는 숫자를 가져옵니다.
코틀린에서 String.find() 그리고 String.findLast() 에 조건문을 넣어 가져올 수 있습니다.
find에 실패하면 null을 반환하므로 해당 경우에는 0으로 처리 했습니다.
fun part1(input: List<String>): Int {
return input.sumOf {
val first = it.find { it.isDigit() } ?: 0
val last = it.findLast { it.isDigit() } ?: 0
"$first$last".toInt()
}
}
🔥 다른 사람들의 풀이는 KotlinLang Slack에 참여하면 확인할 수 있습니다!

단순히 0~9의 숫자 뿐만 아니라, one, two, three … 와 같은 숫자를 의미하는 영어 단어의 경우에도 교정 값을 의미하고 있다고 합니다.
즉 zoneight234 와 같은 단어는 앞에서 one, 뒤에서 4라는 숫자가 나오고, 교정 숫자는 14가 됩니다.
아까처럼 단순히 코틀린에서 제공해주는 확장 함수를 사용하기에는 어렵습니다. 따라서 직접 순회를 돌면서 문자를 하나하나 저장하면서, 숫자를 의미하는 단어가 포함되어 있는지 판단하도록 로직을 작성했습니다.
/**
* 주어진 String에 one, two와 같은 숫자 단어가 들어가 있는 경우,
* 해당 숫자 단어를 반환하는 확장 함수
*/
fun String.convertStringToNumber(): Int = when {
contains("zero") -> 0
contains("one") -> 1
contains("two") -> 2
contains("three") -> 3
contains("four") -> 4
contains("five") -> 5
contains("six") -> 6
contains("seven") -> 7
contains("eight") -> 8
contains("nine") -> 9
else -> 0
}
fun part2(input: List<String>): Int {
var sum = 0
input.forEach { str ->
val lastIndex = str.lastIndex
var firstTemp = ""
var lastTemp = ""
var isFindFirst = false
var isFindLast = false
str.indices.forEach { idx ->
firstTemp += str[idx]
lastTemp = str[lastIndex - idx] + lastTemp
val firstNum = if (str[idx].isDigit()) str[idx].digitToInt()
else firstTemp.convertStringToNumber()
val lastNum = if (str[lastIndex - idx].isDigit()) str[lastIndex - idx].digitToInt()
else lastTemp.convertStringToNumber()
if (firstNum != 0 && isFindFirst.not()) {
sum += firstNum.times(10)
isFindFirst = true
}
if (lastNum != 0 && isFindLast.not()) {
sum += lastNum
isFindLast = true
}
}
}
return sum
}

mapOrNull 로 순회하면서, 0, 1, 2, 3… 개 만큼 문자열의 앞을 잘라냅니다.abcdefg, bcdefg, cdefg … 이런 식으로 비교 가능합니다.indexOfFirst 에 의해 부분 문자열이 digits 목록의 단어 (숫자를 의미하는 영어 단어 리스트) 에 포함된다면 해당되는 인덱스를 반환, 없다면 -1을 반환합니다.zero 면 인덱스는 0, one 은 인덱스가 1 …?: 이후에 구문이 실행됩니다. 이번에는 문자 자체가 숫자인지 digitToIntOrNull 을 통해 판단합니다.mapNotNull 구문에 의하여 매핑된 리스트에는 포함되지 않습니다.동작 순서를 이해하기 어려울 수 있습니다.
예시로 주어진 문자열 중 하나인 ****zoneight234를 예시로 들어보겠습니다.
zoneight234, oneight234, neight234 … 와 같은 부분 문자열들이 만들어집니다.indexOfFirst 에 의해 부분 문자열이 digits 목록의 단어 (숫자를 의미하는 영어 단어 리스트) 에 포함된다면 해당되는 인덱스를 반환, 없다면 -1을 반환합니다.zoneight234 의 경우에는 어떠한 숫자 단어로도 시작하지 않습니다. -1를 반환합니다.oneight234 의 경우에는 one 으로 시작합니다. 1을 반환합니다.zoneight234 의 경우에는 -1로 반환되었기 때문에 null을 반환합니다.oneight234 의 경우에는 1이 반환되었기 때문에 1을 그대로 반환합니다.?: 이후에 구문이 실행됩니다. 이번에는 문자 자체가 숫자인지 digitToIntOrNull 을 통해 판단합니다.zoneight234 의 경우 첫 번째 문자가 z 입니다. 숫자가 아니므로 null이 반환됩니다.oneight234 의 경우에는 null이 반환되지 않았으므로 해당 로직이 수행되지 않습니다.oneight234 라는 문자열에서는 [1(one), 8(eight), 2, 3, 4] 이라는 숫자 리스트가 만들어집니다.