1
0
This commit is contained in:
2022-09-21 14:01:45 +02:00
parent 0a30f39eb4
commit cfee4d93d0
349 changed files with 4 additions and 4 deletions

View File

@ -0,0 +1,3 @@
* Logo 73c: https://patorjk.com/software/taag/#p=display&f=Slant&t=Condorcet%20PHP _(default)_
* Logos 90c && 125c: https://patorjk.com/software/taag/#p=display&h=1&f=Lean&t=Condorcet%20PHP _(fitted)_

View File

@ -0,0 +1,7 @@
<condor1> _/_/_/ _/ _/ </><condor2> _/_/_/ _/ _/ _/_/_/</>
<condor1> _/ _/_/ _/_/_/ _/_/_/ _/_/ _/ _/_/ _/_/_/ _/_/ _/_/_/_/ </><condor2> _/ _/ _/ _/ _/ _/</>
<condor1> _/ _/ _/ _/ _/ _/ _/ _/ _/ _/_/ _/ _/_/_/_/ _/ </><condor2> _/_/_/ _/_/_/_/ _/_/_/</>
<condor1>_/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ </><condor2> _/ _/ _/ _/</>
<condor1> _/_/_/ _/_/ _/ _/ _/_/_/ _/_/ _/ _/_/_/ _/_/_/ _/_/ </><condor2> _/ _/ _/ _/</>
<condor1>========================================================================================== </><condor2> ===============================</>

View File

@ -0,0 +1,5 @@
<condor1> ______ __ __ </><condor2> ____ __ ______ </>
<condor1> / ____/___ ____ ____/ /___ _____________ / /_</><condor2> / __ \/ / / / __ \ </>
<condor1> / / / __ \/ __ \/ __ / __ \/ ___/ ___/ _ \/ __/</><condor2> / /_/ / /_/ / /_/ /</>
<condor1>/ /___/ /_/ / / / / /_/ / /_/ / / / /__/ __/ /_ </><condor2> / ____/ __ / ____/ </>
<condor1>\____/\____/_/ /_/\__,_/\____/_/ \___/\___/\__/ </><condor2>/_/ /_/ /_/_/ </>

View File

@ -0,0 +1,7 @@
<condor1> _/_/_/ _/ _/ </>
<condor1> _/ _/_/ _/_/_/ _/_/_/ _/_/ _/ _/_/ _/_/_/ _/_/ _/_/_/_/</>
<condor1> _/ _/ _/ _/ _/ _/ _/ _/ _/ _/_/ _/ _/_/_/_/ _/ </>
<condor1>_/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ </>
<condor1> _/_/_/ _/_/ _/ _/ _/_/_/ _/_/ _/ _/_/_/ _/_/_/ _/_/ </>
<condor1>==========================================================================================</>

View File

