— Day 4: Secure Container (Part 1) —
Hello – if you’re just visiting this blog for the first time all code can be found on GitHub here. The state of the code at the beginning of this post is release Day3Part2 (as in the end of Day3Part2). You also might like to check out Getting Started – or my solution for Day3Part1 to get up to speed. I hope somebody finds this useful!
So this puzzle looks like it might not be too complex. I’m thinking to write a few test cases for valid passwords. Then loop through all the possible numbers and record those which pass all the tests.
So lets start by setting up a test file – from the instructions it looks like there’s four good tests to start with:
- It is a six-digit number.
- The value is within the range given in your puzzle input.
- Two adjacent digits are the same (like
22
in1<em>22</em>345
). - Going from left to right, the digits never decrease; they only ever increase or stay the same (like
111123
or135679
)
/tests/Day4Part1Test.php
<?php
use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day4\PasswordCracker;
final class Day4Part1Test extends TestCase
{
public function testHasSixDigits()
{
}
public function testIsWithingRange()
{
}
public function testHasTwoAdjacentSameDigits()
{
}
public function testDigitsNeverDecrease()
{
}
}
And let’s fill those out:
<?php
use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day4\PasswordCracker;
final class Day4Part1Test extends TestCase
{
public function testEndToEnd(): void
{
$outputs = [1, 0, 0];
foreach ($outputs as $inputFileNumber => $output) {
$inputFile = 'day4/part1/' . $inputFileNumber;
\PuzzleDebugger::print('TESTING INPUT FILE ' . $inputFile);
$puzzleSolver = new CrossedWiresMeasurer($inputFile);
$puzzleSolver->run();
$this->assertSame($output, $puzzleSolver->getOutput());
}
}
public function testHasSixDigits()
{
$passwordCracker = new PasswordCracker('day4/part1/sample_range');
$passwordCracker->password = '123';
$this->assertFalse($passwordCracker->hasSixDigits());
$passwordCracker->password = '123456';
$this->assertTrue($passwordCracker->hasSixDigits());
}
public function testIsWithingRange()
{
$passwordCracker = new PasswordCracker('day4/part1/sample_range');
$passwordCracker->password = '1';
$this->assertFalse($passwordCracker->isWithinRange());
$passwordCracker->password = '123456';
$this->assertTrue($passwordCracker->isWithinRange());
}
public function testHasTwoAdjacentSameDigits()
{
$passwordCracker = new PasswordCracker('day4/part1/sample_range');
$passwordCracker->password = '123456';
$this->assertFalse($passwordCracker->hasTwoAdjacentSameDigits());
$passwordCracker->password = '223456';
$this->assertTrue($passwordCracker->hasTwoAdjacentSameDigits());
}
public function testDigitsNeverDecrease()
{
$passwordCracker = new PasswordCracker('day4/part1/sample_range');
$passwordCracker->password = '654321';
$this->assertFalse($passwordCracker->hasDigitsNeverDecrease());
$passwordCracker->password = '123456';
$this->assertTrue($passwordCracker->hasDigitsNeverDecrease());
}
}
Now lets create the PasswordCracker class
/src/day4/PasswordCracker.php
<?php
namespace PuzzleSolvers\Day4;
use PuzzleSolvers\PuzzleSolver;
class PasswordCracker extends PuzzleSolver
{
public $lowerRange;
public $upperRange;
public $password;
public function run()
{
}
public function hasSixDigits()
{
}
public function isWithinRange()
{
}
public function hasTwoAdjacentSameDigits()
{
}
public function hasDigitsNeverDecrease()
{
}
}
Next I created a test input file sample_range
/inputs/day4/part1/sample_range
100000-200000
Now let’s run make tests
and get the tests passing one-by-one. Let’s start with hasSixDigits()
public function hasSixDigits()
{
return (strlen($this->password) === 6);
}
That test is now passing. Let’s move on to isWithinRange()
public function __construct($filename)
{
parent::__construct($filename);
$inputPieces = explode('-', $this->inputs[0]);
$this->lowerRange = $inputPieces[0];
$this->upperRange = $inputPieces[1];
}
public function isWithinRange()
{
return ($this->password >= $this->lowerRange && $this->password <= $this->upperRange);
}
That test is now passing. Let’s move on to hasTwoAdjacentSameDigits()
public function hasTwoAdjacentSameDigits()
{
return preg_match('/([0-9])\\1/', $this->password) === 1;
}
The regex here basically just checks for a digit [0-9]
and then the result of that match again directly after \\1
That test is now passing. Let’s move on to hasDigitsNeverDecrease()
public function hasDigitsNeverDecrease()
{
$i = 1;
while ($i < strlen($this->password)) {
if ($this->password[$i] < $this->password[$i -1]) {
return false;
}
$i++;
}
return true;
}
Ok – so make tests
are all passing now. Next we want to loop through all of the inputs and test them for validity. Let’s create our end-to-end tests now, with 3 input files restricting the input range to the provided examples:
/inputs/day4/part1/0
111111-111111
/inputs/day4/part1/1
223450-223450
/inputs/day4/part1/2
123789-123789
public function testEndToEnd(): void
{
$outputs = [1, 0, 0];
foreach ($outputs as $inputFileNumber => $output) {
$inputFile = 'day4/part1/' . $inputFileNumber;
\PuzzleDebugger::print('TESTING INPUT FILE ' . $inputFile);
$puzzleSolver = new PasswordCracker($inputFile);
$puzzleSolver->run();
$this->assertSame($output, $puzzleSolver->getOutput());
}
}
Now let’s implement the logic in run()
public function run() {
$validPasswords = [];
$this->password = $this->lowerRange;
while ($this->password <= $this->upperRange) {
if (
$this->hasSixDigits()
&& $this->isWithinRange()
&& $this->hasTwoAdjacentSameDigits()
&& $this->hasDigitsNeverDecrease()
) {
$validPasswords[] = $this->password;
}
$this->password = (string) ($this->password + 1);
}
$this->output = count($validPasswords);
}
Alright make tests
everything is passing. Note that I am treating password as a string to need to cast it back after adding 1. I think we’re ready to write our solve.php script
/src/day4/solve.php
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use PuzzleSolvers\Day4\PasswordCracker;
$passwordCracker = new PasswordCracker('day4/part1/input');
$passwordCracker->run();
$output = $passwordCracker->getOutput();
echo "Day 4 Part 1 answer: " . $output . "\n";
And don’t forget to grab our input for the puzzle
/inputs/day4/part1/input
109165-576723
Here is the final PasswordCracker class:
<?php
namespace PuzzleSolvers\Day4;
use PuzzleSolvers\PuzzleSolver;
class PasswordCracker extends PuzzleSolver
{
public $lowerRange;
public $upperRange;
public $password;
public function __construct($filename)
{
parent::__construct($filename);
$inputPieces = explode('-', $this->inputs[0]);
$this->lowerRange = $inputPieces[0];
$this->upperRange = $inputPieces[1];
}
public function run()
{
$validPasswords = [];
$invalidPasswords = [];
$this->password = $this->lowerRange;
while ($this->password <= $this->upperRange) {
if (
$this->hasSixDigits()
&& $this->isWithinRange()
&& $this->hasTwoAdjacentSameDigits()
&& $this->hasDigitsNeverDecrease()
) {
$validPasswords[] = $this->password;
} else {
$invalidPasswords[] = $this->password;
}
$this->password = (string) ($this->password + 1);
}
$this->output = count($validPasswords);
}
public function hasSixDigits()
{
return (strlen($this->password) === 6);
}
public function isWithinRange()
{
return ($this->password >= $this->lowerRange && $this->password <= $this->upperRange);
}
public function hasTwoAdjacentSameDigits()
{
return preg_match('/([0-9])\\1/', $this->password) === 1;
}
public function hasDigitsNeverDecrease()
{
$i = 1;
while ($i < strlen($this->password)) {
if ($this->password[$i] < $this->password[$i - 1]) {
return false;
}
$i++;
}
return true;
}
}
Let’s get that gold star!
$ php src/day4/solve.php
Day 4 Part 1 answer: 2814
The final code for this part can be found on GitHub here. Remember my answer may not be the same as yours for your real input. Make sure to replace your /inputs/day4/part1/input
with your own file. Did it work? Let me know!
Next up: Day 4 Part 2