diff --git a/Tests/src/Console/Commands/ConverterCommandTest.php b/Tests/src/Console/Commands/ConverterCommandTest.php new file mode 100644 index 00000000..e5cccdb7 --- /dev/null +++ b/Tests/src/Console/Commands/ConverterCommandTest.php @@ -0,0 +1,119 @@ +converterCommand = new CommandTester(CondorcetApplication::$SymfonyConsoleApplication->find('convert')); + } + + protected function tearDown(): void + { + file_exists(self::OUTPUT_FILE) && unlink(self::OUTPUT_FILE); + } + + public static function conversionsProvider(): array + { + return [ + 'fromDebianToCondorcet' => ['--from-debian-format', '--to-condorcet-election-format', self::DEBIAN_INPUT_FILE, __DIR__.'/files/fromDebianExpectedFile.cvotes'], + 'fromDavidHillToCondorcet' => ['--from-david-hill-format', '--to-condorcet-election-format', self::DAVIDHILL_INPUT_FILE, __DIR__.'/files/fromDavidHillExpectedFile.cvotes'], + ]; + } + + #[DataProvider('conversionsProvider')] + public function testSuccessfullConversions(string $from, string $to, string $input, string $comparaison): void + { + $this->converterCommand->execute( + [ + $from => true, + $to => true, + 'input' => $input, + 'output' => self::OUTPUT_FILE, + ], + [ + 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, + ] + ); + + $output = $this->converterCommand->getDisplay(); + $this->converterCommand->assertCommandIsSuccessful(); + self::assertSame(0, $this->converterCommand->getStatusCode()); + self::assertEmpty($output); + + self::assertSame(file_get_contents($comparaison), file_get_contents(self::OUTPUT_FILE)); + } + + public function testLacksAnOption(): void + { + $this->expectException(CondorcetInternalException::class); + $this->expectExceptionMessageMatches('/output/'); + + $this->converterCommand->execute( + [ + '--from-debian-format' => true, + // Missing option + 'input' => self::DEBIAN_INPUT_FILE, + 'output' => self::OUTPUT_FILE, + ], + [ + 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, + ] + ); + } + + public function testLacksAnArgument(): void + { + $this->expectException(CondorcetInternalException::class); + $this->expectExceptionMessageMatches('/output/'); + + $this->converterCommand->execute( + [ + '--from-debian-format' => true, + '--to-condorcet-election-format' => true, + 'input' => self::DEBIAN_INPUT_FILE, + // Missing + ], + [ + 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, + ] + ); + } + + public function testWrongInput(): void + { + $this->expectException(CondorcetInternalException::class); + $this->expectExceptionMessage('The input file does not exist'); + + $this->converterCommand->execute( + [ + '--from-debian-format' => true, + '--to-condorcet-election-format' => true, + 'input' => '42.txt', + // Missing + ], + [ + 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, + ] + ); + } +} diff --git a/Tests/src/Console/Commands/ElectionCommandTest.php b/Tests/src/Console/Commands/ElectionCommandTest.php index aa62bdd0..1a681e48 100644 --- a/Tests/src/Console/Commands/ElectionCommandTest.php +++ b/Tests/src/Console/Commands/ElectionCommandTest.php @@ -150,8 +150,8 @@ public function testConsoleMultiplesMethods(): void public function testConsoleFileInput(): void { $this->electionCommand->execute([ - '--candidates' => __DIR__.'/data.candidates', - '--votes' => __DIR__.'/data.votes', + '--candidates' => __DIR__.'/files/data.candidates', + '--votes' => __DIR__.'/files/data.votes', ]); $output = $this->electionCommand->getDisplay(); diff --git a/Tests/src/Console/Commands/data.candidates b/Tests/src/Console/Commands/files/data.candidates similarity index 100% rename from Tests/src/Console/Commands/data.candidates rename to Tests/src/Console/Commands/files/data.candidates diff --git a/Tests/src/Console/Commands/data.votes b/Tests/src/Console/Commands/files/data.votes similarity index 100% rename from Tests/src/Console/Commands/data.votes rename to Tests/src/Console/Commands/files/data.votes diff --git a/Tests/src/Console/Commands/files/fromDavidHillExpectedFile.cvotes b/Tests/src/Console/Commands/files/fromDavidHillExpectedFile.cvotes new file mode 100644 index 00000000..402884ce --- /dev/null +++ b/Tests/src/Console/Commands/files/fromDavidHillExpectedFile.cvotes @@ -0,0 +1,14 @@ +#/Candidates: 1 ; 2 ; 3 +#/Number of Seats: 1 +#/Implicit Ranking: true +#/Weight Allowed: false + +3 * 39 +1 > 3 * 38 +3 > 1 * 36 +3 > 2 * 29 +1 > 2 * 28 +2 > 1 * 15 +1 * 14 +2 > 3 * 9 +2 * 5 \ No newline at end of file diff --git a/Tests/src/Console/Commands/files/fromDebianExpectedFile.cvotes b/Tests/src/Console/Commands/files/fromDebianExpectedFile.cvotes new file mode 100644 index 00000000..19982dc1 --- /dev/null +++ b/Tests/src/Console/Commands/files/fromDebianExpectedFile.cvotes @@ -0,0 +1,65 @@ +#/Candidates: Jonathan Carter ; Sruthi Chandran ; Brian Gupta ; None Of The Above +#/Number of Seats: 1 +#/Implicit Ranking: true +#/Weight Allowed: false + +Jonathan Carter > Sruthi Chandran > Brian Gupta > None Of The Above * 37 +Jonathan Carter > Sruthi Chandran > None Of The Above > Brian Gupta * 36 +Jonathan Carter > Brian Gupta > Sruthi Chandran > None Of The Above * 35 +Jonathan Carter > Brian Gupta > Sruthi Chandran * 17 +Sruthi Chandran > Jonathan Carter > None Of The Above > Brian Gupta * 15 +Jonathan Carter > Brian Gupta = Sruthi Chandran > None Of The Above * 12 +Sruthi Chandran > Jonathan Carter > Brian Gupta > None Of The Above * 12 +Jonathan Carter > None Of The Above > Brian Gupta = Sruthi Chandran * 11 +Jonathan Carter > Brian Gupta > None Of The Above > Sruthi Chandran * 10 +Jonathan Carter > Sruthi Chandran > Brian Gupta * 10 +Jonathan Carter > None Of The Above * 9 +Jonathan Carter > None Of The Above > Brian Gupta > Sruthi Chandran * 9 +Jonathan Carter > None Of The Above > Sruthi Chandran > Brian Gupta * 9 +Jonathan Carter * 8 +Brian Gupta > Jonathan Carter > Sruthi Chandran * 7 +Jonathan Carter > Brian Gupta = Sruthi Chandran * 7 +Brian Gupta > Jonathan Carter > Sruthi Chandran > None Of The Above * 6 +Jonathan Carter = Sruthi Chandran > Brian Gupta > None Of The Above * 6 +Jonathan Carter > Sruthi Chandran * 6 +None Of The Above * 5 +Sruthi Chandran > None Of The Above > Jonathan Carter > Brian Gupta * 5 +Jonathan Carter = Sruthi Chandran > None Of The Above > Brian Gupta * 4 +None Of The Above > Jonathan Carter > Brian Gupta = Sruthi Chandran * 4 +Sruthi Chandran > Jonathan Carter > Brian Gupta * 4 +Brian Gupta > Jonathan Carter > None Of The Above > Sruthi Chandran * 3 +Brian Gupta > Sruthi Chandran > Jonathan Carter * 3 +Jonathan Carter > Brian Gupta * 3 +Jonathan Carter > Sruthi Chandran > None Of The Above * 3 +Brian Gupta = Jonathan Carter = None Of The Above = Sruthi Chandran * 2 +Brian Gupta > Jonathan Carter > None Of The Above * 2 +Brian Gupta > None Of The Above * 2 +Brian Gupta > Sruthi Chandran > Jonathan Carter > None Of The Above * 2 +Brian Gupta > Sruthi Chandran > None Of The Above * 2 +Jonathan Carter > Sruthi Chandran > Brian Gupta = None Of The Above * 2 +None Of The Above > Brian Gupta > Jonathan Carter > Sruthi Chandran * 2 +None Of The Above > Jonathan Carter > Brian Gupta > Sruthi Chandran * 2 +Sruthi Chandran > Brian Gupta > Jonathan Carter > None Of The Above * 2 +Sruthi Chandran > None Of The Above * 2 +Sruthi Chandran > None Of The Above > Brian Gupta = Jonathan Carter * 2 +Brian Gupta * 1 +Brian Gupta = Jonathan Carter * 1 +Brian Gupta = Jonathan Carter = Sruthi Chandran * 1 +Brian Gupta = Jonathan Carter = Sruthi Chandran > None Of The Above * 1 +Brian Gupta = Jonathan Carter > None Of The Above > Sruthi Chandran * 1 +Brian Gupta > Jonathan Carter * 1 +Brian Gupta > Jonathan Carter > None Of The Above = Sruthi Chandran * 1 +Brian Gupta > None Of The Above = Sruthi Chandran > Jonathan Carter * 1 +Brian Gupta > None Of The Above > Jonathan Carter = Sruthi Chandran * 1 +Brian Gupta > None Of The Above > Jonathan Carter > Sruthi Chandran * 1 +Jonathan Carter > None Of The Above = Sruthi Chandran > Brian Gupta * 1 +None Of The Above > Brian Gupta = Jonathan Carter = Sruthi Chandran * 1 +None Of The Above > Brian Gupta > Sruthi Chandran > Jonathan Carter * 1 +None Of The Above > Jonathan Carter * 1 +None Of The Above > Jonathan Carter > Sruthi Chandran > Brian Gupta * 1 +None Of The Above > Sruthi Chandran > Brian Gupta > Jonathan Carter * 1 +Sruthi Chandran * 1 +Sruthi Chandran > Brian Gupta = Jonathan Carter > None Of The Above * 1 +Sruthi Chandran > Brian Gupta > Jonathan Carter * 1 +Sruthi Chandran > Jonathan Carter * 1 +Sruthi Chandran > Jonathan Carter > None Of The Above * 1 \ No newline at end of file diff --git a/src/Console/Commands/ConvertCommand.php b/src/Console/Commands/ConvertCommand.php new file mode 100644 index 00000000..e2ae7745 --- /dev/null +++ b/src/Console/Commands/ConvertCommand.php @@ -0,0 +1,120 @@ +addOption( + name: 'from-'.$converter::COMMAND_LINE_OPTION_NAME, + mode: InputOption::VALUE_NONE, + ); + } + } + + foreach (self::$converters as $converter) { + if (isset(class_implements($converter)[ConverterExport::class])) { + $this->addOption( + name: 'to-'.$converter::COMMAND_LINE_OPTION_NAME, + mode: InputOption::VALUE_NONE, + ); + } + } + + $this + ->addArgument( + name: 'input', + mode: InputArgument::REQUIRED, + description: 'Input file', + ) + ->addArgument( + name: 'output', + mode: InputArgument::REQUIRED, + description: 'Output file', + ); + + } + + public function initialize(InputInterface $input, OutputInterface $output): void + { + // Get converters class + + $this->fromConverter = match (true) { + $input->getOption('from-debian-format') => DebianFormat::class, + $input->getOption('from-david-hill-format') => DavidHillFormat::class, + $input->getOption('from-condorcet-election-format') => CondorcetElectionFormat::class, + + default => throw new CondorcetInternalException('The option defining the input format is missing') + }; + + $this->toConverter = match (true) { + $input->getOption('to-condorcet-election-format') => CondorcetElectionFormat::class, + // $input->getOption('to-civs-format') => CivsFormat::class, + + default => throw new CondorcetInternalException('The option defining the output format is missing') + }; + + // Get Files + $this->input = $input->getArgument('input') ?? throw new CondorcetInternalException('Argument "input" is required'); + $this->input = CommandInputHelper::getFilePath($this->input) ?? throw new CondorcetInternalException('The input file does not exist'); + + $output = CommandInputHelper::isAbsoluteAndExist($input->getArgument('output') ?? throw new CondorcetInternalException('Argument "output" is required')) ? + $input->getArgument('output') : + CommandInputHelper::getFilePath($input->getArgument('output')); + + $this->output = new SplFileObject($input->getArgument('output'), 'w+'); + } + + + public function execute(InputInterface $input, OutputInterface $output): int + { + $this->election = (new $this->fromConverter($this->input))->setDataToAnElection(); + + $this->toConverter::createFromElection(election: $this->election, file: $this->output); + + return Command::SUCCESS; + } +} diff --git a/src/Console/CondorcetApplication.php b/src/Console/CondorcetApplication.php index 8d05067f..48164d74 100644 --- a/src/Console/CondorcetApplication.php +++ b/src/Console/CondorcetApplication.php @@ -12,7 +12,7 @@ namespace CondorcetPHP\Condorcet\Console; use CondorcetPHP\Condorcet\Condorcet; -use CondorcetPHP\Condorcet\Console\Commands\ElectionCommand; +use CondorcetPHP\Condorcet\Console\Commands\{ConvertCommand, ElectionCommand}; use CondorcetPHP\Condorcet\Throwable\Internal\NoGitShellException; use Symfony\Component\Console\Output\AnsiColorMode; use Symfony\Component\Console\{Application as SymfonyConsoleApplication, Terminal}; @@ -36,9 +36,11 @@ public static function create(): true self::$SymfonyConsoleApplication = new SymfonyConsoleApplication('Condorcet', Condorcet::getVersion()); // Election command - $command = new ElectionCommand; - self::$SymfonyConsoleApplication->add($command); - self::$SymfonyConsoleApplication->setDefaultCommand($command->getName(), false); + $defaultCommand = new ElectionCommand; + self::$SymfonyConsoleApplication->add($defaultCommand); + self::$SymfonyConsoleApplication->add(new ConvertCommand); + + self::$SymfonyConsoleApplication->setDefaultCommand($defaultCommand->getName(), false); // Force True color for Docker (or others), based on env getenv('CONDORCET_TERM_ANSI24') && Terminal::setColorMode(AnsiColorMode::Ansi24); diff --git a/src/Console/Helper/CommandInputHelper.php b/src/Console/Helper/CommandInputHelper.php index b50de976..a38aebaf 100644 --- a/src/Console/Helper/CommandInputHelper.php +++ b/src/Console/Helper/CommandInputHelper.php @@ -17,13 +17,18 @@ abstract class CommandInputHelper { public static function getFilePath(string $path): ?string { - if (self::pathIsAbsolute($path) && is_file($path)) { + if (self::isAbsoluteAndExist($path)) { return $path; } else { return (is_file($file = getcwd().\DIRECTORY_SEPARATOR.$path)) ? $file : null; } } + public static function isAbsoluteAndExist(string $path): bool + { + return self::pathIsAbsolute($path) && is_file($path); + } + public static function pathIsAbsolute(string $path): bool { return empty($path) ? false : (strspn($path, '/\\', 0, 1) || (mb_strlen($path) > 3 && ctype_alpha($path[0]) && $path[1] === ':' && strspn($path, '/\\', 2, 1))); diff --git a/src/Tools/Converters/CondorcetElectionFormat.php b/src/Tools/Converters/CondorcetElectionFormat.php index 3099e726..792c4e8a 100644 --- a/src/Tools/Converters/CondorcetElectionFormat.php +++ b/src/Tools/Converters/CondorcetElectionFormat.php @@ -14,11 +14,13 @@ use CondorcetPHP\Condorcet\{Candidate, Election}; use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI, Related}; use CondorcetPHP\Condorcet\Throwable\FileDoesNotExistException; -use CondorcetPHP\Condorcet\Tools\Converters\Interface\ConverterImport; +use CondorcetPHP\Condorcet\Tools\Converters\Interface\{ConverterExport, ConverterImport}; use CondorcetPHP\Condorcet\Utils\CondorcetUtil; -class CondorcetElectionFormat implements ConverterImport +class CondorcetElectionFormat implements ConverterExport, ConverterImport { + public const COMMAND_LINE_OPTION_NAME = 'condorcet-election-format'; + ////// # Static Export Method ////// public const SPECIAL_KEYWORD_EMPTY_RANKING = '/EMPTY_RANKING/'; diff --git a/src/Tools/Converters/DavidHillFormat.php b/src/Tools/Converters/DavidHillFormat.php index 3ffd4851..561717d7 100644 --- a/src/Tools/Converters/DavidHillFormat.php +++ b/src/Tools/Converters/DavidHillFormat.php @@ -17,6 +17,8 @@ class DavidHillFormat implements ConverterImport { + public const COMMAND_LINE_OPTION_NAME = 'david-hill-format'; + protected array $lines; #[PublicAPI] diff --git a/src/Tools/Converters/DebianFormat.php b/src/Tools/Converters/DebianFormat.php index 5a928414..1b41f637 100644 --- a/src/Tools/Converters/DebianFormat.php +++ b/src/Tools/Converters/DebianFormat.php @@ -17,6 +17,8 @@ class DebianFormat implements ConverterImport { + public const COMMAND_LINE_OPTION_NAME = 'debian-format'; + protected array $lines; #[PublicAPI] diff --git a/src/Tools/Converters/Interface/ConverterExport.php b/src/Tools/Converters/Interface/ConverterExport.php new file mode 100644 index 00000000..b776efbc --- /dev/null +++ b/src/Tools/Converters/Interface/ConverterExport.php @@ -0,0 +1,19 @@ +