@ -0,0 +1,803 @@
<?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\Algo\Tools\StvQuotas;
use CondorcetPHP\Condorcet\Constraints\NoTie;
use CondorcetPHP\Condorcet\DataManager\DataHandlerDrivers\PdoDriver\PdoHandlerDriver;
use CondorcetPHP\Condorcet\{Condorcet, Election};
use CondorcetPHP\Condorcet\Algo\StatsVerbosity;
use CondorcetPHP\Condorcet\Console\Helper\{CommandInputHelper, FormaterHelper};
use CondorcetPHP\Condorcet\Console\Style\CondorcetStyle;
use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption};
use CondorcetPHP\Condorcet\Throwable\{FileDoesNotExistException, VoteConstraintException};
use CondorcetPHP\Condorcet\Throwable\Internal\CondorcetInternalException;
use CondorcetPHP\Condorcet\Timer\{Chrono, Manager};
use Symfony\Component\Console\Helper\{Table, TableSeparator, TableStyle};
use CondorcetPHP\Condorcet\Tools\Converters\{CondorcetElectionFormat, DavidHillFormat, DebianFormat};
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\{CompletionInput, CompletionSuggestions};
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
use Symfony\Component\Yaml\Yaml;
#[AsCommand(
name: 'election',
description: 'Process an election',
hidden: false,
aliases: ['condorcet']
)]
class ElectionCommand extends Command
{
protected ?Election $election;
protected ?string $candidates;
protected ?string $votes;
protected bool $displayMethodsStats = false;
protected ?string $CondorcetElectionFormatPath;
protected ?string $DebianFormatPath;
protected ?string $DavidHillFormatPath;
public static int $VotesPerMB = 100;
protected string $iniMemoryLimit;
protected int $maxVotesInMemory;
// Internal Process
protected bool $candidatesListIsWrite = false;
protected bool $votesCountIsWrite = false;
protected bool $pairwiseIsWrite = false;
public ?string $SQLitePath = null;
// TableFormat & Terminal
protected Terminal $terminal;
protected CondorcetStyle $io;
// Debug
public static ?string $forceIniMemoryLimitTo = null;
protected Manager $timer;
protected function configure(): void
{
$this->setHelp('This command takes candidates and votes as input. The output is the result of that election.')
->addOption(
name: 'candidates',
shortcut: 'c',
mode: InputOption::VALUE_REQUIRED,
description: 'Candidates list file path or direct input',
)
->addOption(
name: 'votes',
shortcut: 'w',
mode: InputOption::VALUE_REQUIRED,
description: 'Votes list file path or direct input',
)
->addOption(
name: 'import-condorcet-election-format',
mode: InputOption::VALUE_REQUIRED,
description: 'File path. Setup an election and his data from a Condorcet Election file as defined as standard on https://github.com/CondorcetPHP/CondorcetElectionFormat . Other parameters from the command line argument have the priority if set. Other votes can be added with the --vote argument, other candidates can\'t be added.',
)
->addOption(
name: 'import-debian-format',
mode: InputOption::VALUE_REQUIRED,
description: 'File path. Setup an election and his data from a Debian tally file. Other votes can be added with the --vote argument, other candidates can\'t be added.',
)
->addOption(
name: 'import-david-hill-format',
mode: InputOption::VALUE_REQUIRED,
description: 'File path. Setup an election and his data from a Debian tally file. Other votes can be added with the --vote argument, other candidates can\'t be added.',
)
->addOption(
name: 'stats',
shortcut: 's',
mode: InputOption::VALUE_NONE,
description: 'Get detailed stats (equivalent to --show-pairwise and --method-stats)',
)
->addOption(
name: 'method-stats',
mode: InputOption::VALUE_NONE,
description: 'Get detailed stats per method',
)
->addOption(
name: 'show-pairwise',
shortcut: 'p',
mode: InputOption::VALUE_NONE,
description: 'Get pairwise computation',
)
->addOption(
name: 'list-votes',
shortcut: 'l',
mode: InputOption::VALUE_NONE,
description: 'List registered votes',
)
->addOption(
name: 'natural-condorcet',
shortcut: 'r',
mode: InputOption::VALUE_NONE,
description: 'Print natural Condorcet winner / loser',
)
->addOption(
name: 'deactivate-implicit-ranking',
shortcut: 'i',
mode: InputOption::VALUE_NONE,
description: 'Deactivate implicit ranking',
)
->addOption(
name: 'allows-votes-weight',
shortcut: 'g',
mode: InputOption::VALUE_NONE,
description: 'Allows vote weight',
)
->addOption(
name: 'no-tie',
shortcut: 't',
mode: InputOption::VALUE_NONE,
description: 'Add no-tie constraint for vote',
)
->addOption(
name: 'seats',
mode: InputOption::VALUE_REQUIRED,
description: 'Specify the number of seats for proportional methods',
)
->addOption(
name: 'quota',
mode: InputOption::VALUE_REQUIRED,
description: 'Quota to be used for STV compatible methods',
)
->addOption(
name: 'deactivate-file-cache',
mode: InputOption::VALUE_NONE,
description: "Don't use a disk cache for very large elections. Forces to work exclusively in RAM.",
)
->addOption(
name: 'votes-per-mb',
mode: InputOption::VALUE_REQUIRED,
description: 'Adjust memory in case of failure. Default is 100. Try to lower it.',
)
->addArgument(
name: 'methods',
mode: InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
description: 'Methods to output',
)
;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('quota')) {
$r = [];
foreach (StvQuotas::cases() as $case) {
$r[] = $case->name;
}
$suggestions->suggestValues($r);
} elseif ($input->mustSuggestArgumentValuesFor('methods')) {
$suggestions->suggestValues(Condorcet::getAuthMethods());
}
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
// Initialize Style & Terminal
$this->io = new CondorcetStyle($input, $output);
$this->terminal = new Terminal;
// Setup Timer Manager
$this->timer = new Manager;
// Setup Memory
if ($input->getOption('votes-per-mb') && ($NewVotesPerMB = (int) $input->getOption('votes-per-mb')) >= 1) {
self::$VotesPerMB = $NewVotesPerMB;
}
// Setup Election Object
$this->election = new Election;
$this->displayMethodsStats = $input->getOption('method-stats') || $input->getOption('stats');
$this->election->setStatsVerbosity($this->displayMethodsStats ? StatsVerbosity::HIGH : StatsVerbosity::NONE);
// Parameters
$this->setUpParameters($input);
$this->iniMemoryLimit = (self::$forceIniMemoryLimitTo === null) ? preg_replace('`[^-0-9KMG]`', '', \ini_get('memory_limit')) : self::$forceIniMemoryLimitTo;
// Non-interactive candidates
$this->candidates = $input->getOption('candidates') ?? null;
// Non-interactive votes
$this->votes = $input->getOption('votes') ?? null;
$this->CondorcetElectionFormatPath = $input->getOption('import-condorcet-election-format') ?? null;
$this->DebianFormatPath = $input->getOption('import-debian-format') ?? null;
$this->DavidHillFormatPath = $input->getOption('import-david-hill-format') ?? null;
// Logo
$this->io->newLine();
$this->io->logo($this->terminal->getWidth());
$this->io->newLine();
// Header
$this->io->version();
$this->io->inlineSeparator();
$this->io->author(Condorcet::AUTHOR);
$this->io->inlineSeparator();
$this->io->homepage(Condorcet::HOMEPAGE);
$this->io->newLine(2);
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
if (empty($this->CondorcetElectionFormatPath) && empty($this->DebianFormatPath) && empty($this->DavidHillFormatPath)) {
// Interactive Candidates
if (empty($this->candidates)) {
$this->io->title('Enter the candidates');
$this->io->instruction('Candidates', 'Enter each candidate names');
$registeringCandidates = [];
while (true) {
$answer = $this->io->ask('Please register candidate N°<fg=magenta>'.(\count($registeringCandidates) + 1).'</> <continue>(or press enter to continue)</>');
if ($answer === null) {
break;
} else {
array_push($registeringCandidates, ...explode(';', $answer));
}
}
$this->candidates = implode(';', $registeringCandidates);
}
// Interactive Votes
if (empty($this->votes)) {
$this->io->title('Enter the votes');
$this->io->instruction('Format', 'Candidate B > CandidateName D > CandidateName C = CandidateName A');
$registeringVotes = [];
while (true) {
$answer = $this->io->ask('Please register vote N°<fg=magenta>'.(\count($registeringVotes) + 1).'</> <continue>(or press enter to continue)</>');
if ($answer === null) {
break;
} else {
array_push($registeringVotes, ...explode(';', $answer));
}
}
$this->votes = implode(';', $registeringVotes);
}
// Interactive Methods
if (empty($input->getArgument('methods'))) {
$this->io->title('Enter the methods');
$this->io->instruction('Voting methods', 'Choose by entering their numbers separated by commas. Press enter for the default method.');
$c = 0;
$registeringMethods = [];
$authMehods = Condorcet::getAuthMethods();
$authMehods = array_merge(['ALL'], $authMehods);
$registeringMethods = $this->io->choiceMultiple('Select methods', $authMehods, Condorcet::getDefaultMethod()::METHOD_NAME[0], true);
$input->setArgument('methods', $registeringMethods);
}
if (empty($input->getOption('seats'))) {
$hasProportionalMethods = false;
$methods = FormaterHelper::prepareMethods($input->getArgument('methods'));
foreach ($methods as $oneMethod) {
if ($oneMethod['class']::IS_PROPORTIONAL) {
$hasProportionalMethods = true;
break;
}
}
if ($hasProportionalMethods) {
$this->io->instruction('Number of Seats', 'Some of the method(s) chosen are proportional and require a number of seats.');
$answer = $this->io->ask('Number of seats to fill', (string) 100, static function ($answer): string {
if (!is_numeric($answer)) {
throw new CondorcetInternalException('Seats must be numeric');
}
if (($answer = (int) $answer) < 1) {
throw new CondorcetInternalException('Seats must be a natural number (positive)');
}
return (string) $answer;
});
$input->setOption('seats', $answer);
$this->election->setNumberOfSeats((int) $answer);
}
}
}
}
protected function importInputsData(InputInterface $input): void
{
// Define Callback
$callBack = $this->useDataHandler($input);
// Setup Elections, candidates and votes from Condorcet Election Format
if ($input->getOption('import-condorcet-election-format')) {
$this->parseFromCondorcetElectionFormat($callBack);
$this->setUpParameters($input); # Do it again, because command line parameters have priority
}
if ($input->getOption('import-debian-format')) {
$this->election->setNumberOfSeats(1); # Debian must be 1
$this->parseFromDebianFormat();
$this->setUpParameters($input); # Do it again, because command line parameters have priority
}
if ($input->getOption('import-david-hill-format')) {
$this->parseFromDavidHillFormat();
$this->setUpParameters($input); # Do it again, because command line parameters have priority
}
// Parse Votes & Candidates from classicals inputs
!empty($this->candidates) && $this->parseFromCandidatesArguments();
!empty($this->votes) && $this->parseFromVotesArguments($callBack);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// Start Timer Chrono
$chrono = new Chrono($this->timer, 'CondorcetConsoleCommand');
// Import Inputs
$this->importInputsData($input);
// Inputs Display Section
$this->displayInputsSection();
// Debug Display Section
if ($output->isDebug()) {
$this->displayDebugSection();
}
// Configuration Display Section
$this->displayConfigurationSection();
// Verbose Display Section
if ($output->isVerbose() || $input->getOption('list-votes')) {
$this->displayDetailedElectionInputsSection($input, $output);
}
// Pairwise Display Section
if ($input->getOption('show-pairwise') || $input->getOption('stats')) {
$this->displayPairwiseSection($output);
$this->io->newLine();
}
// NaturalCondorcet Dislay Section
$this->displayNaturalCondorcet($input, $output);
// MethodsResults Display Section
$this->io->newLine();
$this->io->title('Results per methods');
$this->displayMethodsResultSection($input, $output);
// RM Sqlite Database if exist
/**
* @infection-ignore-all
*/
if (($SQLitePath = $this->SQLitePath) !== null) {
$this->election = null;
unlink($SQLitePath);
}
// Timer
unset($chrono);
$this->displayTimerSection();
return Command::SUCCESS;
}
protected function displayInputsSection(): void
{
$messageCandidates = "<condor1>{$this->election->countCandidates()} candidate".($this->election->countCandidates() > 1 ? 's' : '').' registered</>';
$messageVotes = '<condor2>'.number_format($this->election->countVotes(), thousands_separator: ' ').' vote'.($this->election->countVotes() > 1 ? 's' : '').' registered</>';
$this->io->writeln('<comment>'.str_repeat('-', mb_strlen($messageCandidates.$messageVotes) + 4 - 24).'</comment>');
$this->io->write($messageCandidates);
$this->io->inlineSeparator();
$this->io->writeln($messageVotes);
$this->io->newLine();
}
protected function displayDebugSection(): void
{
$this->io->newLine();
$this->io->title('Debug - External Handler Informations');
$this->io->definitionList(
['Ini max_memory' => $this->iniMemoryLimit],
['Votes per Mb' => self::$VotesPerMB],
['Db is used' => (empty($this->SQLitePath)) ? 'no' : 'yes, using path: '.$this->SQLitePath],
['Max Votes in Memory' => $this->maxVotesInMemory],
);
}
protected function displayConfigurationSection(): void
{
$this->io->title('Configuration');
$this->io->definitionList(
['Is vote weight allowed?' => $this->election->isVoteWeightAllowed() ? 'TRUE' : 'FALSE'],
new TableSeparator,
['Votes are evaluated according to the implicit ranking rule?' => $this->election->getImplicitRankingRule() ? 'TRUE' : 'FALSE'],
new TableSeparator,
['Is vote tie in rank allowed?' => \in_array(needle: NoTie::class, haystack: $this->election->getConstraints(), strict: true) ? 'FALSE' : 'TRUE']
);
}
protected function displayDetailedElectionInputsSection(InputInterface $input, OutputInterface $output): void
{
$this->io->title('Detailed Election Inputs');
if ($output->isVerbose()) {
$this->displayVerbose($output);
}
// List-Votes Display Section
if ($input->getOption('list-votes')) {
$this->displayVotesCount($output);
$this->io->newLine();
$this->displayVotesList($output);
$this->io->newLine();
}
}
protected function displayVerbose(OutputInterface $output): void
{
$this->displayCandidatesList($output);
$this->io->newLine();
$this->displayVotesCount($output);
$this->io->newLine();
}
protected function displayCandidatesList(OutputInterface $output): void
{
if (!$this->candidatesListIsWrite) {
// Candidate List
($candidateTable = new Table($output))
->setHeaderTitle('Registered candidates')
->setHeaders(['Num', 'Candidate name'])
->setStyle($this->io->MainTableStyle)
->setColumnStyle(0, $this->io->FirstColumnStyle)
->setColumnWidth(0, 14)
;
$candidate_num = 1;
foreach ($this->election->getCandidatesListAsString() as $oneCandidate) {
$candidateTable->addRow([$candidate_num++, $oneCandidate]);
}
$candidateTable->render();
$this->candidatesListIsWrite = true;
}
}
protected function displayVotesCount(OutputInterface $output): void
{
if (!$this->votesCountIsWrite) {
// Votes Count
($votesStatsTable = new Table($output))
->setHeaderTitle('Stats - votes registration')
->setHeaders(['Stats', 'Value'])
->setColumnStyle(0, (new Tablestyle)->setPadType(\STR_PAD_LEFT))
->setStyle($this->io->MainTableStyle)
;
$formatter = static fn (int $input): string => number_format($input, thousands_separator: ' ');
$votesStatsTable->addRow(['Count registered votes', $formatter($this->election->countVotes())]);
$votesStatsTable->addRow(['Count valid registered votes with constraints', $formatter($this->election->countValidVoteWithConstraints())]);
$votesStatsTable->addRow(['Count invalid registered votes with constraints', $formatter($this->election->countInvalidVoteWithConstraints())]);
$votesStatsTable->addRow(['Sum vote weight', $formatter($this->election->sumVotesWeight())]);
$votesStatsTable->addRow(['Sum valid votes weight with constraints', $formatter($this->election->sumValidVotesWeightWithConstraints())]);
$votesStatsTable->render();
$this->votesCountIsWrite = true;
}
}
protected function displayVotesList(OutputInterface $output): void
{
($votesTable = new Table($output))
->setHeaderTitle('Registered Votes List')
->setHeaders(['Vote Num', 'Vote', 'Weight', 'Vote Tags'])
->setColumnWidth(0, 8)
->setColumnWidth(1, 30)
->setColumnMaxWidth(2, 6)
->setStyle($this->io->MainTableStyle)
;
foreach ($this->election->getVotesValidUnderConstraintGenerator() as $voteKey => $oneVote) {
$votesTable->addRow([($voteKey + 1), $oneVote->getSimpleRanking($this->election, false), $oneVote->getWeight($this->election), implode(',', $oneVote->getTags())]);
}
$votesTable->render();
}
protected function displayPairwiseSection(OutputInterface $output): void
{
if (!$this->pairwiseIsWrite) {
(new Table($output))
->setHeaderTitle('Pairwise')
->setHeaders(['For each candidate, show their win, null, or lose'])
->setRows([[preg_replace('#!!float (\d+)#', '\1.0', Yaml::dump($this->election->getExplicitPairwise(), 100))]])
->setStyle($this->io->MainTableStyle)
->render()
;
$this->pairwiseIsWrite= true;
}
}
protected function displayNaturalCondorcet(InputInterface $input, OutputInterface $output): void
{
if ($input->getOption('natural-condorcet')) {
$this->io->methodResultSection('Condorcet natural winner & loser');
$this->io->newLine();
(new Table($output))
->setHeaderTitle('Natural Condorcet')
->setHeaders(['Type', 'Candidate'])
->setRows([
[CondorcetStyle::CONDORCET_WINNER_SYMBOL_FORMATED.' Condorcet Winner', (string) ($this->election->getCondorcetWinner() ?? '-')],
[CondorcetStyle::CONDORCET_LOSER_SYMBOL_FORMATED.' Condorcet Loser', (string) ($this->election->getCondorcetLoser() ?? '-')],
])
->setStyle($this->io->MainTableStyle)
->render()
;
$this->io->newLine();
}
}
protected function displayMethodsResultSection(InputInterface $input, OutputInterface $output): void
{
$methods = FormaterHelper::prepareMethods($input->getArgument('methods'));
foreach ($methods as $oneMethod) {
$this->io->methodResultSection($oneMethod['name']);
if (isset($oneMethod['class']::$optionQuota) && $input->getOption('quota') !== null) {
$this->election->setMethodOption($oneMethod['class'], 'Quota', StvQuotas::make($input->getOption('quota'))); // @phpstan-ignore-line
}
// Result
$result = $this->election->getResult($oneMethod['name']);
if (!empty($options = $result->getMethodOptions())) {
$rows = [];
foreach ($options as $key => $value) {
if ($value instanceof \BackedEnum) {
$value = $value->value;
} elseif (\is_array($value)) {
$value = implode(' / ', $value);
}
$rows[] = [$key.':', $value];
}
$this->io->newLine();
(new Table($output))
->setHeaderTitle('Configuration: '.$oneMethod['name'])
->setHeaders(['Variable', 'Value'])
->setRows($rows)
->setColumnStyle(0, $this->io->FirstColumnStyle)
->setColumnMaxWidth(0, 30)
->setColumnMaxWidth(0, 40)
->setStyle($this->io->MainTableStyle)
->render()
;
}
$this->io->newLine();
$this->io->write('<condor3>'.CondorcetStyle::CONDORCET_WINNER_SYMBOL_FORMATED.' Condorcet Winner</>');
$this->io->inlineSeparator();
$this->io->writeln('<condor3>'.CondorcetStyle::CONDORCET_LOSER_SYMBOL_FORMATED.' Condorcet Loser</>');
(new Table($output))
->setHeaderTitle('Results: '.$oneMethod['name'])
->setHeaders(['Rank', 'Candidates'])
->setRows(FormaterHelper::formatResultTable($result))
->setColumnWidth(0, $fcw = 20)
->setColumnWidth(1, 50)
->setColumnMaxWidth(1, ($this->terminal->getWidth() - $fcw - 10 - 3))
->setColumnStyle(0, $this->io->FirstColumnStyle)
->setStyle($this->io->MainTableStyle)
->render()
;
// Stats
if ($this->displayMethodsStats) {
$table = (new Table($output))
->setHeaderTitle('Stats: '.$oneMethod['name'])
->setColumnWidth(0, 73)
->setColumnMaxWidth(0, $this->terminal->getWidth() - 10)
// ->setColumnStyle(0, $this->io->FirstColumnStyle)
->setStyle($this->io->MainTableStyle)
;
$line = 0;
foreach ($result->getStats() as $oneStatKey => $oneStatEntry) {
++$line !== 1 && $table->addRow(new TableSeparator);
$table->addRow([preg_replace('#!!float (\d+)#', '\1.0', Yaml::dump([$oneStatKey => $oneStatEntry], 100))]);
}
$table->render();
}
}
}
protected function displayTimerSection(): void
{
$executionTime = round($this->timer->getGlobalTimer(), 4);
$this->io->newLine();
$this->io->writeln('<comment>'.str_repeat('-', 23).'</comment>', OutputInterface::VERBOSITY_VERBOSE);
$this->io->writeln("<comment>Execution Time: {$executionTime}s</comment>", OutputInterface::VERBOSITY_VERBOSE);
$this->io->newLine();
}
# Processing
protected function setUpParameters(InputInterface $input): void
{
/// Implicit Ranking
if ($input->getOption('deactivate-implicit-ranking')) {
$this->election->setImplicitRanking(false);
}
// Allow Votes Weight
if ($input->getOption('allows-votes-weight')) {
$this->election->allowsVoteWeight(true);
}
if ($input->getOption('seats') && ($seats = (int) $input->getOption('seats')) >= 1) {
$this->election->setNumberOfSeats($seats);
}
try {
if ($input->getOption('no-tie')) {
$this->election->addConstraint(NoTie::class);
}
} catch (VoteConstraintException $e) {
str_contains($e->getMessage(), 'class is already registered') || throw $e;
}
}
protected function parseFromCandidatesArguments(): void
{
if ($file = CommandInputHelper::getFilePath($this->candidates)) {
$this->election->parseCandidates($file, true);
} else {
$this->election->parseCandidates($this->candidates);
}
}
protected function parseFromVotesArguments(\Closure $callBack): void
{
// Parses Votes
if ($file = CommandInputHelper::getFilePath($this->votes)) {
$this->election->parseVotesWithoutFail(input: $file, isFile: true, callBack: $callBack);
} else {
$this->election->parseVotesWithoutFail(input: $this->votes, isFile: false, callBack: $callBack);
}
}
protected function parseFromCondorcetElectionFormat(\Closure $callBack): void
{
$file = CommandInputHelper::getFilePath($this->CondorcetElectionFormatPath);
if ($file !== null) {
(new CondorcetElectionFormat($file))->setDataToAnElection($this->election, $callBack);
} else {
throw new FileDoesNotExistException('File does not exist, path: '.$this->CondorcetElectionFormatPath);
}
}
protected function parseFromDebianFormat(): void
{
$file = CommandInputHelper::getFilePath($this->DebianFormatPath);
if ($file !== null) {
(new DebianFormat($file))->setDataToAnElection($this->election);
} else {
throw new FileDoesNotExistException('File does not exist, path: '.$this->CondorcetElectionFormatPath);
}
}
protected function parseFromDavidHillFormat(): void
{
$file = CommandInputHelper::getFilePath($this->DavidHillFormatPath);
if ($file !== null) {
(new DavidHillFormat($file))->setDataToAnElection($this->election);
} else {
throw new FileDoesNotExistException('File does not exist, path: '.$this->CondorcetElectionFormatPath);
}
}
protected function useDataHandler(InputInterface $input): ?\Closure
{
if ($input->getOption('deactivate-file-cache') || !class_exists('\PDO') || !\in_array(needle: 'sqlite', haystack: \PDO::getAvailableDrivers(), strict: true)) {
return null;
} else {
if ($this->iniMemoryLimit === '-1') {
$memoryLimit = 8 * (1000 * 1048576); # Limit to 8GB, use a true memory limit to go further
} else {
$memoryLimit = (int) preg_replace('`[^0-9]`', '', $this->iniMemoryLimit);
$memoryLimit *= match (mb_strtoupper(mb_substr($this->iniMemoryLimit, -1, 1))) {
'K' => 1024,
'M' => 1048576,
'G' => (1000 * 1048576),
default => 1
};
}
$memoryLimit = (int) ($memoryLimit / 1048576);
$this->maxVotesInMemory = self::$VotesPerMB * $memoryLimit;
$callBack = function (int $inserted_votes_count): bool {
if ($inserted_votes_count > $this->maxVotesInMemory) {
/**
* @infection-ignore-all
*/
if (file_exists($this->SQLitePath = getcwd().\DIRECTORY_SEPARATOR.'condorcet-bdd.sqlite')) {
unlink($this->SQLitePath);
}
/**
* @infection-ignore-all
*/
$this->election->setExternalDataHandler(new PdoHandlerDriver(new \PDO('sqlite:'.$this->SQLitePath, '', '', [\PDO::ATTR_PERSISTENT => false]), true));
return false; // No, stop next iteration
} else {
return true; // Yes, continue
}
};
return $callBack;
}
}
}

