Skip to content

Commit

Permalink
Console: New Convert command
Browse files Browse the repository at this point in the history
  • Loading branch information
julien-boudry committed May 16, 2023
1 parent 0b27254 commit 8355956
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 9 deletions.
119 changes: 119 additions & 0 deletions Tests/src/Console/Commands/ConverterCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

namespace CondorcetPHP\Condorcet\Tests\Console\Commands;

use PHPUnit\Framework\TestCase;
use CondorcetPHP\Condorcet\Console\CondorcetApplication;
use CondorcetPHP\Condorcet\Throwable\Internal\{CondorcetInternalError, CondorcetInternalException};
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;

class ConverterCommandTest extends TestCase
{
public const DEBIAN_INPUT_FILE = __DIR__.'/../../Tools/Converters/DebianData/leader2020_tally.txt';
public const DAVIDHILL_INPUT_FILE = __DIR__.'/../../Tools/Converters/TidemanData/A77.HIL';

public const OUTPUT_FILE = __DIR__.'/files/out.txt';

private readonly CommandTester $converterCommand;

protected function setUp(): void
{
CondorcetApplication::create();

$this->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,
]
);
}
}
4 changes: 2 additions & 2 deletions Tests/src/Console/Commands/ElectionCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
File renamed without changes.
File renamed without changes.
14 changes: 14 additions & 0 deletions Tests/src/Console/Commands/files/fromDavidHillExpectedFile.cvotes
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions Tests/src/Console/Commands/files/fromDebianExpectedFile.cvotes
Original file line number Diff line number Diff line change
@@ -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
120 changes: 120 additions & 0 deletions src/Console/Commands/ConvertCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
/*
Condorcet PHP - Election manager and results calculator.
Designed for the Condorcet method. Integrating a large number of algorithms extending Condorcet. Expandable for all types of voting systems.
By Julien Boudry and contributors - MIT LICENSE (Please read LICENSE.txt)
https://github.com/julien-boudry/Condorcet
*/

declare(strict_types=1);

namespace CondorcetPHP\Condorcet\Console\Commands;

use CondorcetPHP\Condorcet\Election;
use CondorcetPHP\Condorcet\Console\Helper\CommandInputHelper;
use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption};
use CondorcetPHP\Condorcet\Throwable\Internal\CondorcetInternalException;
use CondorcetPHP\Condorcet\Tools\Converters\{CondorcetElectionFormat, DavidHillFormat, DebianFormat};
use CondorcetPHP\Condorcet\Tools\Converters\Interface\{ConverterExport, ConverterImport};
use SplFileObject;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
name: 'convert',
description: 'Convert an election format input to another format as output',
hidden: false,
)]
class ConvertCommand extends Command
{
public static array $converters = [
CondorcetElectionFormat::class,
DebianFormat::class,
DavidHillFormat::class,
];

protected readonly string $fromConverter;
protected readonly string $toConverter;

protected readonly Election $election;

protected string $input;
protected SplFileObject $output;


protected function configure(): void
{
foreach (self::$converters as $converter) {
if (isset(class_implements($converter)[ConverterImport::class])) {
$this->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;
}
}
10 changes: 6 additions & 4 deletions src/Console/CondorcetApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion src/Console/Helper/CommandInputHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
6 changes: 4 additions & 2 deletions src/Tools/Converters/CondorcetElectionFormat.php
Original file line number Diff line number Diff line change
Expand Up @@ -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/';
Expand Down
Loading

0 comments on commit 8355956

Please sign in to comment.