Move to src-php to make room for Go
This commit is contained in:
89
src-php/Commands/AbstractDayCommand.php
Normal file
89
src-php/Commands/AbstractDayCommand.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use trizz\AdventOfCode\Solution;
|
||||
|
||||
abstract class AbstractDayCommand extends Command
|
||||
{
|
||||
protected int $day;
|
||||
|
||||
protected int $year;
|
||||
|
||||
protected string $title;
|
||||
|
||||
protected bool $skipExamples;
|
||||
|
||||
protected int $part;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('day', InputArgument::REQUIRED, 'The day number')
|
||||
->addArgument('year', InputArgument::OPTIONAL, 'The year', date('y'))
|
||||
->addOption('one', '1', null, 'Run only part 1')
|
||||
->addOption('two', '2', null, 'Run only part 2')
|
||||
->addOption('skip-example', 's', null, 'Skip the example data');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->day = $input->getArgument('day');
|
||||
$this->year = $input->getArgument('year');
|
||||
|
||||
$this->title = sprintf("Advent of Code '%d - Day %d", $this->year, $this->day);
|
||||
$this->skipExamples = $input->getOption('skip-example');
|
||||
$this->part = $input->getOption('one') ? 1 : ($input->getOption('two') ? 2 : 0);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln($this->title);
|
||||
$output->writeln(str_repeat('-', strlen($this->title)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
protected function getSolutions(): array
|
||||
{
|
||||
$solution = $this->loadClass();
|
||||
|
||||
// Solve the examples if available.
|
||||
$resultPart1Example = 'n/a';
|
||||
$resultPart2Example = 'n/a';
|
||||
if (!$this->skipExamples && $solution->hasExampleData()) {
|
||||
['part1' => $resultPart1Example, 'part2' => $resultPart2Example] = $solution->results(useExampleData: true, part: $this->part);
|
||||
}
|
||||
|
||||
// Solve the real puzzle if available.
|
||||
$resultPart1 = 'n/a';
|
||||
$resultPart2 = 'n/a';
|
||||
if ($solution->hasData()) {
|
||||
['part1' => $resultPart1, 'part2' => $resultPart2] = $solution->results(useExampleData: false, part: $this->part);
|
||||
}
|
||||
|
||||
return [
|
||||
'part1' => $resultPart1,
|
||||
'part2' => $resultPart2,
|
||||
'part1Example' => $resultPart1Example,
|
||||
'part2Example' => $resultPart2Example,
|
||||
];
|
||||
}
|
||||
|
||||
protected function loadClass(): Solution
|
||||
{
|
||||
require_once sprintf('%s/Y%d/day%d/Day%d.php', DATA_DIR, $this->year, $this->day, $this->day);
|
||||
$className = sprintf('%s\\Y%d\\Day%d', substr(__NAMESPACE__, 0, -9), $this->year, $this->day);
|
||||
|
||||
/** @var Solution $class */
|
||||
$class = new $className();
|
||||
$class->loadData();
|
||||
|
||||
return $class;
|
||||
}
|
||||
}
|
135
src-php/Commands/AddDay.php
Normal file
135
src-php/Commands/AddDay.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode\Commands;
|
||||
|
||||
use Nette\PhpGenerator\ClassType;
|
||||
use Nette\PhpGenerator\PhpNamespace;
|
||||
use Nette\PhpGenerator\Printer;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use trizz\AdventOfCode\Solution;
|
||||
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
final class AddDay extends Command
|
||||
{
|
||||
private string $dataDir;
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('new')
|
||||
->setDescription('Add a new day.')
|
||||
->addArgument('day', InputArgument::OPTIONAL, 'The day number.')
|
||||
->addArgument('year', InputArgument::OPTIONAL, 'The year', date('y'));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$day = $input->getArgument('day');
|
||||
$year = $input->getArgument('year');
|
||||
|
||||
if ($day === null) {
|
||||
$day = text(
|
||||
label: 'For which day?',
|
||||
placeholder: '3',
|
||||
default: date('j'),
|
||||
required: true,
|
||||
validate: static fn ($value): ?string => is_numeric($value) && $value > 0 && $value < 26 ? null : 'Please enter a valid day number.'
|
||||
);
|
||||
}
|
||||
|
||||
$example1Result = text(
|
||||
label: 'Please enter the result for example 1.',
|
||||
required: true,
|
||||
validate: static fn ($value): ?string => is_numeric($value) ? null : 'Please enter a valid number.'
|
||||
);
|
||||
|
||||
// Create short year.
|
||||
$year = strlen((string) $year) === 4 ? substr((string) $year, 2) : $year;
|
||||
$this->dataDir = DATA_DIR.'/Y'.$year.'/day'.$day;
|
||||
$output->writeln(sprintf("Adding files for day %s of year '%s.", $day, $year));
|
||||
|
||||
$this
|
||||
->addDirsAndExampleFiles($year, $day)
|
||||
->createClass($day, $year, $example1Result);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function createClass(string $day, string $year, string $example1Result): self
|
||||
{
|
||||
$phpNamespace = new PhpNamespace('trizz\AdventOfCode\Y'.$year);
|
||||
$classType = new ClassType('Day'.$day);
|
||||
|
||||
$classType->setFinal()->setExtends(Solution::class);
|
||||
|
||||
$this->createClassProperties($classType, $example1Result);
|
||||
$this->createClassMethods($classType);
|
||||
|
||||
$phpNamespace->add($classType);
|
||||
$this->printClassToFile($classType, $year, $day, $phpNamespace);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function addDirsAndExampleFiles(mixed $year, mixed $day): self
|
||||
{
|
||||
if (!is_dir($this->dataDir) && !mkdir($this->dataDir, recursive: true) && !is_dir($this->dataDir)) {
|
||||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $this->dataDir));
|
||||
}
|
||||
|
||||
touch($this->dataDir.'/example.txt');
|
||||
touch($this->dataDir.'/data.txt');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function createClassProperties(ClassType $classType, string $example1Result): void
|
||||
{
|
||||
$properties = [
|
||||
'part1ExampleResult' => $example1Result,
|
||||
'part1Result' => null,
|
||||
'part2ExampleResult' => null,
|
||||
'part2Result' => null,
|
||||
];
|
||||
|
||||
foreach ($properties as $name => $value) {
|
||||
$classType
|
||||
->addProperty($name, $value)
|
||||
->setPublic()
|
||||
->setStatic()
|
||||
->setType('null|int|string');
|
||||
}
|
||||
}
|
||||
|
||||
private function createClassMethods(ClassType $classType): void
|
||||
{
|
||||
$methods = ['part1', 'part2'];
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$classType
|
||||
->addMethod($method)
|
||||
->setReturnType('int')
|
||||
->setBody('return -1;')
|
||||
->addParameter('data')->setType('array');
|
||||
}
|
||||
}
|
||||
|
||||
private function printClassToFile(ClassType $classType, string $year, string $day, PhpNamespace $phpNamespace): void
|
||||
{
|
||||
$printer = new Printer();
|
||||
$printer->indentation = ' ';
|
||||
|
||||
$filename = $this->dataDir.'/Day'.$day.'.php';
|
||||
if (file_exists($filename)) {
|
||||
throw new \RuntimeException(sprintf('File %s already exists.', $filename));
|
||||
}
|
||||
|
||||
file_put_contents($filename, '<?php'.PHP_EOL.$printer->printNamespace($phpNamespace));
|
||||
}
|
||||
}
|
44
src-php/Commands/ExecuteDay.php
Normal file
44
src-php/Commands/ExecuteDay.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class ExecuteDay extends AbstractDayCommand
|
||||
{
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('day')
|
||||
->setDescription('Run day');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$results = $this->getSolutions();
|
||||
|
||||
// Output all the results.
|
||||
if (in_array($this->part, [0, 1], true)) {
|
||||
$output->writeln('<fg=bright-green>Part 1</>');
|
||||
$output->writeln(sprintf(' <fg=blue>Example:</> <comment>%s</comment>', $results['part1Example']));
|
||||
$output->writeln(sprintf(' <fg=blue>Result: </> <comment>%s</comment>', $results['part1']));
|
||||
}
|
||||
|
||||
if ($this->part === 0) {
|
||||
$output->writeln(str_repeat('-', strlen($this->title)));
|
||||
}
|
||||
|
||||
if (in_array($this->part, [0, 2], true)) {
|
||||
$output->writeln('<fg=bright-green>Part 2</>');
|
||||
$output->writeln(sprintf(' <fg=blue>Example:</> <comment>%s</comment>', $results['part2Example']));
|
||||
$output->writeln(sprintf(' <fg=blue>Result: </> <comment>%s</comment>', $results['part2']));
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
47
src-php/Commands/Puzzle.php
Normal file
47
src-php/Commands/Puzzle.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode\Commands;
|
||||
|
||||
use PhpPkg\CliMarkdown\CliMarkdown;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class Puzzle extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('puzzle')
|
||||
->setDescription('Show the puzzle description.')
|
||||
->addArgument('day', InputArgument::REQUIRED, 'The day number.')
|
||||
->addArgument('year', InputArgument::OPTIONAL, 'The year', date('y'));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$contents = file_get_contents(
|
||||
sprintf(
|
||||
'%s/../../data/Y%d/day%s/puzzle.md',
|
||||
__DIR__,
|
||||
$input->getArgument('year'),
|
||||
(int) $input->getArgument('day')
|
||||
)
|
||||
);
|
||||
|
||||
if (!$contents) {
|
||||
$output->writeln('Can not read puzzle.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$rendered = (new CliMarkdown())->render($contents);
|
||||
|
||||
$output->writeln($rendered);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
56
src-php/Commands/TestDay.php
Normal file
56
src-php/Commands/TestDay.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class TestDay extends AbstractDayCommand
|
||||
{
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('test')
|
||||
->setDescription('Test day');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$results = $this->getSolutions();
|
||||
$solution = $this->loadClass();
|
||||
|
||||
if (in_array($this->part, [0, 1], true)) {
|
||||
$output->writeln('<fg=bright-green>Part 1</>');
|
||||
$this->render($output, 'Example', $solution::$part1ExampleResult, $results['part1Example']);
|
||||
$this->render($output, 'Puzzle', $solution::$part1Result, $results['part1']);
|
||||
}
|
||||
|
||||
if ($this->part === 0) {
|
||||
$output->writeln(str_repeat('-', strlen($this->title)));
|
||||
}
|
||||
|
||||
if (in_array($this->part, [0, 2], true)) {
|
||||
$output->writeln('<fg=bright-green>Part 2</>');
|
||||
$this->render($output, 'Example', $solution::$part2ExampleResult, $results['part2Example']);
|
||||
$this->render($output, 'Puzzle', $solution::$part2Result, $results['part2']);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function render(OutputInterface $output, string $title, null|int|string $expected, null|int|string $result): void
|
||||
{
|
||||
if ($title === 'Example' && $this->skipExamples) {
|
||||
return;
|
||||
}
|
||||
|
||||
$color = $expected === $result ? 'green' : 'red';
|
||||
$output->writeln(sprintf(' <fg=blue>%s:</>', $title));
|
||||
$output->writeln(sprintf(' <fg=%s>Expected: </> <comment>%s</comment>', $color, $expected));
|
||||
$output->writeln(sprintf(' <fg=%s>Result: </> <comment>%s</comment>', $color, $result));
|
||||
}
|
||||
}
|
137
src-php/Solution.php
Normal file
137
src-php/Solution.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode;
|
||||
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
|
||||
abstract class Solution
|
||||
{
|
||||
public static null|int|string $part1ExampleResult = null;
|
||||
|
||||
public static null|int|string $part1Result = null;
|
||||
|
||||
public static null|int|string $part2ExampleResult = null;
|
||||
|
||||
public static null|int|string $part2Result = null;
|
||||
|
||||
/**
|
||||
* @var bool When false, do not apply the `array_filter` function when the data is loaded.
|
||||
*/
|
||||
public bool $filterDataOnLoad = true;
|
||||
|
||||
/**
|
||||
* @var string[] The data to use.
|
||||
*
|
||||
* @psalm-suppress PropertyNotSetInConstructor
|
||||
*/
|
||||
public ?array $data = null;
|
||||
|
||||
/**
|
||||
* @var array<array<int, string>|null> The example data to use.
|
||||
*/
|
||||
#[ArrayShape(['part1' => 'array|null', 'part2' => 'array|null', 'global' => 'array|null'])]
|
||||
public array $exampleData = [
|
||||
'global' => null,
|
||||
'part1' => null,
|
||||
'part2' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Solve the given data for part one of the puzzle.
|
||||
*
|
||||
* @param string[] $data The data to process.
|
||||
*
|
||||
* @return int|string The result or null if not (yet?) implemented.
|
||||
*/
|
||||
public function part1(array $data): int|string
|
||||
{
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
/**
|
||||
* Solve the given data for part one of the puzzle.
|
||||
*
|
||||
* @param string[] $data The data to process.
|
||||
*
|
||||
* @return int|string The result or null if not (yet?) implemented.
|
||||
*/
|
||||
public function part2(array $data): int|string
|
||||
{
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
public function loadData(): void
|
||||
{
|
||||
$dataFile = sprintf('%s/../data/Y%d/day%d/data.txt', __DIR__, $this->year(), $this->day());
|
||||
$dataExampleFiles = [
|
||||
'global' => sprintf('%s/../data/Y%d/day%d/example.txt', __DIR__, $this->year(), $this->day()),
|
||||
'part1' => sprintf('%s/../data/Y%d/day%d/example-part1.txt', __DIR__, $this->year(), $this->day()),
|
||||
'part2' => sprintf('%s/../data/Y%d/day%d/example-part2.txt', __DIR__, $this->year(), $this->day()),
|
||||
];
|
||||
|
||||
if (file_exists($dataFile)) {
|
||||
$data = file_get_contents($dataFile);
|
||||
if ($data !== false) {
|
||||
$this->data = $this->filterDataOnLoad ? array_filter(explode(PHP_EOL, $data)) : explode(PHP_EOL, $data);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dataExampleFiles as $type => $filePath) {
|
||||
if (file_exists($filePath)) {
|
||||
$data = file_get_contents($filePath);
|
||||
if ($data !== false) {
|
||||
$this->exampleData[$type] = $this->filterDataOnLoad ? array_filter(explode(PHP_EOL, $data)) : explode(PHP_EOL, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function year(): int
|
||||
{
|
||||
return (int) substr(explode('\\', static::class)[2], 1);
|
||||
}
|
||||
|
||||
public function day(): int
|
||||
{
|
||||
return (int) substr(explode('\\', static::class)[3], 3);
|
||||
}
|
||||
|
||||
public function hasData(): bool
|
||||
{
|
||||
return $this->data !== null && $this->data !== [];
|
||||
}
|
||||
|
||||
public function hasExampleData(): bool
|
||||
{
|
||||
return
|
||||
($this->exampleData['global'] !== null && $this->exampleData['global'] !== [])
|
||||
|| ($this->exampleData['part1'] !== null && $this->exampleData['part1'] !== [])
|
||||
|| ($this->exampleData['part2'] !== null && $this->exampleData['part2'] !== []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{part1: int|string, part2: int|string}
|
||||
*/
|
||||
#[ArrayShape(['part1' => 'int|string', 'part2' => 'int|string'])]
|
||||
public function results(bool $useExampleData = true, int $part = 0): array
|
||||
{
|
||||
return [
|
||||
'part1' => ($part === 1 || $part === 0) ? $this->part1Data($useExampleData) : 'n/a',
|
||||
'part2' => ($part === 2 || $part === 0) ? $this->part2Data($useExampleData) : 'n/a',
|
||||
];
|
||||
}
|
||||
|
||||
public function part1Data(bool $useExampleData = true): int|string
|
||||
{
|
||||
$data = $useExampleData ? ($this->exampleData['part1'] ?? $this->exampleData['global']) : $this->data;
|
||||
|
||||
return $this->part1($data ?? []);
|
||||
}
|
||||
|
||||
public function part2Data(bool $useExampleData = true): int|string
|
||||
{
|
||||
$data = $useExampleData ? ($this->exampleData['part2'] ?? $this->exampleData['global']) : $this->data;
|
||||
|
||||
return $this->part2($data ?? []);
|
||||
}
|
||||
}
|
38
src-php/Utils/Arr.php
Normal file
38
src-php/Utils/Arr.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode\Utils;
|
||||
|
||||
final class Arr
|
||||
{
|
||||
/**
|
||||
* Flatten a multi-dimensional array into a single level.
|
||||
*
|
||||
* Based on:
|
||||
*
|
||||
* @see https://github.com/laravel/framework/blob/c16367a1af68d8f3a1addc1a819f9864334e2c66/src/Illuminate/Collections/Arr.php#L221-L249
|
||||
*
|
||||
* @param array<mixed> $array
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public static function flatten(iterable $array, float|int $depth = INF): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($array as $item) {
|
||||
if (!is_array($item)) {
|
||||
$result[] = $item;
|
||||
} else {
|
||||
$values = $depth === 1
|
||||
? array_values($item)
|
||||
: self::flatten($item, $depth - 1);
|
||||
|
||||
foreach ($values as $value) {
|
||||
$result[] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
35
src-php/Utils/Str.php
Normal file
35
src-php/Utils/Str.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace trizz\AdventOfCode\Utils;
|
||||
|
||||
final class Str
|
||||
{
|
||||
/**
|
||||
* Check if the entirety of string two matches string one.
|
||||
*
|
||||
* @see https://github.com/MueR/adventofcode/blob/master/src/Util/StringUtil.php
|
||||
*/
|
||||
public static function matchesAll(string $one, string $two): bool
|
||||
{
|
||||
for ($index = 0, $length = strlen($two); $index < $length; ++$index) {
|
||||
if (!str_contains($one, $two[$index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alphabetically sort characters in a string.
|
||||
*
|
||||
* @see https://github.com/MueR/adventofcode/blob/master/src/Util/StringUtil.php
|
||||
*/
|
||||
public static function sort(string $string): string
|
||||
{
|
||||
$letters = array_unique(str_split($string));
|
||||
sort($letters, SORT_STRING);
|
||||
|
||||
return implode('', $letters);
|
||||
}
|
||||
}
|
3
src-php/bootstrap.php
Normal file
3
src-php/bootstrap.php
Normal file
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
define('DATA_DIR', dirname(__DIR__).'/data');
|
Reference in New Issue
Block a user