View File

@ -0,0 +1,97 @@
<?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;
use Symfony\Component\Console\Application as SymfonyConsoleApplication;
use CondorcetPHP\Condorcet\Condorcet;
use CondorcetPHP\Condorcet\Console\Commands\ElectionCommand;
use CondorcetPHP\Condorcet\Throwable\Internal\NoGitShellException;
abstract class CondorcetApplication
{
public static SymfonyConsoleApplication $SymfonyConsoleApplication;
/**
* @infection-ignore-all
*/
public static function run(): void
{
// Run
self::create() && self::$SymfonyConsoleApplication->run();
}
public static function create(): bool
{
// New App
self::$SymfonyConsoleApplication = new SymfonyConsoleApplication('Condorcet', Condorcet::getVersion());
// Election command
$command = new ElectionCommand;
self::$SymfonyConsoleApplication->add($command);
self::$SymfonyConsoleApplication->setDefaultCommand($command->getName(), false);
return true;
}
public static function getVersionWithGitParsing(): string
{
$git = static function (string $path): string {
if (!is_dir($path . \DIRECTORY_SEPARATOR . '.git')) {
throw new NoGitShellException('Path is not valid');
}
$process = proc_open(
'git describe --tags --match="v[0-9]*\.[0-9]*\.[0-9]*"',
[
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
],
$pipes,
$path
);
if (!\is_resource($process)) {
throw new NoGitShellException;
}
$result = trim(stream_get_contents($pipes[1]));
fclose($pipes[1]);
fclose($pipes[2]);
$returnCode = proc_close($process);
if ($returnCode !== 0) {
throw new NoGitShellException;
}
return $result;
};
$applicationOfficialVersion = Condorcet::getVersion();
try {
$version = $git(__DIR__.'/../../');
$commit = explode('-', $version)[2];
$match = [];
preg_match('/^v([0-9]+\.[0-9]+\.[0-9]+)/', $version, $match);
$gitLastestTag = $match[1];
$version = (version_compare($gitLastestTag, $applicationOfficialVersion, '>=')) ? $version : $applicationOfficialVersion.'-(dev)-'.$commit;
} catch (NoGitShellException) { // Git no available, use the Condorcet Version
$version = $applicationOfficialVersion;
}
return $version;
}
}

