Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Step-by-step guide

Step 1 - Algorithm

readLines
  > parsePasswordPolicy
  > countValidPasswords
  • readLines() : open the file and return each line as String
  • parsePasswordPolicy : get a String as arg and return the corresponding data Structure
    • We need to represent the password with its associated policy
  • countValidPasswords : function implementing the validation logic of the password based on the couple : password / policy

Step 2 - What do we need ?

  • Class / Data Class to represent the data structure
  • Companion object
  • Extension functions
  • Scope functions

Let’s learn this quickly

Then go back to main course

Step 3 - simple/naive implementation

  • Read lines from the input file
    File("src/main/kotlin/day2/input.txt")
      .readLines()
    
  • Represent the data structure
    data class PasswordWithPolicy(
      val password: String,
      // Use a range or a Pair
      val range: IntRange,
      val letter: Char
    )
    
  • Parse the password policy from String
    fun parse(line: String) = PasswordWithPolicy(
      password = line.substringAfter(": "),
      letter = line.substringAfter(" ").substringBefore(":").single(),
      range = line.substringBefore(" ").let {
          val (start, end) = it.split("-")
          //instantiate a range
          start.toInt()..end.toInt()
      },
    )
    
  • Where do we need to put the parsing logic ?
    • Companion objects
data class PasswordWithPolicy(
    val password: String,
    val range: IntRange,
    val letter: Char
) {
  companion object {
    fun parse(line: String) = PasswordWithPolicy(
      password = line.substringAfter(": "),
      letter = line.substringAfter(" ").substringBefore(":").single(),
      range = line.substringBefore(" ").let {
        val (start, end) = it.split("-")
        start.toInt()..end.toInt()
      },
    )
  }
}
  • Or Extension functions
    • separate more clearly data from behaviors (functions)
data class PasswordWithPolicy(
    val password: String,
    val range: IntRange,
    val letter: Char
)

fun String.toPasswordPolicy() = PasswordWithPolicy(
    password = this.substringAfter(": "),
    letter = this.substringAfter(" ").substringBefore(":").single(),
    range = this.substringBefore(" ").let {
        val (start, end) = it.split("-")
        start.toInt()..end.toInt()
    },
)

Validate the password

Part 1

  • Write a function to check if a password is valid :
    • We count the number of occurrence of the given letter in the password
    • Then we check if this count is in the Policy range
private fun isValidPart1(passwordWithPolicy: PasswordWithPolicy) =
    // equivalent to : range.first <= x && x <= range.last
    passwordWithPolicy.password.count { it == passwordWithPolicy.letter } in passwordWithPolicy.range

Part 2

  • We need to check that exactly one of the positions (stored in the range) contains the given letter
  • We can use the boolean xor operator for that, which returns true if the operands are different
private fun isValidPart2(passwordWithPolicy: PasswordWithPolicy): Boolean {
    return (passwordWithPolicy.password[passwordWithPolicy.range.first - 1] == passwordWithPolicy.letter) xor
            (passwordWithPolicy.password[passwordWithPolicy.range.last - 1] == passwordWithPolicy.letter)
}
  • We still have some redundancy here
  • Let’s factorize by using an inner function
private fun isValidPart2(passwordWithPolicy: PasswordWithPolicy): Boolean {
    fun isValid(rangeIndex: Int) = passwordWithPolicy.password[rangeIndex - 1] == passwordWithPolicy.letter
    return isValid(passwordWithPolicy.range.first) xor isValid(passwordWithPolicy.range.last)
}

Putting all together

class Challenge {
    private fun isValidPart1(passwordWithPolicy: PasswordWithPolicy) =
        passwordWithPolicy.password.count { it == passwordWithPolicy.letter } in passwordWithPolicy.range

    @Test
    fun part1() {
        val countValidPasswords = File("src/main/kotlin/day2/input.txt")
            .readLines()
            .map { it.toPasswordPolicy() }
            .count { isValidPart1(it) }

        Assertions.assertEquals(622, countValidPasswords)
    }

    private fun isValidPart2(passwordWithPolicy: PasswordWithPolicy): Boolean {
        fun isValid(rangeIndex: Int) = passwordWithPolicy.password[rangeIndex - 1] == passwordWithPolicy.letter
        return isValid(passwordWithPolicy.range.first) xor isValid(passwordWithPolicy.range.last)
    }


    @Test
    fun part2() {
        val countValidPasswords = File("src/main/kotlin/day2/input.txt")
            .readLines()
            .map { it.toPasswordPolicy() }
            .count { isValidPart2(it) }

        Assertions.assertEquals(263, countValidPasswords)
    }
}

Congratulations we have solved the challenge !!!

Step 4 - Refactor / improve our code

class Challenge {
    private fun isValidPart1(passwordWithPolicy: PasswordWithPolicy) =
        passwordWithPolicy.password.count { it == passwordWithPolicy.letter } in passwordWithPolicy.range

    @Test
    fun part1() {
        // Hardcoded path + repeated
        val countValidPasswords = File("src/main/kotlin/day2/input.txt")
            .readLines()
            .map { it.toPasswordPolicy() }
            .count { isValidPart1(it) }

        Assertions.assertEquals(622, countValidPasswords)
    }

    private fun isValidPart2(passwordWithPolicy: PasswordWithPolicy): Boolean {
        fun isValid(rangeIndex: Int) = passwordWithPolicy.password[rangeIndex - 1] == passwordWithPolicy.letter
        return isValid(passwordWithPolicy.range.first) xor isValid(passwordWithPolicy.range.last)
    }


    @Test
    fun part2() {
        // Same pipeline than part 1 (only the count function changed)
        val countValidPasswords = File("src/main/kotlin/day2/input.txt")
            .readLines()
            .map { it.toPasswordPolicy() }
            .count { isValidPart2(it) }

        Assertions.assertEquals(263, countValidPasswords)
    }
}

data class PasswordWithPolicy(
    val password: String,
    val range: IntRange,
    val letter: Char
)

fun String.toPasswordPolicy() = PasswordWithPolicy(
    // Not really sexy
    // Use a regex instead ?
    password = this.substringAfter(": "),
    letter = this.substringAfter(" ").substringBefore(":").single(),
    range = this.substringBefore(" ").let {
        val (start, end) = it.split("-")
        start.toInt()..end.toInt()
    },
)
  • Use a regex to parse the PasswordPolicy
  • We assume that every input is valid
private val regex = Regex("""(\d+)-(\d+) ([a-z]): ([a-z]+)""")
fun String.toPasswordPolicy() =
    regex.matchEntire(this)!!
        .destructured
        .let { (start, end, letter, password) ->
            PasswordWithPolicy(password, start.toInt()..end.toInt(), letter.single())
        }
  • !! is the not-null assertion operator
    • Converts any value to a non-null type and throws an exception if the value is null
    • More info here
  • The destructured property provides components for a destructuring assignment for groups defined in the regular expression
  • We use its result together with let and destruct it inside the lambda expression, defining start, end, letter, and password as parameters

  • Remove duplication by using a Higher Order Function taking the validation function as parameter
private fun countValidPasswords(isValid: (PasswordWithPolicy) -> Boolean): Int {
    return File("src/main/kotlin/day2/input.txt")
        .readLines()
        .map { it.toPasswordPolicy() }
        .count { isValid(it) }
}

Based on JetBrains work available here