View File

@ -0,0 +1,31 @@
<?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\Helper;
use CondorcetPHP\Condorcet\{Condorcet, Election, Result};
abstract class CommandInputHelper
{
public static function getFilePath(string $path): ?string
{
if (self::pathIsAbsolute($path) && is_file($path)) {
return $path;
} else {
return (is_file($file = getcwd().\DIRECTORY_SEPARATOR.$path)) ? $file : null;
}
}
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)));
}
}

View File

@ -0,0 +1,67 @@
<?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\Helper;
use CondorcetPHP\Condorcet\Console\Style\CondorcetStyle;
use CondorcetPHP\Condorcet\{Condorcet, Result};
abstract class FormaterHelper
{
public static function formatResultTable(Result $result): array
{
$resultArray = $result->getResultAsArray(true);
foreach ($resultArray as $rank => &$line) {
if (\is_array($line)) {
$line = implode(',', $line);
}
if ($rank === 1 && \count($result[1]) === 1 && $result[1][0] === $result->getCondorcetWinner()) {
$line .= ' '.CondorcetStyle::CONDORCET_WINNER_SYMBOL_FORMATED;
} elseif ($rank === max(array_keys($resultArray)) && \count($result[max(array_keys($resultArray))]) === 1 && $result[max(array_keys($resultArray))][0] === $result->getCondorcetLoser()) {
$line .= ' '.CondorcetStyle::CONDORCET_LOSER_SYMBOL_FORMATED;
}
$line = [$rank, $line];
}
return $resultArray;
}
public static function prepareMethods(array $methodArgument): array
{
if (empty($methodArgument)) {
return [['name' => Condorcet::getDefaultMethod()::METHOD_NAME[0], 'class' => Condorcet::getDefaultMethod()]];
} else {
$methods = [];
foreach ($methodArgument as $oneMethod) {
if (mb_strtolower($oneMethod) === 'all') {
$methods = Condorcet::getAuthMethods(false);
$methods = array_map(static fn ($m) => ['name' => $m, 'class' => Condorcet::getMethodClass($m)], $methods);
break;
}
if (Condorcet::isAuthMethod($oneMethod)) {
$method_class = Condorcet::getMethodClass($oneMethod);
$method_name = $method_class::METHOD_NAME[0];
if (!\in_array(needle: $method_name, haystack: $methods, strict: true)) {
$methods[] = ['name' => $method_name, 'class' => $method_class];
}
}
}
return $methods;
}
}
}

View File

@ -0,0 +1,163 @@
<?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\Style;
use CondorcetPHP\Condorcet\Console\CondorcetApplication;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\TableStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
class CondorcetStyle extends SymfonyStyle
{
public const CONDORCET_MAIN_COLOR = '#f57255';
public const CONDORCET_SECONDARY_COLOR = '#8993c0';
public const CONDORCET_THIRD_COLOR = '#e3e1e0';
public const CONDORCET_WINNER_SYMBOL = '★';
public const CONDORCET_LOSER_SYMBOL = '⚐';
public const CONDORCET_WINNER_SYMBOL_FORMATED = '<fg=#ffff00>'.self::CONDORCET_WINNER_SYMBOL.'</>';
public const CONDORCET_LOSER_SYMBOL_FORMATED = '<fg=#33beff>'.self::CONDORCET_LOSER_SYMBOL.'</>';
public readonly TableStyle $MainTableStyle;
public readonly TableStyle $FirstColumnStyle;
public function __construct(InputInterface $input, OutputInterface $output)
{
parent::__construct($input, $output);
$output->getFormatter()->setStyle('condor1', new OutputFormatterStyle(foreground: self::CONDORCET_MAIN_COLOR));
$output->getFormatter()->setStyle('condor2', new OutputFormatterStyle(foreground: self::CONDORCET_SECONDARY_COLOR));
$output->getFormatter()->setStyle('condor3', new OutputFormatterStyle(foreground: self::CONDORCET_THIRD_COLOR));
$output->getFormatter()->setStyle('condor1b', new OutputFormatterStyle(foreground: self::CONDORCET_MAIN_COLOR, options: ['bold']));
$output->getFormatter()->setStyle('condor2b', new OutputFormatterStyle(foreground: self::CONDORCET_SECONDARY_COLOR, options: ['bold']));
$output->getFormatter()->setStyle('condor3b', new OutputFormatterStyle(foreground: self::CONDORCET_THIRD_COLOR, options: ['bold']));
$output->getFormatter()->setStyle('continue', new OutputFormatterStyle(foreground: '#008080'));
$output->getFormatter()->setStyle('comment', new OutputFormatterStyle(foreground: '#fed5b7'));
$this->MainTableStyle = (new TableStyle)
->setBorderFormat('<condor1>%s</>')
->setHeaderTitleFormat('<fg='.self::CONDORCET_THIRD_COLOR.';bg='.self::CONDORCET_SECONDARY_COLOR.';options=bold> %s </>')
->setCellHeaderFormat('<condor2>%s</>')
->setCellRowFormat('<condor3>%s</>')
;
$this->FirstColumnStyle = (new TableStyle)
->setPadType(\STR_PAD_BOTH)
// ->setCellRowFormat('<condor1>%s</>') # Buggy
;
}
public function logo(int $terminalSize): void
{
$path = __DIR__.\DIRECTORY_SEPARATOR.'..'.\DIRECTORY_SEPARATOR.'Assets'.\DIRECTORY_SEPARATOR;
if ($terminalSize >= 125) {
$path .= 'logo.125c.ascii';
} elseif ($terminalSize >= 90) {
$path .= 'logo.90c.ascii';
} else {
$path .= 'logo.73c.ascii';
}
$this->writeln(file_get_contents($path));
}
public function author(string $author): void
{
$this->write("<condor1b>Author:</> <condor2>{$author}</>");
}
public function choiceMultiple(string $question, array $choices, mixed $default = null, bool $multi): mixed
{
if ($default !== null) {
$values = array_flip($choices);
$default = $values[$default] ?? $default;
}
$questionChoice = new ChoiceQuestion($question, $choices, $default);
$questionChoice->setMultiselect($multi);
return $this->askQuestion($questionChoice);
}
public function homepage(string $homepage): void
{
$this->write("<condor1b>Homepage:</> <condor2><href={$homepage}>{$homepage}</></>");
}
public function inlineSeparator(): void
{
$this->write('<condor3> || </>');
}
public function instruction(string $prefix, string $message): void
{
$this->writeln("<condor1b>{$prefix}:</> <condor2>{$message}</>");
}
public function section(string $message): void
{
$this->block(
messages: $message,
type: null,
style: 'fg='.self::CONDORCET_MAIN_COLOR.';bg='.self::CONDORCET_SECONDARY_COLOR.';options=bold',
padding: false
);
}
public function methodResultSection(string $message): void
{
$messageLength = mb_strlen($message) + 4;
$prefixLength = 15;
$totalLength = $messageLength + $prefixLength;
$totalBorderLength = $totalLength + 4;
$horizontalBorder = '<condor2>'.str_repeat('=', (int) $totalBorderLength).'</>';
$vbs = '<condor2>|</>';
$spaceMessage = '<bg='.self::CONDORCET_MAIN_COLOR.'>'.str_repeat(' ', $messageLength).'</>';
$spacePrefix = '<bg='.self::CONDORCET_SECONDARY_COLOR.'>'.str_repeat(' ', $prefixLength).'</>';
$bande = "{$vbs} ".$spacePrefix.$spaceMessage." {$vbs}";
$this->writeln($horizontalBorder);
$this->writeln($bande);
$this->writeln("{$vbs} <bg=".self::CONDORCET_SECONDARY_COLOR.';options=bold> Vote Method </><bg='.self::CONDORCET_MAIN_COLOR.";options=bold> {$message} </> {$vbs}");
$this->writeln($bande);
$this->writeln($horizontalBorder);
}
public function note(string|array $message): void
{
$this->block(messages: $message, style: 'fg=gray');
}
public function success(string|array $message): void
{
$this->block(
messages: $message,
type: 'OK',
style: 'fg='.self::CONDORCET_MAIN_COLOR.';bg='.self::CONDORCET_SECONDARY_COLOR.';options=bold',
padding: true
);
}
public function version(): void
{
$version = CondorcetApplication::getVersionWithGitParsing();
$this->write("<condor1b>Version:</> <condor2>{$version}</>");
}
}